JVM - Java的内存区域 & 对象布局
虚拟机内存的各个部分
如上图所示,JVM运行时内存可以分为堆,栈,非堆。
栈
栈是线程私有的,里面主要包括了程序计数器(Program Counter), 虚拟机栈(VM Stack)和本地方法栈(Native Method Stack)。
- 程序计数器
可以看做是当前线程执行字节码的行号指示器,程序的分支,循环,跳跃,异常处理,以及线程切换后恢复到正确执行位置都依赖此计数器完成。 - 虚拟机栈
每个方法在被执行的时候,虚拟机都会创建一个栈帧(Stack Frame)。一个方法被调用的过程就是一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧里保存的就是局部变量表,操作数栈,动态连接,方法出口等信息。 - 本地方法栈
本地方法栈和虚拟机栈的功能作用相似,只是虚拟机栈是为了虚拟机执行Java字节码服务;本地方法栈是为了虚拟机调用本地方法服务。
堆
堆,就是我们垃圾回收的主要部分,是线程共享的空间。Java中所有的实例对象都是在堆上进行分配空间的。按照一般的分代理论,可以分为新生代(Young Gen)和老年代(Old Gen)。对新生代的GC被叫做Minor GC, 对老年代的GC叫做Major GC,整个堆的GC叫做Full GC。新生代又被分为eden区,s0,s1。
非堆
除了堆栈,还有一部分非堆的部分,叫做方法区,以前通过永久代来实现方法区。这部分空间也是线程共享的,主要包括了运行时常量池部分。
对象的内存布局
我们知道堆是用来给对象分配空间的,而每个对象在堆上的内存结构是怎样的呢?一般是分为三个部分:对象头(Header), 实例数据(Instance Data)和对齐填充(Padding)。
对象头
对象头分为三部分,一个是Mark Word,用来存储对象自身运行时的数据,如哈希码,GC分代年龄等等;一个是类型指针,指向类型元数据的指针;一个是数组长度,如果该对象是一个数组的情况下。对象头是一个动态数据结构,在未锁定和锁定状态下的大小都是不一样的,
Mark Word在我们说并发,
synchronized
, 锁升级的时候都有涉及
实例数据
存储对象真正有意义的有效信息
对齐填充
为了使得对象大小都是8字节的整数倍而存在。
对象分配策略
对象优先在eden区分配
大部分情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间时,虚拟机发起Minor GC。
大对象直接进入老年代
大对象:需要大量连续内存空间的Java对象,例如很长的字符串或者元素数量很大的数组。
大对象对虚拟机内存分配来说是个坏消息,而更坏的消息是遇到一群“朝生夕灭”的大对象。因为大对象可能会导致内存明明还有不少空间就不得已提前触发GC;移动大对象的开销等等。通过-XX:PretenureSizeThreshold
参数,指定大于该值的在老年代分配。也可以避免两个s区来回复制的开销。
长期存活对象晋升老年代
大部分情况,对象会首先在eden区分配,在一次垃圾回收后如果对象还存活就会进入s0或s1(s0,s1总有一块是空的,假设现在是s1是空的,回收后eden区存活的对象和s0存活的对象都会通过“标记复制”算法移动到s1,那么此时s0是空的,供下次GC用),在s0,s1中待得比较久的对象会晋升到老年代(待得比较久:年龄超过“平均年龄”,平均年龄:s区中相同年龄对象大小总和大于s区一半的年龄,或者年龄大于-XX:MaxTenuringThreshold
,一次GC还存活年龄就+1)。
空间分配担保机制
发生Minor GC前, 虚拟机检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,那么此次GC是安全的。如果不大于,JVM会先check -XX:HandlePromotionFailure
是否允许“担保失败”,如果允许,就检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,就尝试Minor GC,如果小于,或者不允许“担保失败”则进行Full GC,如下图: