Java内存区域学习
文章出处:原文地址
运行时区域
Java虚拟机在执行Java程序过程中把其所管理的内存划分成若干个不同的数据区域。
程序计数器
当前线程所执行的字节码的指示器。通过改变这个计数器的值来选取下一个需要执行的字节码指令,分支、循环、跳转、异常、线程恢复等都需要这个计数器完成。
每个线程一个独立的程序计数器,各线程之间互不影响,独立存储。
执行Java方法时:正在执行虚拟字节码的指令地址 执行Native方法:值为空(Undefined)
唯一一个Java虚拟机规范中没有规定OutOfMemoryError的区域
虚拟机栈
线程私有,生命周期和线程一致。
描述的是Java方法执行的内存模型:方法执行同时创建一个栈帧,方法从调用到执行完成,就是一个栈帧在虚拟机栈中入栈到出栈的过程。
其中栈帧用于存储:局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表中存放了编译期间可知的基本数据类型和对象引用。
64位的long和double类型的数据会占2个局部空间变量,其余类型只占1个。因局部变量表中所需内存是在编译期间完成的,所以这个方法在帧中需要分配多少局部变量空间是确定的。
这部分区域异常:StackOverFlowError和OutOfMemoryError 、
本地方法栈
与虚拟机栈的作用类似。本地方法栈是为Native方法服务。抛出异常与虚拟机栈一致。
堆
堆是Java虚拟机中内存最大的一块。线程共享,虚拟机启动时创建。存放对象实例。
几乎所有的对象都在这里分配内存。【随着JIT编译器的发展和逃逸分析技术的成熟,栈上分配和标量替换,对象不一定在堆中分配】
堆是垃圾收集管理器的主要区域,也叫GC堆。细分为:新生代、老年代。eden->from survivor->to survivor
通过-Xmx和-Xms控制扩展,无法扩展时抛出OutOfMemoryError异常
方法区
线程共享。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码。也叫非堆,也称永久代。
-XX:MaxPermSize
内存回收目标:常量池的回收和类的卸载
OutOfMemoryError
运行时常量池
方法区的一部分
OutOfMemoryError
直接内存
不是虚拟机运行时区的一部分。
JDK1.4新加入的NIO类,引入Channel和Buffer的I/O方式,使用Native函数库直接分配堆外内存,使用DirectByteBuffer作为这块内存的引用进行操作。
受本机内存大小和处理器寻址空间的限制
OutOfMemoryError
虚拟机中的对象
对象的创建
当遇到new指令时
首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用。【并检查该符号引用代表的类是否已被加载、解析、初始化过,若没有则执行类加载】 类加载通过之后,为新生对象分配内存 内存分配完成后,需将分配到的内存空间都初始化为零值,不包括对象头 接下来,虚拟机要对对象进行必要的设置,设置对象头。如这个对象是那个类的实例。如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
分配内存方式:
指针碰撞 空闲列表
当使用Serial、ParNew等带compact过程的收集器时:分配算法是指针碰撞
使用CMS基于Mark-Sweep算法的收集器时:分配算法采用空闲列表
分配对象内存空间并发下线程安全问题:
采用CAS加上失败重试保证更新的原子性 把内存分配动作按线程划分在不同的空间进行,即每个线程在Java堆中预先分配一块小内存,成为本地线程分配缓冲区【TLAB】 -XX:+、-UseTLAB
对象创建完成之后还有进行init,按照程序员的意愿进行初始化。
对象内存布局
对象内存布局分为三块
对象头、实例数据、对齐填充
对象头
对象头包含两部分:
存储对象自身的运行数据 类型指针
对象自身的运行数据
包含哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等。也称Mrak Word
类型指针
对象指向它的类元数据的指针。虚拟机通过这个指针来确定对象是属于哪个类的实例
对象实例数据部分
对象真正存储的有效信息,各种类型的字段内容。
对齐填充部分
不是必然存在的。起着占位符的作用。
对象起始地址必须是8字节的整数倍。当对象实例数据部分没有对齐时,就需要对齐填充了。
对象的访问定位
对象的访问定位目前有两种流行的方式:
使用句柄 直接指针
句柄
直接指针
对比
使用句柄好处:栈中引用存储的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,引用本身不需变化
使用直接引用好处:速度快,节省一次指针定位的时间开销