Java虚拟机:垃圾收集与内存分配策略
一、确认对象是否存活:
垃圾收集器在对堆进行回收前,首先要做的事情就是确定对象是否还存活,判断对象是否存活主要有两种算法:引用计数算法和可达性分析算法。
1、引用计数算法:对象创建时,给对象添加一个引用计数器,每当有一个地方引用到它时,计数器值加1,;引用失效时,计数器值减1;当计数值值为0时,这个对象就是不可能再被引用的。
2、可达性分析算法:以“GC Roots”对象为起点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连接时,则证明此对象是不可用的。
3、GC Roots对象包括:
(1)虚拟机栈(栈帧中的本地变量表)中引用的对象;
(2)方法区中类静态属性引用的对象;
(3)方法区中常量引用的对象
(4)本地方法栈中JVM(Native)引用的对象。
4、对象引用类别:4种,关联强度向下递减。
(1)强引用:GC不会回收强引用的对象。Object obj = new Object();就是一个强引用。
(2)软引用:系统在发生内存溢出异常之前,会把这些对象列进回收范围之中,进行第2次回收。
(3)弱引用:被弱引用关联的对象,只能生存到下一次垃圾收集之前。
(4)虚引用:目的是能在对象被回收时收到一个系统通知。
二、对象的回收经历:
对象在真正死亡,需要经历两个阶段:
(1)可达性分析后,没有与GC Roots相连接的引用链,会被第一次标记并筛选。如果对象没有覆盖finalize()方法或已经调用finalize()方法,则不会调用finalize()方法。如果需要调用finalize()方法,则对象会被放在F-Queue队列中,等待线程执行。
(2)若对象想要存活下来,finalize()方法是最后的机会,只需在finalize()方法中重新与引用链上的对象相关联,否则,GC对F-Queue队列进行第二次小规模标记后对象真正死亡。
三、垃圾收集算法:
1、标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点:效率低,会产生大量不连续的内存碎片。
2、复制算法:将可用内存划分成大小相等的两块,每次只使用其中的一块,当这块的内存用完时,就将还存活的对象复制到另一块内存中,然后再把原来的内存空间清理掉。缺点:内存缩小为原来的一半。
3、标记-整理算法:首先标记出需要回收的对象,接着将所有存活的对象都向一端移动,然后清理掉端边界以外的内存。
4、分代收集算法:根据各个年龄代的特点选择合适的收集算法。在新生代中,每次垃圾收集都有大量的对象死去,因此采用复制算法。老年代中,因为对象的存活率高,没有额外的空间对他进行担保,因此使用“标记-清除”和“标记-整理”算法。
四、内存分配与回收策略:
1、对象优先在Eden分配:当Eden没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
2、大对象直接进入老年代:避免Eden区及两个Survivor区之间发生大量的内存复制。
3、长期存活的对象将进入老年代:对象在Eden区出生,并经过一次Minor GC后仍存活,年龄加1,若年龄超过阈值(默认15),则被晋升到老年代。
4、动态年龄判断:Survivor空间中相同年龄所有对象大小大于Survivor空间的一半,年龄大于或等于该年龄的对象直接进入老年代。
5、空间分配担保:Minor GC前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间,若成立,则Minor GC是安全的。若不成立,则检查是否允许担保失败,如果允许,检查老年代最大可用连续空间是否大于历次晋升到老年代的平均大小,大于,则尝试进行Minor GC;如果小于或者不允许冒险,则Full GC。
补充说明:HotSpot虚拟机把年轻代分成三部分:一个Eden区和两个Survivor区(即From区和To区),比例为8:1:1。在GC开始的时候,对象会存在Eden和From区,To区是空的,进行GC时,Eden区存活的对象会被复制到To区,From区存活的对象会根据年龄值决定去向,阈值达到一定值的对象会被移动到老年代中,没有达到阈值的对象会被复制到To。这时Eden区和From区已经被清空了。接下来From区和To区交换角色,以保证To区在GC开始时是空的。Minor GC会一直重复这样的过程,直到To区被填满,To被填满之后,会将所有对象移动到老年代中。