深入理解JVM(三)垃圾收集器和内存分配策略

时间:2022-04-08 12:16:55

3.1 关于垃圾收集和内存分配

  垃圾收集和内存分配主要针对的区域是Java虚拟机中的堆和方法区;

3.2 如何判断对象是否“存活”(存活判定算法)

  垃圾收集器在回收对象前判断其是否“存活”的两个算法:

  1.引用计数算法:一个对象在被引用之后这个计数器就加1,不被引用之后则减1,如果是0,那么就被回收,这个一般不被主流Java虚拟机所使用,原因:对象的循环引用会导致计数器始终不为0,那么就无法回收。

  2.可达性分析算法:通过GC roots对象作为起点,向下搜索和它进行有效链接的对象,如果对象最终不和GC roots所链接,那么它(们)将会被回收。GC roots对象:①栈中的引用;②方法区中的引用;③本地方法中的引用;

深入理解JVM(三)垃圾收集器和内存分配策略

  3.对象的引用:

    ①强引用:只要引用A a = new A()存在,就不会被回收,这就是强引用;

    ②软引用:可能会被回收的引用,如果内存不足则会进行回收,通过实现SoftReference类;

    ③弱引用:只能活到下一次垃圾收集之前,比软引用更弱,通过实现WeakReference类;

    ④虚引用:最弱的引用,此引用无法通过引用获得对象,作用就是此引用的对象在被回收时会收到一个系统通知;

  4.对象的存亡与finalize():当对象没有重写finalize()方法或虚拟机已经执行过finalize()方法时(finalize()方法只能执行一次),那么这个对象会“不需要执行finalize()”,否则对象会被加入到一个低优先级(多线程的)的队列去执行finalize()方法,finalize()方法是对象被回收前最后的挣扎,如果对象能够再被引用则能自救不被回收,但是最好不用这个方法,因为其不确定性很强。

  5.方法区的回收:一般来说虚拟机可以不实现方法区(老年代)的回收,因为其回收性价比低,不如堆的回收效率高(75%以上),但是有一些大量使用反射等机制的情况下就需要了。回收常量还比较轻松,而回收class的条件苛刻:①这个class的实例对象已被回收;②加载这个class的ClassLoader已被回收;③class对象本身没有再被引用;然后才可以被回收,而且要设置一些参数才行。

3.3 垃圾收集算法:

  1.标记-清理算法:先标记对象再清理,缺点:效率低,清理后的空间碎片化,容易出现连续空间不足问题,后续的算法在此算法上进行改良;

  2.复制算法:分两块区域,将存活的对象按顺序复制到另一块内存中,然后清理掉此内存。主要用于清理新生代,因为新生代的存活时间不长,一般商业虚拟机中,80%的Eden内存空间用于存放新生代,另外两个Survivor空间分别占据10%的空间,每次都将存活的新生代对象从Eden和一块Survivor中复制到另一个Survivor空间中,然后进行清理,但是不保证每次都能保证Survivor空间足够大,所以需要一块分配担保的空间来应对Survivor空间不够大的问题;

  3.标记-整理算法:从标记-清理算法优化而来,先标记对象,清理之后对内存空间进行整理,存活的对象往一端移动,然后清理掉边界外的对象,此算法主要用于存活较久的老年代;

  4.分代收集算法:根据对象存活周期将内存划分为几块,一般就是新生代和老年代。然后根据不同的代来选择不同的算法,新生代使用复制算法,老年代使用标记-整理算法。

3.4 HotSpot的算法实现(如何发起内存回收):

  1.枚举根节点:GC roots,GC时虚拟机“停顿”,HotSpot使用OopMap保存引用的起始位置和偏移量,避免了一个不漏地检查执行上下文和全局引用位置;(总结:判断对象的引用存在)

  2.安全点(Safepoint):程序执行到安全点时才会停顿更新OopMap进行GC,安全点所在位置:程序指令需要较长时间执行的,如方法调用、循环跳转、异常跳转,线程在安全点中断策略:①抢先式中断:GC时中断所有线程,判断是否到达安全点,如果没有则回复线程让它继续到安全点;②主动式中断:在安全点上加一个标记,线程会轮训这个标记,如果有这个标记,则GC。(总结:判断在哪里进行GC);

  3.安全区域(Safe Region):安全点需要线程的运行,但是当线程不运行时(sleep)需要GC怎么办呢,则通过安全区域,线程执行到安全区域时会做标识,安全区域内的对象的引用是不会发生改变的(安全区域特征),所以GC对安全区域内引用的对象没有影响;

3.5 垃圾收集器:

深入理解JVM(三)垃圾收集器和内存分配策略

  垃圾收集器没有绝对的优劣,在不同的情况下有不同的表现

  1.Serial:最基本的、发展最久的,GC年轻代,单一而高效但是线程停顿,GC时停止所有其他线程(Stop The World);适用:Client模式;

深入理解JVM(三)垃圾收集器和内存分配策略

  2.ParNew:Serial的多线程版,其他方面基本上没有区别,GC年轻代;适用:Server模式,适用于多核CPU的计算机;配置:限制线程数-XX:ParallelGCThreads() ;

深入理解JVM(三)垃圾收集器和内存分配策略

  3.Parallel Scavenge:并行的GC收集器,GC年轻代,可以控制吞吐量的GC收集器;配置:①-XX:MaxGCPauseMills 最大停顿毫秒数,牺牲新生代内存大小,来提高速度,但会导致吞吐量减少;②-XX:GCTimeRatio 垃圾收集的时间比重;③-XX:+UserAdaptiveSizePolicy 开关参数,开启之后就无需配置新生代大小(-Xmn)、Eden和Survivor区的比重(-XX:SurvivorRatio)、晋升老年代的年龄(-XX:PretenureSizeThreshold),注意开启此配置之后给堆分配好最大内存(-Xmx);

深入理解JVM(三)垃圾收集器和内存分配策略

  小结:以上三种GC收集器都是用于回收年轻代的,回收的算法都是用的复制算法;

  4.Serial Old:是Serial的GC老年代的版本,回收的算法是标记-整理算法;

  5.Parallel Old:并行的GC收集器,是Parallel Scavenge的老年代版本,和Parallel Scavenge组成“吞吐量优先”组合;

  6.CMS(Concurrent Mark Sweep):并发GC收集器

    (1)优点:这是一个以停顿时间最短为目标的GC收集器,对于需要快速响应的系统来说非常;

    (2)缺点:

    ①需要和运行线程抢占资源,会降低吞吐量,对单核计算机不友好;

    ②无法处理浮动垃圾,可能导致“Concurrent Mode Falure”然后进行Full GC,浮动垃圾是伴随CMS处理过程中产生的新的对象,如果下次运行的时候,老年代占用太高而导致CMS自身需要的内存不足时就会出现“Concurrent Mode Falure”然后使用备用的Serial Old收集器,配置:-XX:+CMSInitiationOccupancyFraction 配置老年代GC的内存阈值百分比;

    ③产生碎片空间,由于其算法是标记-清理,所以出现大对象时可能连续空间不足,就会提前触发Full GC,通过配置:-XX:+UserCMSCompactAtFullCollection(默认开启的)CMS顶不住时整理碎片(此处需要停顿无法并发)、-XX:CMSFullGCsBeforeCompaction:多少次Full GC之后才进行压缩整理(默认0,每次都整理);

深入理解JVM(三)垃圾收集器和内存分配策略

  7.G1(Garbage-first):G1收集器是一个最新的研究成果,G1是一个面向服务端的收集器

    (1)它是一个并行并发的GC收集器,可降低“Stop-The-World”的停顿时间,可同时收集年轻代和老年代;

    (2)整理空间:总的来说使用“标记-整理”算法,而在Region区域之间采用复制算法,高效地保证了没有碎片产生;

    (3)可预测的停顿:可以指定M毫秒内,GC停顿的时间不超过N毫秒;

    (4)内存分区,优先收集优先列表前列的Region(G1收集器名字的由来),使用Remambered Set保存有互相引用的区域地址,针对这个Set的区域会进行GC;

深入理解JVM(三)垃圾收集器和内存分配策略

  8.GC日志:-XX:+PrintGCDetails 打印GC日志的配置

33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]

100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

    (1)33.125:从虚拟机启动到这次GC的秒数;

    (2)Full GC:代表发生了停顿“Stop-The-World”的GC;

    (3)DefNew:不同的收集器对不同的新生代和老年代有不同的叫法,这里是新生代;

    (4)3324K->152K(3712K):回收前占用内存大小 -> 回收后占用内存大小(总的内存大小);

    (5)0.0031680:此次GC的秒数;

  9.GC配置参数总结

深入理解JVM(三)垃圾收集器和内存分配策略

3.6 内存分配与回收策略

  1.对象优先在Eden分配:

    (1)Minor GC:在新生代中的GC,一般速度都很快,且频率高;

    (2)Major GC/Full GC:在老年代中的GC,经常伴随至少一次Minor GC,速度慢,频率低,速度比Minor GC慢10倍以上;

  2.大对象直接进入老年代:

    (1)大对象:例如:很长字符串,byte数组;

    (2)-XX:PretenureSizeThreshold xx:超过xx字节的对象直接进入到老年代,只有Serial和ParNew GC收集器可以进行此配置,必要的情况下可以使用ParNew + CMS的组合;

  3.长期存活的对象进入老年代:

    -XX:MaxTenuringThreshold=xx:对象在新生代经历过xx次Minor GC还没有被回收,那么此对象进入老年代;

  4.动态对象年龄判定:

    当某一年龄的所有对象占据的Survivor空间大于一半时,年龄大于这些对象的对象则会直接进入到老年代,而无需等到-XX:MaxTenuringThreshold的年龄;

  5.空间分配担保:

    (1)Minor GC前都会有一次对老年代最大连续空间是否大于新生代所有对象总空间的判断,如果大于,则看是否开启HandlePromptionFailure是否为true,如果是,则检查老年代最大连续空间是否大于可晋升老年代的对象的总空间,如果大于则进行一次有风险的Minor GC,否则如果为false或者老年代最大连续空间小于时,则进行一次Full GC;

    (2)而(1)中所说的有风险的Minor GC就是空间分配担保,当Survivor空间不足以容纳Minor GC之后的新生代时,则向老年代晋升(空间分配担保);