深入理解Java虚拟机系列(二)--垃圾收集器与内存分配策略

系列文章目录

深入理解Java虚拟机系列文章

一.判断对象是否死亡

堆里面存放着Java世界当中几乎所有的对象实例,而堆也是GC高频的地方,也叫GC堆,有这么一群垃圾回收器会把一些没有用的对象进行回收。那么GC在对堆进行回收前,第一件事情是要确定这些对象当中,哪些是还活着的,哪些是已经死去的。

1.1 引用计数法

给一个教科书式的介绍:给对象中添加一个引用计数器,每当有一个地方引用它适,计数器值+1。当引用失效时,计数器就-1。任何时刻计数器为0的对象就是不可能在被使用的。

  • 优点:实现简单,判定效率也很高。
  • 缺点:很难解决对象之间相互循环引用的问题。

例子:

public class Test {
    
    
    public Object instance = null;

    public static void main(String[] args) {
    
    
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;

        a = null;
        b = null;

        System.gc();
    }
}

执行后的结果:这里可以看到虚拟机并没有因为两个对象相互引用就不回收他们,这里可以侧面的说明虚拟机并不是通过引用计时法来判断对象是否存活的。
在这里插入图片描述

1.2 可达性分析法

Java主要是通过可达性分析法来判断对象是否已死。

该算法的主要思路:

  1. 通过一系列的称为“GC Root”的对象作为起始点。
  2. 从这些节点开始向下搜索,搜索走过的路径称为引用链。
  3. 当一个对象到GC roots没有任何引用链相连(即从GC root到这个对象不可达),则说明这个对象是不可用的。

如图:对象Object5、Object6、Object7虽然互相有关联,但是他们到GC Root是不可达的,所以这3个对象被判定为可回收的对象。
在这里插入图片描述

可以作为GC Root的对象包括:
1.虚拟机栈中引用的对象。(本地变量表)
2.方法区中类静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法栈中(native方法)引用的变量。

1.3 再谈引用

无论是通过引用计数法还是可达性分析法来判断对象的引用是否可达,判断对象是否存活都与“引用”有关。而引用包括很多种:(强度从大到小排序)

  • 强引用(Strong Reference)

强引用是代码中普遍存在的,如Object o = new Object()这一类通过new出来的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

  • 软引用(Soft Reference)

软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围内进行第二次回收。

  • 弱引用(Weak Reference)

弱引用也是用来描述非必须对象的,但是它的强度比软引用要弱一点,被弱引用关联的对象只能生存到下一次GC发生之前,GC工作的时候,无论当前内存是否足够,都会回收到只被弱引用关联的对象。
举个例子:
ThreadLocal类,他的底层可以说是ThreadLocalMap,相当于一个HashMap,那么他的key其实就是个弱引用,所以经历过一次GC后,容易出现key=null的情况,导致这个键值对无法删除,造成内存泄漏,但是ThreadLocal本身已经考虑过这个情况,会有一个对应的处理来解决(具体情况就不说了)

  • 虚引用(Phantom Reference)

虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就就是能在这个对象被GC回收的时候得到一个系统通知。

1.4 finalize()方法

其实,即使在可达性分析法中被认定为不可达的对象,也并非是“非死不可”的,他们这个时候暂时处于一个“缓刑”阶段。因为要真正宣告一个对象死亡的话,至少要经历两次标记过程。(GC回收器的算法都会经历2次标记)

那么怎样摆脱死亡的命运呢?大致流程如下:

  1. 对象在进行可达性分析法后发现没有与GC Root相连的引用链,那么他会被第一次标记并且进行筛选.
  2. 筛选的条件是这个对象是否有必要执行finalize()方法。
  3. 如果这个finalize()方法被覆盖过(重写)且没被执行过,那么这个对象会被判为有必要执行finalize方法的。
  4. 若有必要执行,则这个对象会放到一个叫F-Queue的队列之中,并在稍后由一个低优先级的Finalizer线程去触发这个finalize()方法。
  5. 如果这个finalize()方法中成功的让此对象重新与引用链上的任何一个对象关联(即可达),那么在二次标记的时候,就会把这个对象移出“即将回收”的集合。相反,如果执行后,这个对象还是不可达的,那么他就会被回收。

注:

  1. 低优先级的Finalizer线程会触发对象的finalize()方法,但是并不承诺会等待他运行结束。

原因:
如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,可能造成F-Queue队列中的其他对象永久的处于等待状况,甚至整个内存回收系统发生崩溃。

  1. 如何让对象在finalize()方法中与引用链重新关联?

举个例子:
把自己(this关键字)赋值给某个类变量或者对象的成员变量。

1.4.1 Demo

public class FinalizeEscapeGC {
    
    
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
    
    
        System.out.println("--------我存活了---------");
    }

    @Override
    protected void finalize() throws Throwable {
    
    
        super.finalize();
        System.out.println("----------finalize方法执行成功!-------");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        SAVE_HOOK = new FinalizeEscapeGC();
        // 对象第一次去尝试拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为finalize方法的优先级很低,所以暂停0.5s等待他。
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
    
    
            SAVE_HOOK.isAlive();
        } else {
    
    
            System.out.println("----------我已经死亡(第一次)----------");
        }
        // 对象第二次去尝试拯救自己,这里代码与上面一样。
        SAVE_HOOK = null;
        System.gc();

        Thread.sleep(500);
        if (SAVE_HOOK != null) {
    
    
            SAVE_HOOK.isAlive();
        } else {
    
    
            System.out.println("----------我已经死亡(第二次)----------");
        }
    }
}

运行后的结果:
在这里插入图片描述
可以看出:SAVE_HOOK对象的finalize()确实被GC收集器触发过,并且在回收前成功的逃脱了。 还需要值得注意的地方时,结果中出现“我已死亡(第二次)”,可以明显的看出,这里第二次尝试逃脱被GC失败了,因为,finalize()方法只能够执行一次!所以第二次自救失败了。

二.垃圾收集算法

2.1 标记-清除算法

标记清除法是最基础的算法(其余的算法可以说是对标记清除算法不足之处的一个改进),他的算法划分为两个阶段。

  1. 标记阶段:标记出所有需要回收的对象。
  2. 清除阶段:统一回收所有被标记的对象。

大致流程如下:
在这里插入图片描述
该算法的不足:

  1. 效率问题,标记和清除两个阶段的效率都不高。
  2. 空间问题,标记清除之后会产生大量不连续的内存碎片。空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发一次GC。

2.2 复制算法

为了解决效率问题,复制算法出现了。
大致流程如下:

  1. 将可用内存容量划分为大小相等的两块,每次只使用其中的一块。
  2. 当这一块的内存用完了,就将还存活着的对象复制到另一块上,然后再把自己曾经使用过的那一块内存空间清理掉。

流程图:
在这里插入图片描述
优势:

  1. 每次都对半个内存区域进行回收,内存分配的时候也不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可。
  2. 实现简单,运行高效。

劣势:

  1. 代价是把内存缩小为原来的一半,内存开销大。

2.3 标记-整理算法

赋值算法在对象存活率较高的时候就要进行较多的赋值操作,效率会变低。 而老年代中的对象一般来说是存活率较高的,所以还需要别的算法来处理。因此产生了标记整理算法。

标记整理算法与标记清除算法大致一样,只是在原来基础上解决了空间碎片化的问题。

  1. 标记:对要存活的的对象进行标记
  2. 整理:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

在这里插入图片描述
最后讲一讲什么是分代收集算法:其实就是根据对象存活周期的不同将内存划分为几块。一般把Java堆分为新生代和老年代。

  • 新生代:每次GC都会有大批对象死去,只有少量存活。采用复制算法。
  • 老年代:对象存活率高、没有额外空间对他进行分配担保。采用标记清除or标记整理算法进行回收。

三.HotSpot的算法实现

上面从理论上介绍了对象存活的判定方法和垃圾收集算法,在HotSpot虚拟机实现上面算法的时候,必须对算法的执行效率做一个严格的考量。接下来就对一些问题以及其解决方案做一个介绍。

3.1 枚举根节点

我们知道,Java主要用的是可达性分析法来判别对象是否已死。首先肯定使要去寻找GC Roots,而可以作为GC Roots的节点主要在全局性的引用(常量or静态变量)与执行上下文(栈桢中的本地变量表)中。 可想而知,如果你的Java程序非常大,那么方法区是非常大的。并且我们的Java程序是实时在运行的,对象也是在不断地创建,虚拟机不断的在运作当中,所以我们对可达性进行分析的时候,这项工作肯定是在一个能确保一致性的快照(状态)中进行的。:

问题1:
如果要逐个检查来寻找可用的GC Roots,那么会消耗很多时间,怎么办?
问题2:
如果出现GC在分析过程中,对象引用关系在不断变化的情况,那么分析结果的准确性和一致性无法得到保证怎么办?

解决:

  1. GC进行时,必须停顿所有Java执行进程(Stop The World),也可以说枚举根节点时必须停顿。
  2. 使用OopMap来实现快速定位GC Roots的枚举。

当执行系统停顿下来后,并不需要一个不漏的去检查完所有上下文和全局的引用位置,因为虚拟机是有办法直接得知那些地方存放着对象引用
在HotSpot的实现中,通过一组成为OopMap的数据结构来达到这个目的。
原理:
1.在类加载完成的时候,HotSpot就把对象中对应偏移量的对应类型数据计算出来。
2.在JIT编译的时候,就会在特定的位置记录下栈和寄存器中哪些位置是引用的。
3.这样在GC扫描的时候就可以直接得知这些信息。

3.2 安全点

虽然使用OopMap可以协助HotSpot快速准确的完成根节点的枚举,但是还有一个问题随之而来。

问题1:
如果为每一条指令都生成对应的OopMap,那么会需要大量的额外空间,这样GC的空间成本会非常高。
解决:
HotSpot利用安全点(Safepoint)来解决,即程序执行的时候,并非所有地方都能停顿下来进行GC,只有在到达安全点的时候才能够暂停。

安全点的选定不能太少——>导致GC等待时间过长。
安全点的选定不能太多——>导致过于频繁的GC以增加运行时的负荷。
因此,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定。
例如:方法调用、循环跳转、指令序列复用等代码,具有这些功能的指令才会产生Safepoint。

问题2:
如何在GC发生的时候,让所有线程都跑到最近的安全点上再停顿下来。
解决:

  • 方案一:抢先式中断(几乎没有虚拟机采用该方案):GC发生的时候,首先把所有线程全部中断,如果发现有线程中断的地方不再安全点上,就恢复该线程,让他跑到安全点上。
  • 方案二:主动式中断:GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行的时候主动去轮询这个标志,发现中断标志为true的时候就自己中断挂起。(轮询标志的地方和安全点的地方是重合的)

3.3 安全区域

使用安全点Safepoint虽然保证了程序执行时,在不太长的时间内就会遇到可进入的GC的Safepoint,但是如果程序不执行的时候,如线程处于sleep或者Blocked状态,那这个时候线程无法响应JVM的中断请求,怎么办?

问题:
如果线程处于Sleep等状态,无法响应JVM的中断请求去走到最近的安全点,JVM显然不太可能等待线程重新分配CPU时间的情况下怎么办?

解决:
使用安全区域(Safe Region)来解决。

安全区域指在一段代码片段中,引用关系不会发生变化,在这个区域中的任何一个地方开始GC都是安全的,我们也可以把Safe Region看作是被扩展了的Safepoint。

原理:

  1. 当线程执行到安全区域中的代码时,首先标识自己已经进入了Safe Region, 这样,在这段时间里JVM发起GC时,就不用管标识了自己为Safe Region状态的线程了。
  2. 线程离开Safe Region时,他要检查系统是否已经完成了根节点枚举,如果完成了,线程继续执行,否则他就必须等待直到收到可以安全离开Safe Region的信号为止。

四.垃圾收集器

上面介绍了什么是安全点、以及一些垃圾收集的算法,那么接下来肯定要开始介绍垃圾收集器了,他是垃圾收集算法的一个具体实现。先放一张虚拟机包含的所有垃圾收集器图:
在这里插入图片描述

4.1 新生代收集器

4.1.1 Serial收集器

Serial收集器是一个单线程的收集器,即只使用一个CPU或者一条收集线程去完成GC工作。
优点:

  1. 简单高效。
  2. 对于单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集,可以获得最高的单线程收集效率。

工作流程图:
在这里插入图片描述

4.1.2 ParNew收集器(新生代)

ParNew收集器作为Serial收集器的多线程版本。(其余行为和Serial收集器完全一样)
工作流程图:
在这里插入图片描述
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,并且除了Serial收集器外,只有他能与CMS收集器配合工作。

4.1.3 Parallel Scavenge收集器

Parallel Scavenge收集器为一个新生代收集器,使用复制算法并且是并行的多线程收集器。
工作流程图:
在这里插入图片描述

特点:
Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集事件)
如虚拟机总共运行100分钟,垃圾收集花费1分钟,那么吞吐量为99%

停顿时间越短就越适合需要与用户交互的程序,因为良好的响应速度能提升用户体验,提高吞吐量可以高效率的利用CPU事件,尽快的完成运算任务。其中,Parallel Scavenge收集器提供了俩参数用于精确控制吞吐量:

  1. -XX:MaxGCPauseMillis——>控制最大垃圾收集停顿时间
  2. -XX:GCTimeRatio——>设置吞吐量大小

1.-XX:MaxGCPauseMillis:值为大于0的毫秒数。它并不是越小越好,因为GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。
2.-XX:GCTimeRatio:值的范围是0<x<100的整数。也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。
例:如果此参数为19,那么允许的最大GC时间就占总时间的5%(1/(1+19)=5%)。默认值为99,即 允许最大1%(1/(1+99))的垃圾收集时间。

Parallel Scavenge收集器的另外一个特性:自适应调节策略
涉及到的重要参数:-XX:+UseAdaptiveSizePolicy,这是一个开关参数,如果打开后,不需要手动指定新生代的大小、比例等细节参数,虚拟机会根据当前系统的运行情况监控信息,动态调整这些参数来提供最合适的停顿时间或者最大的吞吐量。(这是它与ParNew收集器的一个重要区别)

4.2 老年代收集器

4.2.1 Serial Old收集器

Serial Old收集器为Serial的老年代版本,同样是一个单线程收集器,使用标记整理算法。作为CMS收集器的后备选择。(流程和Serial一样)

4.2.2 Parallel Old收集器

Parallel Old收集器作为Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。

4.2.3 CMS收集器(重点)

CMS收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,他第一次实现了让垃圾收集器线程和用户线程(基本上)同时工作。

这里对并行和并发做一个解释:

对于垃圾收集器的语境来说:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程处于等待状态。
并发(Concurrent):用户线程和GC收集线程同时执行,用户程序继续运行,而GC程序运行于另外一个CPU上。

回到CMS收集器,它以获取最短回收停顿时间为目标,基于标记清除算法。
工作流程图:
在这里插入图片描述

  1. 初始标记:暂停所有的其他线程,并记录下直接与Root相连的对象,速度很快。
  2. 并发标记:同时开启GC和用户线程,用一个闭包结果去记录可达对象。但在这个阶段结束的时候,这个闭包结构不能保证包含当前所有的可达对象。(因为用户线程可能会不断的更新引用,所以GC线程无法保证可达性分析的实时性和准确性)
  3. 重新标记:重新标记阶段是为了修正并发标记(第二步)期间由于用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段要稍长,远远比并发标记阶段时间短。
  4. 并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫。(注意不是对标记的区域做清扫)

优点:

  1. 并发收集、停顿低。

缺点:

  1. CMS对CPU资源非常敏感。
  2. 无法收集浮动垃圾。

解释下什么是浮动垃圾:
CMS并发清理阶段用户线程还在运行着,伴随程序的运行自然而然就会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中去处理掉他们,只能等待下一次GC的时候在清理。这一部分就叫浮动垃圾。
那么CMS无法处理浮动垃圾,可能会出现Concurrent Mode Failure失败而导致另外一次Full GC的产生。

  1. 容易产生内存碎片。(原因在于算法使用了标记清除算法)

4.3 G1收集器(重点)

G1是一款面向服务器的垃圾收集器,主要针对配备多颗处理器以及大容量内存的机器,在满足GC停顿时间要求的同时,还具备高吞吐量性能的特征。
工作流程图:
在这里插入图片描述
G1收集器的特点:

  1. 并行与并发:G1利用CPU、多核的优势来缩短Stop The World的停顿时间。
  2. 分代收集:可以管理整个GC堆。
  3. 空间整合:G1从整体来看基于标记整理算法。从局部来看基于复制算法。
  4. 可预测的停顿:G1和CMS收集器一样,都有降低停顿时间的共同目标,但是G1还可能建立起可以预测的停顿时间模型让使用者明确指定一个时间片段来GC。(因为他可以有计划的避免在整个Java堆上进行全区域的垃圾收集

补充:
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式,保证G1在有限时间内可以尽可能的提高收集效率。

使用G1收集器的时候,Java堆的内存布局和其他收集器就有很大区别(因为G1可以负责整个堆,其他的收集器要么就是单个新生代,要么单个老年代),所以它将Java堆划分为多个大小相等的独立区域——>Region,这样新生代和老年代就不是物理隔离的了,他们都是Region的集合。

4.4 小总结

这一章节讲的有点多了,做一个小总结,让上述的知识点有一个大体的框架:
首先再次把一个重点拿出来:
为什么要有个停顿时间,为什么谈到GC收集器总会扯上回收停顿时间?

答:
1.因为GC收集器在工作的时候,需要判断对象是否已死,那么判断的方法使用的是可达性分析法。
2.可达性分析法要求我们去寻找哪些对象属于GC Roots。
3.因为程序允许过程中,引用域是随时改变的,影响GC Roots引用链,影响结果。
4.所以GC工作的时候,会停止用户线程,那么在这个期间,就是所谓的回收停顿时间。

我觉得理解了上面的停顿时间后,再去反过头看看CMS和G1收集器,可能效果会更好点,并且把CMS的GC流程好好记一下。

新生代GC:

  • Serial(单线程)
  • ParNew(多线程)
  • Parallel Scavenge(多线程,目标:达到一个可控制的吞吐量支持自适应调节策略

老年代GC:

  • Serial Old(单线程、标记整理
  • Parallel Old(参考Parallel Scavenge,只是服务的范围不一样、标记整理
  • CMS(目标:获取最短回收停顿时间并发收集标记清除

独立的GC:

  • G1(标记整理、复制算法、并行与并发、分代、可预测停顿)

五.理解GC日志和垃圾收集器参数总结

5.1 GC日志解析

第一步:添加JVM参数

-XX:+PrintGCDetails

如图:
在这里插入图片描述
我拿个简单的例子:

[GC (Allocation Failure) [PSYoungGen: 2166K->624K(9216K)] 2166K->632K(19456K), 0.0015824 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 624K->560K(9216K)] 632K->568K(19456K), 0.0018029 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 560K->0K(9216K)] [ParOldGen: 8K->475K(10240K)] 568K->475K(19456K), [Metaspace: 2936K->2936K(1056768K)], 0.0043543 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(9216K)] 475K->475K(19456K), 0.0009224 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(9216K)] [ParOldGen: 475K->458K(10240K)] 475K->458K(19456K), [Metaspace: 2936K->2936K(1056768K)], 0.0043196 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 410K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 5% used [0x00000007bf600000,0x00000007bf666800,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 458K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 4% used [0x00000007bec00000,0x00000007bec72928,0x00000007bf600000)
 Metaspace       used 2975K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 323K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at leetcode.Test.<init>(Test.java:9)
	at leetcode.Test.main(Test.java:12)

Process finished with exit code 1

对其中的一条信息进行拆分:

[GC (Allocation Failure) [PSYoungGen: 2166K->624K(9216K)] 2166K->632K(19456K), 0.0015824 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 

参数含义:

  • GC:年轻代的垃圾回收日志。
  • Allocation Failure:表示垃圾回收的原因是因为对象分配内存的时候空间不够了。
  • [PSYoungGen: 2166K->624K(9216K)] :代表新生代回收前占用的内存大小为2166K,回收后占用的内存大小为624K,新生代占用的总内存大小为9216K。(PS代表新生代使用的是Parallel Scavenge收集器)
  • 0.0015824 secs:代表回收的时间。
  • [Times: user=0.01 sys=0.00, real=0.00 secs] :代表用户状态耗时0101,系统耗时0.00,实际的执行时间为0.00s。

其他:

  • Full GC:整个堆的垃圾回收(包括老年代和新生代)(带有Full的则说明这次发生了Stop The World,即STW
  • ParOldGen:代表老年代的回收,使用的收集器为ParNew收集器。其他的则同理。

5.2 GC收集器参数

参数 描述
UseSerialGC 虚拟机运行在Client模式下的默认值,打开该开关后,则使用Serial+Serial Old的收集器组合进行内存回收。
UseParNewGC 打开后,使用ParNew+Serial Old的收集器组合。
UseConcMarkSweepGC 打开后使用ParNew+CMS+Serial Old的组合进行回收。(Serial Old作为CMS出现Concurrent Mode Failure失败后的备选收集器)
UseParallelGC 虚拟机在Server模式下的默认值,打开后,使用Parallel Scavenge+Serial Old的组合。
UseParallelOldGC 打开后,使用Parallel Scavenge+Parallel Old
SurvivorRatio 新生代中Eden区域和Survivor区域的容量比,默认为8。
PretenureSizeThreshold 直接晋升到老年代的对象大小,大于这个参数的对象直接在老年代分配。
MaxTenuringThreshold 晋升到老年代的对象年龄,每个对象在经历过一次MinorGC后,若存活下来则年龄就增加1,当超过这个值就进入老年代。
UseAdaptiveSizePolicy 动态调整Java堆中各个区域的大小和进入老年代的年龄。
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足的时候以应对新生代的整个Eden和Survivor区域所有对象都存活的情况。
ParallelGCThreads 设置并行GC时进行内存回收的线程数。
GCTimeRatio GC时间占总时间的比率,默认值为99.
MaxGCPauseMillis GC的最大停顿时间,仅在使用Parallel Scavenge的时候有效。
CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间被使用多少后触发垃圾收集,默认值为68%,仅适用CMS收集器。
UseCMSCompactAtFullCollection 设置CMS收集器在完成垃圾收集后是否需要进行一次内存碎片整理。
CMSFullGCsBeforeCompaction 设置CMS收集器在进行若干次垃圾收集后在再动一次内存碎片整理。

(敲到这里手都敲累了,书上一字不落抄下来的。)

六.内存分配与回收策略

  1. 对象优先在Eden区分配

Java在大多数情况下,对象在新生代Eden区中分配。(如果启动了本地线程分配缓冲区,则按照线程优先在TLAB上分配)。当Eden区中没有足够空间进行分配的时候,虚拟机将发起一次Minor GC。

  1. 大对象直接进入老年代

所谓的大对象即需要大量连续内存空间的Java对象。例如:很长的字符串、大数组。

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

JVM给每个对象都定义了一个年龄计数器
流程:
1.对象在Eden区出生后,经历一次Minor GC后存活,则进入Survivor区,此时年龄为1.
2.对象在Survivor区中,每经历一次Minor GC并存活,年龄继续+1,直到加入15岁(默认),对象会被晋升到老年代。

  1. 动态对象年龄判定

如果Survivor空间中相同年龄所有对象大小的总和>Survivor空间的一半,则年龄>=该年龄的对象可以直接进入到老年代。

  1. 空间分配担保

第一步.检查老年代最大连续可用空间(max(old))是否>新生代中所有对象所占空间总和。
若为True——>此时Minor GC是安全的。
若为False——>第二步↓
第二步.检查HandlePromotionFailure是否设置为允许担保失败。
若为True——>第三步↓
若为False——>第四步↓↓
第三步.检查max(old)是否>历来晋升到老年代中的所有对所占空间的平均大小。
若为True——>进行一次Minor GC
若为False——>第四步↓
第四步.进行一次Full GC

猜你喜欢

转载自blog.csdn.net/Zong_0915/article/details/110289871