JVM:自动内存管理-垃圾收集器与内存分配策略

Java与C++之间有一堵由内存分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

垃圾搜集器与内存分配策略.png

一、概述

Java堆和方法区这两个区域有着很显著的不确定性:

1、一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样
2、只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的

垃圾收集器所关注的正是这部分的内存该如何管理
2020072320140535.png

二、对象已死?

1、引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器就减一;任何时刻计数器为零的对象是不可能再被使用的。

引用计数器虽然占用了一些额外的内存空间来进行计数,原理简单,判定效率很高;

为什么主流Java虚拟机没有使用引用计数器来管理内存呢

引用计数法看似简单的算法有很多例外情况要考虑,必须配合大量额外处理才能保证正确的工作,比如单纯的引用计数很难解决对象之间互相循环引用的问题

引用计数器的缺陷

/**
 * @Author: yky
 * @CreateTime: 2020-12-13
 * @Description: 引用计数器的缺陷
 */
public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
     * 这个成员变量唯一作用是占内存
     */
    private byte[] bigSize = new byte[2 * _1MB];
    public static void testGC(){
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        //发生GC,objA、objB能否被回收
        System.gc();
    }
}

运行代码收查看日志信息发现,这两个对象均被回收虚拟机并没有因为这两个相互引用就放弃回收他们---->Java虚拟机并不是通过计数算法来判断对象是否存活的;

2、可达性分析算法

该算法的核心思想:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些结点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连(图论话来说从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的)

image

对象obj5obj6obj7虽然有关联,但是他们到GC roots不可达因此他们会被判定为可回收对象

在 Java 语言中,可作为 GC Roots 的对象包括以下几种

  • 虚拟机栈(栈中的本地变量表)中的引用对象,如各线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等;
  • 方法区中的类静态属性引用的对象,如Java类的引用类型静态变量;
  • 方法区中的常量引用的对象,如字符串常量里的引用;
  • 本地方法栈总JNI(Navicat方法)引用的对象;
  • Java虚拟机内部的引用
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;
  • 根据用户所选的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入;

3、再谈引用

无论通过哪种算法判断对象是否存活都和“引用”离不开关系。

1)强引用

是指在程序代码之间普遍存在的引用赋值,Object obj = new Object();这种引用关系。
无论什么情况下,只要强引用关系还在,垃圾收集器就不会回收掉被引用的对象;

2)软引用

用来描述一些还有用,但非必须的对象。只要软引用关联着的对象,在系统将要发生内存溢出前,会把这些对象列进回收范围之中进行第二次回收;如果这次的回收还没有足够的空间,才会抛出内存溢出的异常;

JDK1.2后提供SoftReference类实现软引用:

Soft reference objects, which are cleared at the discretion of the garbage
collector in response to memory demand. Soft references are most often used
to implement memory-sensitive caches.

3)弱引用

弱引用也被用来描那些非必须对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作,无论当前内存是足够,都会回收掉只被弱引用关联的对象;

JDK1.2后WeakReference类用来实现弱引用:

Weak reference objects, which do not prevent their referents from being

made finalizable, finalized, and then reclaimed. Weak references are most

often used to implement canonicalizing mappings.

4)虚引用

也叫“幽灵引用”、“幻影引用”,最弱的一种引用关系

  • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用获得一个对象的实例;

  • 为一个对象设置虚引用的唯一目的是为了能在这个对象被收集器回收时收到一个系统通知;

PhantomReference类来实现虚引用:

Phantom reference objects, which are enqueued after the collector determines that their referents may otherwise be reclaimed. Phantom references are most often used to schedule post-mortem cleanup actions.

应用需要读取大量本地图片

如果每次读取图片都从硬盘读取,则会严重影响性能;解决方案:【软引用或者弱引用】

Map<String,SoftReference<BitMap>> imp = new HashMap<String,SoftReference<BitMap>>

4、生存还是死亡?

在进行过可达性分析后的对象也不一定是非死不可的,该对象进行可达性分析后,发现没有与GC Roots相连接的引用链

  • 这个对象就会第一次被标记起来;对对象是否必要执行finalize()方法进行判断(已经被虚拟机调用过finalize()方法或者没有覆盖finalize()方法都认为是没有必要执行该finalize()方法)
  • F-Queue队列中存放该对象,优先级较低的Finalizer线程会去执行它;Gc 会对这个队列里面的对象再进行一次标记,如果在finalize方法中,对象没有自己自救的话,它就会被标记回收
  • finalize方法自救自己的办法是:重新与引用链上面的任何一个对象建立连接;如把自己this赋值给某个类或对象的成员变量
/**
1.对象可以在GC时自救
2.自救的办法只有一次,因为一个finalize方法最多只能被调用一次
**/
public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive(){
        System.out.println("yes,I am still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed !");
        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("no, i am dead :(");
        }

        //下面这段代码再执行一遍,验证对象是不是可以成功
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no, i am dead :(");
        }
    }

}

结果如下:

finalize method executed !
yes,I am still alive :)
no, i am dead :(
  • 并不鼓励使用这种办法来拯救对象,它的运行代价高昂,不确定性大,无法保证顺序;
  • finalize方法能做的所有工作,try-finally也可以做的更好,更及时,所以希望忘记这个方法的存在;

5、回收方法区

很多人认为方法区(或者HotSpot虚拟机中的元空间或永久代)是没有垃圾收集行为的,《Java虚拟机规范》中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。

方法区的垃圾收集主要回收两部分:废弃的常量和不再使用的类型;

  • 回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例:

    假如一个字符串“Java”已经进入了常量池中,但是当前系统没有任何一个字符串对象的值是“Java”,换句话说是没有任何String对象引用常量池中的“Java”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“Java”常量就会被系统清理出常量池。

  • 常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判定一个常量是否是“废弃常量”比较简单。而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“被允许”,而不是和对象一样,不使用了就必然会回收。在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证不会被方法区造成过大的内存压力。

三、垃圾收集算法

1、分代收集理论

分代收集理论.png

分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分 代假说之上:

  • 1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  • 2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  • 3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

  1. 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
  1. 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

2、标记-清除算法

最基础的收集算法

算法分为“标记”和“清除”两个阶段:

  • 首先标记出所有需要回收的对象
  • 在标记完成后,统一回收掉所有被标记的对象
  • 也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
  • 标记过程就是对象是否属于垃圾的判定过程

缺点

  • 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
  • 第二个是内存空间的碎片化问题,标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

image.png

3、标记-复制算法

标记-复制算法常被简称为**复制算法,**为了解决标记-清除算法面对大量可回收对象时执行效率低的问题

  • 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
  • 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,
  • 然后再把已使用过的内存空间一次清理掉。

缺点

  • 这种复制回收算法的代价是将可用内存缩小为了原来的一半;
  • 在对象存活率较高时就要进行较多的复制操作,效率将会降低;
  • 如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存

活的极端情况,所以在老年代一般不能直接选用这种算法

image.png

现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代

Appel式回收

  • 把新生代分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。
  • HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。
  • Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保。

内存的分配担保也一样,如果另外一块 Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的。

4、标记-整理算法

  • 针对老年代对象的存亡特征 ----->“标记-整 理”算法,
  • 其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理
  • 而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

image.png

是否移动回收后的存活对象是一项优缺点并存的风险决策:

  • 如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新 所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行
  • 全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的 空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。----->直接影响应用程序的吞吐量。

HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的

四、HotSpot的算法细节实现

1、根节点枚举

  • 所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的

即使是号称停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、 ZGC等收集器,枚举根节点时也是必须要停顿的。

  • 根节点枚举始终还 是必须在一个能保障一致性的快照中才得以进行

“一致性”的意思是整个枚举期间执行子系统 看起来就像被冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化的情况

目前主流Java虚拟机使用的都是准确式垃圾收集,所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有 执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。

在HotSpot 的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候, HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用;

HotSpot虚拟机客户端模式下生成的一段String::hashCode()方法的本地代码:

String::hashCode()方法.png

2、安全点

实际上HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录 了这些信息,这些位置被称为安全点(Safepoint)。

  • 有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停
  • 安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准 进行选定的

“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转 等都属于指令序列复用

如何在垃圾收集发生时让所有线程(这里其实不包括 执行JNI调用的线程)都跑到最近的安全点,然后停顿下来。

这里有两种方案可供选择:抢先式中断主动式中断

  • 抢先式中断不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地 方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚 拟机实现采用抢先式中断来暂停线程响应GC事件。
  • 主动式中断的思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他 需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。

3、安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任 意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

4、记忆集与卡表

为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建 立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。

记忆集.png

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。

卡表最简单的形式可以只是一个字节数组,而HotSpot虚拟机确实也是这样做的。以下这行代码是HotSpot默认的卡表标记逻辑:
image.png

字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作“卡页”

卡页.png

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代 指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃 圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针;

5、写屏障

在HotSpot虚拟机里是通过写屏障技术维护卡表状态的
写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面
在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的 前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障,在赋值后的则叫作写后屏障

卡表状态.png
卡表在高并发场景下还面临着“伪共享”问题。

伪共享是处 理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行 为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。

在JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启 卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。

6、并发的可达性分析

可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析, 这意味着必须全程冻结用户线程的运行。

为什么必须在一个能保障一致性的快照上才能进 行对象图的遍历

为了能解释清楚这个问题,我们引入三色标记(Tri-color Marking)作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

两种解决方案:

  • 增量更新(Incremental Update)

    • 增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新 插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
  • 原始快照(Snapshot At The Beginning,SATB)。

    • 原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描 一次。

五、经典垃圾收集器

垃圾收集器.png

1、Serial收集器

  • 单线程工作的收集器、简单而高效
  • 它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束
  • 使用算法:复制算法
  • 适用范围:新生代
  • 应用:客户端模式下的默认新生代收集器

2、ParNew收集器

  • ParNew收集器实质上是Serial收集器的多线程并行版本
  • 能与CMS 收集器配合工作。

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

  • 使用算法:复制算法
  • 适用范围:新生代
  • 应用:运行在Server模式下的虚拟机中首选的新生代收集器

3、Parallel Scavenge收集器

  • Parallel Scavenge收集器也是一款新生代收集器,
  • 它同样是基于标记-复制算法实现的收集器,
  • 也是能够并行收集的多线程收集器
  • Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量

高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算 任务,主要适合在后台运算而不需要太多交互的分析任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量

  • 控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis参数

    • 允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的 时间不超过用户设定值。
    • 垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的
  • 直接设置吞吐量大小的-XX:GCTimeRatio参数。

    • 参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的 比率,相当于吞吐量的倒数。

4、Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。

5、Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实 现。

6、CMS收集器

  • 以获取最短回收停顿时间为目标的收集器(并发收集、低停顿)

  • 基于标记-清除算法实现

  • 整个过程分为四个步骤,

    • 1)初始标记(CMS initial mark)
    • 2)并发标记(CMS concurrent mark)
    • 3)重新标记(CMS remark)
    • 4)并发清除(CMS concurrent sweep)

缺点:

  • CMS收集器无法处理“浮动垃圾”
  • 产生大量空间碎片产生

7、Carbage First收集器

开创了收集 器面向局部收集的设计思路和基于Region的内存布局形式。

G1的特性

  • 面向服务端应用的垃圾收集器
  • 并行与并发:G1能充分利用多CPU、多核环境使用多个CPU或CPU核心来缩短STW(Stop-The-World)停顿时间。
  • 分代收集:G1物理上不分代,但逻辑上仍然有分代的概念。
  • 空间整合:不会产生内存空间碎片,收集后可提供规整的可用内存,整理空闲空间更快。
  • 可预测的停顿(它可以有计划的避免在整个JAVA堆中进行全区域的垃圾收集)
  • 适用于不需要实现很高吞吐量的场景
  • JAVA堆内存布局与其它收集器存在很大差别,它将整个JAVA堆划分为多个大小相等的独立区域或分区(Region)。
  • G1收集器中,虚拟机使用Remembered Set来避免全堆扫描。

停顿时间模型收集器

“停顿时间模型”(Pause Prediction Model)的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段 内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时Java(RTSJ)的中软实时垃圾收集器特征了。

Mixed GC模式

在G1收集器出现之前的所有 其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老 年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

G1设计

G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。

Region中还有一类特殊的Humongous区域,专门用来存储大对象

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免 在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。 这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

image

将Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决?

使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应用其实要复杂很多,它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一 种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这 种“双向”的卡表结构,比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担

在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?

G1 收集器则是通过原始快照(SATB)算法来实现。此外,垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设 计了两个名为TAMS的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。

怎样建立起可靠的停顿预测模型?

G1收集器的停顿 预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记 录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。

Garbage First收集器的运作过程

  1. 初始标记

    • 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值
    • 需要暂停线程(耗时短)
  2. 并发标记

    • 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图
    • 与用户线程并发执行,耗时长
  3. 最终标记

    • 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  4. 筛选标记

    • 负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划
    • 暂停用户线程,由多条收集器线程并行完成的。

G1收集器与与CMS收集器比较

G1CMS
垃圾收集算法G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现“标记-清除”算法
内存占用高G1的记忆集(和 其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间较低CMS的卡表就相当简单, 只有唯一一份
执行负载写前屏障和写后屏障用写后屏障来更新维护卡表

目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其 优势

六、低延迟垃圾收集器

衡量垃圾收集器的三项最重要的指标是:内存占用、吞吐量和延迟

低延迟垃圾收集器.png

Shenandoah和ZGC,几乎整个工作过程全部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定 的,与堆的容量、堆中对象的数量没有正比例关系。这两款目前仍处于实验状态的收集器,被官方命名为“低延迟垃圾收集器”

1、Shenandoah收集器

  • Shenandoah(目前)是默认不使用分代收集的
  • Shenandoah使用连接矩阵的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。
  • 支持并发的整理算法,Shenandoah的回收阶段可以和用户线程并发执行;

Shenandoah收集器的工作过程大致可以划分为以下九个阶段:

  1. 初始标记

    • 首先标记与GC Roots直接关联的对象,暂停用户线程,暂停的时间只与GC Roots的数量相关
  2. 并发标记

    • 遍历对象图,标记出全部可达的对象,这个阶段 是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
  3. 最终标记

    • 处理剩余的SATB扫描,并在这个阶段统计出回收价值 最高的Region,将这些Region构成一组回收集
  4. 并发清理

    • 清理那些整个区域内连一个存活对象都没有找到的Region
  5. 并发回收

    • 并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。
    • Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。
    • 并发回收阶段运行的时间长短取决于回收集的大小。
    • 与用户线程一起并发执行
    • Shenandoah将会通过读屏障和被称为Brooks Pointers的转发指针来解决并发回收阶段遇到的这些困难
  6. 初始引用更新

    • 把堆中所有指 向旧对象的引用修正到复制后的新地址
    • 建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务
    • 产生产生短暂的暂停
  7. 并发引用更新

    • 真正开始进行引用更新操作
    • 这个阶段是与用户 线程一起并发的
    • 时间长短取决于内存中涉及的引用数量的多少。
    • 按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为 新值即可。
  8. 最终引用更新

    • 修正存在于GC Roots 中的引用。
    • 这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。
  9. 并发清理

    • 最后再调用一次并发清理过程来回收Immediate Garbage Regions的内存空间,供以后新对象分配使用。

image.png
image.png

转发指针(Brooks Pointer)

Shenandoah收集器的并发回收的核心是,转发指针。

转发指针的核心内容就是,在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己。

  • 从结构上来看,Brooks提出的转发指针与某些早期Java虚拟机使用过的句柄定位有一些相似之处,两者都是一种间接性的对象访问方式,差别是句柄通常会统一存储在专门的句柄池中,而转发指针是分散存放在每一个对象头前面
  • 缺点:每次对象访问会带来一次额外的转向开销
  • 转发指针加入后带来的收益自然是当对象拥有了一份新的副本时,只需要修改一处指针的值,即旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转 发到新的副本上。这样只要旧对象的内存仍然存在,未被清理掉,虚拟机内存中所有通过旧引用地址访问的代码便仍然可用,都会被自动转发到新对象上继续工作,

Brooks Pointers 转发指针在设计上决定了它是必然会出现多线程竞争问题的。Shenandoah收集器是通过比较交换(Compare And Swap,CAS)操作来保证并发时堆中的访问正确性的。

转发指针另一点必须注意的是执行频率的问题

除写屏障以外,为了实现Brooks Pointer,Shenandoah在读、写屏障中都加入了额外的转发处理

Shenandoah在实际应用中的性能表现

image.png

2、ZGC收集器

以低延迟为首要目标的一款垃圾收集器。它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障染色指针内存多重映射等技术来实现可并发的标记-整理算法的收集器。

1)ZGC的内存布局

  • ZGC的Region具有动态性——动态创建和销毁,以及动态的区域容量大小。

  • 在x64硬件平台下,ZGC的 Region可以具有大、中、小三类容量

    • 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
    • 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
    • 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,大型Region在ZGC的实 现中是不会被重分配的,因为复制一个大对象的代价非常高昂。

2)并发整理算法的实现

使用了读屏障染色指针内存多重映射等技术来实现可并发的标记-整理算法
ZGC收集器有一个标志性的设计是它采用的染色指针技术

染色指针

染色指针是一种直接将少量额外的信息存储在指针上的技术,可是为什么指针本身也可以存储额外信息呢?

在64位系统中,理论可以访问的内存高达16EB(2的64次幂)字节。实际上,基于需求(用不到那么多内存)、性能(地址越宽在做地址转换时需要的页表级数越多)和成本(消耗更多晶 体管)的考虑,在AMD64架构中只支持到52位(4PB)的地址总线和48位(256TB)的虚拟地址空 间,所以目前64位的硬件实际能够支持的最大内存只有256TB。此外,操作系统一侧也还会施加自己的约束,64位的Linux则分别支持47位(128TB)的进程虚拟地址空间和46位(64TB)的物理地址空 间,64位的Windows系统甚至只支持44位(16TB)的物理地址空间。

尽管Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今天仍然能够充分满足大型服务器的需要。鉴于此,ZGC的染色指针技术继续盯上了这剩下的46位指针宽度,将其高4位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到。当然,由于这些标志位进一步压缩了原本就只有46位的地址空间,也直接导致 ZGC能够管理的内存不可以超过4TB(2的42次幂)

image

缺点

  • 染色指针有4TB的内存限制
  • 不能支持32位平台
  • 不能支持压缩指针(-XX: +UseCompressedOops)

优势:

  • 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。
  • 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。
  • 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以 便日后进一步提高性能。

3)ZGC收集器工作四个大的阶段

  1. 并发标记

    • 并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是,ZGC 的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。
  2. 并发预备重分配

    • 根据特定的查询条件统计得出 本次收集过程要清理哪些Region,将这些Region组成重分配集
  3. 并发重分配

    • 把重分 配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表记录从旧对象到新对象的转向关系
  4. 并发重映射

    • 修正整个堆中指向重分配集中旧对象的所 有引用

NUMA-Aware的内存分配

ZGC还有一个常在技术资料上被提及的优点是支持“NUMA-Aware”的内存分配。NUMA(非统一内存访问架构)是一种为多处理器或者多核处理器的计算机所设计的 内存架构。在NUMA架构下,ZGC收集器会优先尝 试在请求线程当前所处的处理器的本地内存上分配对象,以保证高效内存访问。在ZGC之前的收集器

就只有针对吞吐量设计的Parallel Scavenge支持NUMA内存分配,如今ZGC也成为另外一个选择。

性能测试

七、选择合适的垃圾收集器

1、Epsilon收集器

Epsilon(A No-Op Garbage Collector)垃圾回收器控制内存分配,但是不执行任何垃圾回收工作。一旦java的堆被耗尽,jvm就直接关闭。设计的目的是提供一个完全消极的GC实现,分配有限的内存分配,最大限度降低消费内存占用量和内存吞吐时的延迟时间。一个好的实现是隔离代码变化,不影响其他GC,最小限度的改变其他的JVM代码。

2、收集器的权衡

我们应该如何选择一款适合自己应用的收集器呢?

这个问题的答案主要受以下三个因素影响:

  1. 应用程序的主要关注点是什么?

    • 如果是数据分析、科学计算类的任务,目标是能尽快算出结果, 那吞吐量就是主要关注点;如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务 超时,这样延迟就是主要关注点;而如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的。
  2. 运行应用的基础设施如何?

    • 譬如硬件规格,要涉及的系统架构是x86-32/64、SPARC还是 ARM/Aarch64;处理器的数量多少,分配内存的大小;选择的操作系统是Linux、Solaris还是Windows等。
  3. 使用JDK的发行商、版本号

假设某个直接面向用户提供服 务的B/S系统准备选择垃圾收集器,一般来说延迟时间是这类应用的主要关注点,那么:

  • 如果你有充足的预算但没有太多调优经验,那么一套带商业技术支持的专有硬件或者软件解决方案是不错的选择,Azul公司以前主推的Vega系统和现在主推的Zing VM是这方面的代表,这样你就可以 使用传说中的C4收集器了。
  • 如果你虽然没有足够预算去使用商业解决方案,但能够掌控软硬件型号,使用较新的版本,同时又特别注重延迟,那ZGC很值得尝试。
  • 如果你对还处于实验状态的收集器的稳定性有所顾虑,或者应用必须运行在Win-dows操作系统下,那ZGC就无缘了,试试Shenandoah吧。
  • 如果你接手的是遗留系统,软硬件基础设施和JDK版本都比较落后,那就根据内存规模衡量一下,对于大概4GB到6GB以下的堆内存,CMS一般能处理得比较好,而对于更大的堆内存,可重点考察一下G1。

3、虚拟机及垃圾收集器日志

-Xlog[:[selector][:[output][:[decorators][:output-options]]]]

命令行中最关键的参数是选择器(Selector):

它由标签(Tag)和日志级别(Level)共同组成。 标签可理解为虚拟机中某个功能模块的名字,它告诉日志框架用户希望得到虚拟机哪些功能的日志输 出。垃圾收集器的标签名称为“gc”,由此可见,垃圾收集器日志只是HotSpot众多功能日志的其中一项,全部支持的功能模块标签名如下所示:

add,age,alloc,annotation,aot,arguments,attach,barrier,biasedlocking,blocks,bot,breakpoint,bytecode,census
  • 日志级别从低到高,共有Trace,Debug,Info,Warning,Error,Off六种级别,日志级别决定了输 出信息的详细程度

  • 还可以使用修饰器(Decorator)来要求每行日志输出都附加上额外的内容,支持附加 在日志行上的信息包括

    • time:当前日期和时间。
    • uptime:虚拟机启动到现在经过的时间,以秒为单位。
    • timemillis:当前时间的毫秒数,相当于System.currentTimeMillis()的输出。
    • uptimemillis:虚拟机启动到现在经过的毫秒数。
    • timenanos:当前时间的纳秒数,相当于System.nanoTime()的输出。
    • uptimenanos:虚拟机启动到现在经过的纳秒数。
    • pid:进程ID。
    • tid:线程ID。
    • level:日志级别

如果不指定,默认值是uptime、level、tags这三个,此时日志输出类似于以下形式:

[3.080s][info][gc,cpu] GC(5) User=0.03s Sys=0.00s Real=0.01s

1)查看GC基本信息,在JDK 9之前使用-XX:+PrintGC,JDK 9后使用-Xlog:gc:

[0.026s][info][gc] Using G1
[0.258s][info][gc] GC(0) Pause Full (System.gc()) 3M->0M(10M) 6.912ms

2)查看GC详细信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后使用-X-log:gc*, 用通配符*将GC标签下所有细分过程都打印出来,如果把日志级别调整到Debug或者Trace,还将获得更多细节信息:

[0.018s][info][gc,heap] Heap region size: 1M
[0.028s][info][gc     ] Using G1
[0.028s][info][gc,heap,coops] Heap address: 0x0000000702400000, size: 4060 MB, Compressed Oops mode: Zero based, Oop shift amount: 3
[0.245s][info][gc,task      ] GC(0) Using 6 workers of 8 for full compaction
[0.245s][info][gc,start     ] GC(0) Pause Full (System.gc())
[0.245s][info][gc,phases,start] GC(0) Phase 1: Mark live objects
[0.246s][info][gc,stringtable ] GC(0) Cleaned string and symbol table, strings: 2886 processed, 3 removed, symbols: 25848 processed, 0 removed
[0.246s][info][gc,phases      ] GC(0) Phase 1: Mark live objects 1.181ms
[0.246s][info][gc,phases,start] GC(0) Phase 2: Prepare for compaction
[0.247s][info][gc,phases      ] GC(0) Phase 2: Prepare for compaction 0.351ms
[0.247s][info][gc,phases,start] GC(0) Phase 3: Adjust pointers
[0.247s][info][gc,phases      ] GC(0) Phase 3: Adjust pointers 0.504ms
[0.247s][info][gc,phases,start] GC(0) Phase 4: Compact heap
[0.248s][info][gc,phases      ] GC(0) Phase 4: Compact heap 0.776ms
[0.251s][info][gc,heap        ] GC(0) Eden regions: 3->0(3)
[0.251s][info][gc,heap        ] GC(0) Survivor regions: 0->0(0)
[0.251s][info][gc,heap        ] GC(0) Old regions: 0->3
[0.251s][info][gc,heap        ] GC(0) Humongous regions: 0->0
[0.251s][info][gc,metaspace   ] GC(0) Metaspace: 6091K->6091K(1056768K)
[0.251s][info][gc             ] GC(0) Pause Full (System.gc()) 3M->0M(10M) 6.408ms
[0.252s][info][gc,cpu         ] GC(0) User=0.09s Sys=0.00s Real=0.01s
[0.253s][info][gc,heap,exit   ] Heap
[0.253s][info][gc,heap,exit   ]  garbage-first heap   total 10240K, used 919K [0x0000000702400000, 0x0000000800000000)
[0.253s][info][gc,heap,exit   ]   region size 1024K, 1 young (1024K), 0 survivors (0K)
[0.253s][info][gc,heap,exit   ]  Metaspace       used 6118K, capacity 6187K, committed 6272K, reserved 1056768K
[0.253s][info][gc,heap,exit   ]   class space    used 532K, capacity 570K, committed 640K, reserved 1048576K

3)查看GC前后的堆、方法区可用容量变化,在JDK 9之前使用-XX:+PrintHeapAtGC,JDK 9之 后使用-Xlog:gc+heap=debug

[0.018s][info][gc,heap] Heap region size: 1M
[0.018s][debug][gc,heap] Minimum heap 8388608  Initial heap 266338304  Maximum heap 4257218560
[0.265s][debug][gc,heap] GC(0) Heap before GC invocations=0 (full 0): garbage-first heap   total 260096K, used 2048K [0x0000000702400000, 0x0000000800000000)
[0.266s][debug][gc,heap] GC(0)   region size 1024K, 3 young (3072K), 0 survivors (0K)
[0.266s][debug][gc,heap] GC(0)  Metaspace       used 6099K, capacity 6155K, committed 6272K, reserved 1056768K
[0.266s][debug][gc,heap] GC(0)   class space    used 529K, capacity 538K, committed 640K, reserved 1048576K
[0.274s][info ][gc,heap] GC(0) Eden regions: 3->0(3)
[0.274s][info ][gc,heap] GC(0) Survivor regions: 0->0(0)
[0.274s][info ][gc,heap] GC(0) Old regions: 0->3
[0.274s][info ][gc,heap] GC(0) Humongous regions: 0->0
[0.274s][debug][gc,heap] GC(0) Heap after GC invocations=1 (full 1): garbage-first heap   total 10240K, used 919K [0x0000000702400000, 0x0000000800000000)
[0.274s][debug][gc,heap] GC(0)   region size 1024K, 0 young (0K), 0 survivors (0K)
[0.274s][debug][gc,heap] GC(0)  Metaspace       used 6099K, capacity 6155K, committed 6272K, reserved 1056768K
[0.274s][debug][gc,heap] GC(0)   class space    used 529K, capacity 538K, committed 640K, reserved 1048576K

4)查看GC过程中用户线程并发时间以及停顿的时间,在JDK 9之前使用-XX:+PrintGCApplicationConcurrentTime以及-XX:+PrintGCApplicationStoppedTime,JDK 9之后使用-Xlog:safepoint:

[0.266s][info][safepoint] Entering safepoint region: EnableBiasedLocking
[0.266s][info][safepoint] Leaving safepoint region
[0.266s][info][safepoint] Total time for which application threads were stopped: 0.0006428 seconds, Stopping threads took: 0.0004714 seconds
[0.268s][info][safepoint] Application time: 0.0020752 seconds
[0.269s][info][safepoint] Entering safepoint region: G1CollectFull
[0.276s][info][safepoint] Leaving safepoint region
[0.277s][info][safepoint] Total time for which application threads were stopped: 0.0082294 seconds, Stopping threads took: 0.0005833 seconds
[0.278s][info][safepoint] Application time: 0.0011743 seconds
[0.278s][info][safepoint] Entering safepoint region: Halt

5)查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收 集器开始支持)自动调节的相关信息。在JDK 9之前使用-XX:+PrintAdaptive-SizePolicy,JDK 9之后使用-Xlog:gc+ergo*=trace

[0.023s][debug][gc,ergo,heap] Expand the heap. requested expansion amount: 266338304B expansion amount: 266338304B
[0.034s][debug][gc,ergo,refine] Initial Refinement Zones: green: 8, yellow: 24, red: 40, min yellow size: 16
[0.281s][debug][gc,ergo,heap  ] GC(0) Attempt heap shrinking (capacity higher than max desired capacity after Full GC). Capacity: 266338304B occupancy: 3145728B live: 941760B maximum_desired_capacity: 10485759B (70 %)
[0.285s][debug][gc,ergo,heap  ] GC(0) Shrink the heap. requested shrinking amount: 255852545B aligned shrinking amount: 255852544B attempted shrinking amount: 255852544B

6)查看熬过收集后剩余对象的年龄分布信息,在JDK 9前使用-XX:+PrintTenuring-Distribution, JDK 9之后使用-Xlog:gc+age=trace

其他参数

参数描述
-XX:+UseSerialGCJvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收
-XX:+UseParNewGC打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收
-XX:+UseConcMarkSweepGC使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。
-XX:+UseParallelGCJvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收
-XX:+UseParallelOldGC使用Parallel Scavenge + Parallel Old的收集器组合进行回收
-XX:SurvivorRatio新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1
-XX:PretenureSizeThreshold直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
-XX:MaxTenuringThreshold晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代
-XX:UseAdaptiveSizePolicy动态调整java堆中各个区域的大小以及进入老年代的年龄
-XX:+HandlePromotionFailure是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留
-XX:ParallelGCThreads设置并行GC进行内存回收的线程数
-XX:GCTimeRatioGC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效
-XX:MaxGCPauseMillis设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效
-XX:CMSInitiatingOccupancyFraction设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSCompactAtFullCollection由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效
-XX:+CMSFullGCBeforeCompaction设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用
-XX:+UseFastAccessorMethods原始类型优化
-XX:+DisableExplicitGC是否关闭手动System.gc
-XX:+CMSParallelRemarkEnabled降低标记停顿
-XX:LargePageSizeInBytes内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m

4、内存分配与回收策略

1)对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起 一次Minor GC。

/**
 * @Author: yky
 * @CreateTime: 2020-12-18
 * @Description: VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 */
public class GCDemoTwo {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        testAllocation();
    }

    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
    }
}

2)大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者 元素数量很庞大的数组

/**
 * @Author: yky
 * @CreateTime: 2020-12-18
 * @Description: VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 * -XX:PretenureSizeThreshold=3145728
 */
public class GCDemoTwo {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        testPretenureSizeThreshold();
    }

    public static void testPretenureSizeThreshold() {
        byte[] allocation;
        allocation = new byte[4 * _1MB]; //直接分配在老年代中
    }
}

结果 :

Heap
 PSYoungGen      total 9216K, used 6103K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 74% used [0x00000000ff600000,0x00000000ffbf5c60,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
 Metaspace       used 3169K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 345K, capacity 388K, committed 512K, reserved 1048576K

3)长期存活的对象将进入老年代

对象通常在Eden区里诞生,如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象 年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

4)动态对象年龄判定

为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。

5)空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总 空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允 许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大 于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

本博客主要参考周志明老师的《深入理解Java虚拟机》第三版

写博客即是为了记录自己的学习历程,也希望能够结交志同道合的朋友一起学习。文章在撰写过程中难免有疏漏和错误,欢迎指出文章的不足之处;更多内容请点进爱敲代码的小游子查看。

已标记关键词 清除标记
表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2020 CSDN 皮肤主题: 鲸 设计师:meimeiellie 返回首页