JVM第二卷

2022-05-06 15:17:34 浏览数 (1)

垃圾回收

  1. 如何判断对象可以回收
  2. 垃圾回收算法
  3. 分代垃圾回收
  4. 垃圾回收器
  5. 垃圾回收调优

如何判断对象可以回收

当一个对象没有人再引用他的时候,他就可以被回收了

垃圾回收算法有如下几种:

引用计数法

Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。 因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效时,计数器值减1。 任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。 那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。

引用计数法有一个大BUG,就是当存在循环引用现象的时候,就会导致双方引用计数始终无法归零,内存没办法被释放

可达性分析算法—根对象

  • 为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。
  • 所谓"GCroots,或者说tracingGC的“根集合”就是一组必须活跃的引用。
  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
  • 哪些对象可以作为 GC Root ?

在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。

一般来说,如下情况的对象可以作为GC Roots:

  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中JNI(Native方法)的引用的对象

常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。 一个对象可以属于多个root,GC root有几下种: 1.Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,. 2.Thread -活着的线程 3.Stack Local - Java方法的local变量或参数 4.JNI Local - JNI方法的local变量或参数 JNI 5.Global - 全局JNI引用 Monitor Used - 用于同步的监控对象 6.Held by JVM -用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。

MAT分析工具使用说明

分析哪些对象可以作为GC Root

  • mat工具需要配合jmap抓取下来的快照进行分析
  • 打开mat,导入快照文件

四种引用

  1. 强引用

只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

  1. 软引用(SoftReference)

仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用 对象 可以配合引用队列来释放软引用自身

  1. 弱引用(WeakReference)

仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身

  1. 虚引用(PhantomReference)

必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler线程调用虚引用相关方法释放直接内存

对象重写了终结方法,并且没有对象强引用他时,对象会被释放,那么终结方法什么时候,如何被调用呢?

  1. 终结器引用(FinalReference)

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

处理引用队列的线程优先级比较低,这样会导致对象的finalize 方法迟迟不被调用,导致对象占用内存迟迟不能被是方法

软引用应用

先介绍三个虚拟机参数:

代码语言:javascript复制
/**
 * <p>
 *    演示软引用
 *    -Xmx20m -XX: PrintGCDetails -verbose:gc
 * </p>
 * @author 大忽悠
 * @create 2022/1/21 21:20
 */
public class Main
{
    private static final int _4MB=4*1024*1024;

    public static void main(String[] args) throws IOException {
         //并不重要的数据    
        List<byte[]> list=new ArrayList<>();

        for(int i=0;i<5;i  )
        {
            list.add(new byte[_4MB]);
        }
        //阻塞
        System.in.read();
    }
}

对于上面的情况来说,如果数据是不太重要的,可以丢弃一些,那么可以采用软引用形式,即第一次垃圾回收后,如果内存还是不够,那么会回收软引用对象

代码语言:javascript复制
    public static void main(String[] args) throws IOException {

        List<SoftReference<byte[]>> list=new ArrayList<>();

        for(int i=0;i<5;i  )
        {
            SoftReference<byte[]> ref=new SoftReference<>(new byte[_4MB]);
            System.out.println("第" i "次创建软引用对象:" ref.get());
            list.add(ref);
        }
        System.out.println("循环结束:" list.size());
        for (SoftReference<byte[]> softReference : list) {
            System.out.println(softReference.get());
        }
        //阻塞
        System.in.read();
    }

软引用可以在内存敏感的情况下,用于关联一些不重要的资源

引用队列

当软引用中的对象被回收后,对应的软引用对象也就没有意义了,需要被回收,那么怎么回收软引用对象呢? —>引用队列

代码语言:javascript复制
       List<SoftReference<byte[]>> list=new ArrayList<>();

        //引用队列
        ReferenceQueue<byte[]> queue=new ReferenceQueue<>();


        for(int i=0;i<5;i  )
        {
            //管理了引用队列,当软引用关联的byte数组被回收时,软引用对象会被加入引用队列中
            SoftReference<byte[]> ref=new SoftReference<>(new byte[_4MB],queue);
            System.out.println("第" i "次创建软引用对象:" ref.get());
            list.add(ref);
        }

        //从队列中获取无用的软引用对象,并移除
        Reference<? extends byte[]> reference = queue.poll();
        while(reference!=null)
        {
            //解除强引用关系
           list.remove(reference);
           reference=queue.poll();
        }

        System.out.println("循环结束:" list.size());
        for (SoftReference<byte[]> softReference : list) {
            System.out.println(softReference.get());
        }
        //阻塞
        System.in.read();

弱引用应用和引用队列

先不管引用队列,只看弱引用:

代码语言:javascript复制
        List<WeakReference<byte[]>> list=new ArrayList<>();

        //引用队列
       // ReferenceQueue<byte[]> queue=new ReferenceQueue<>();


        for(int i=0;i<10;i  )
        {
            //管理了引用队列,当软引用关联的byte数组被回收时,软引用对象会被加入引用队列中
            WeakReference<byte[]> ref=new WeakReference<>(new byte[_4MB]);
            System.out.println("第" (i 1) "次创建弱引用对象:" ref.get());
            list.add(ref);
            System.out.println("当前剩余的弱引用对象列表: ");
            for (WeakReference<byte[]> weakReference : list) {
                System.out.print(weakReference.get() " ");
            }
            System.out.println();
        }

        //从队列中获取无用的软引用对象,并移除
//        Reference<? extends byte[]> reference = queue.poll();
//        while(reference!=null)
//        {
//            //解除强引用关系
//           list.remove(reference);
//           reference=queue.poll();
//        }

        System.out.println("循环结束:" list.size());
        for (WeakReference<byte[]> weakReference : list) {
            System.out.println(weakReference.get());
        }
        //阻塞
        System.in.read();

输出分析

分析之前首先明确一点,我们每次往list集合中增加了一个弱引用对象,不仅弱引用关联的对象会占用内存,弱引用对象本身也会占用内存,即使关联对象被回收了,弱引用对象依然存在

加上引用队列

代码语言:javascript复制
        List<WeakReference<byte[]>> list=new ArrayList<>();

        //引用队列
       ReferenceQueue<byte[]> queue=new ReferenceQueue<>();


        for(int i=0;i<10;i  )
        {
            //管理了引用队列,当弱引用关联的byte数组被回收时,软引用对象会被加入引用队列中
            WeakReference<byte[]> ref=new WeakReference<>(new byte[_4MB],queue);
            System.out.println("第" (i 1) "次创建弱引用对象:" ref.get());
            list.add(ref);
            System.out.println("当前剩余的弱引用对象列表: ");
            for (WeakReference<byte[]> weakReference : list) {
                System.out.print(weakReference.get() " ");
            }
            System.out.println();
        }

        //从队列中获取无用的软引用对象,并移除
        Reference<? extends byte[]> reference = queue.poll();
        while(reference!=null)
        {
            //解除强引用关系
           list.remove(reference);
           reference=queue.poll();
        }

        System.out.println("循环结束:" list.size());
        for (WeakReference<byte[]> weakReference : list) {
            System.out.println(weakReference.get());
        }
        //阻塞
        System.in.read();

软引用和弱引用区别

软引用是一次FULL GC还不够的时候才回去回收,弱引用一次FULL GC后就会回收

垃圾回收算法

标记清除

定义: Mark Sweep

  • 速度较快
  • 会造成内存碎片

这里清除不是真正的清零,而是将被回收的内存区域标记为了空闲内存

标记整理

定义:Mark Compact

  • 速度慢
  • 没有内存碎片
  • 在清理垃圾的过程中,将可用的对象向前移动

由于整理牵扯到对象的移动,需要考虑内存的拷贝,还有引用对象地址的改变

复制

定义:Copy

  • 不会有内存碎片
  • 需要占用双倍内存空间
  • 划分出一块和from一样大小的空闲内存
  • 对form上面非垃圾对象做上标记
  • 将这些存活对象赋值到to内存上,这个过程也完成了整理的工作
  • 然候将form内存清空
  • 交换form和to的位置

分代垃圾回收

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的 对象年龄加1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

相关 VM 参数

GC分析

参数设置:

代码语言:javascript复制
-Xms20M -Xmx20M  -Xmn10M  -XX: UseSerialGC -XX: PrintGCDetails -verbose:gc
-Xms20M---堆初始化大小 -Xmx20M---堆最大大小 -Xmn10M--->新生代大小 -XX: UseSerialGC --->防止幸存区大小动态调整 -XX: PrintGCDetails -verbose:gc

实例演示:

代码语言:javascript复制
/**
 * @author 大忽悠
 * @create 2022/1/21 21:20
 */
public class Main
{
    private static final int _4MB=4*1024*1024;

    //-Xms20M -Xmx20M  -Xmn10M  -XX: UseSerialGC -XX: PrintGCDetails -verbose:gc
    // -Xms20M---堆初始化大小 -Xmx20M---堆最大大小 -Xmn10M--->新生代大小 -XX: UseSerialGC --->防止幸存区大小动态调整 -XX: PrintGCDetails -verbose:gc
    public static void main(String[] args) throws IOException {

    }

}

垃圾日志分析

实例分析:

代码语言:javascript复制
public class Main
{
    private static final int _521KB=521*1024;
    private static final int _1MB=1024*1024;
    private static final int _6MB=6*1024*1024;
    private static final int _7MB=7*1024*1024;
    private static final int _8MB=8*1024*1024;

    //-Xms20M -Xmx20M  -Xmn10M  -XX: UseSerialGC -XX: PrintGCDetails -verbose:gc
    public static void main(String[] args) throws IOException {
        List<byte[]> bytes = Arrays.asList(new byte[_7MB], new byte[_521KB], new byte[_521KB]);
    }

}

最后可以看出,并不是一定要到达阈值时,才会将对象移动到老年代,当新生代内存不足时,会将新生代存活对象移动到老年代,但是如果此时老年代内存也不足,会触发Full GC

大对象,OOM分析

大对象在老年代空间足够,新生代空间不足时,会直接晋升到老年代

OOM分析

当一个线程抛出OOM异常后,它所占据的内存资源会被全部释放掉,从而不影响其他线程的运行

垃圾回收器

  1. 串行
  • 单线程
  • 堆内存较小,适合个人电脑
  1. 吞吐量优先
  • 多线程
  • 堆内存较大,多核 cpu
  • 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高(执行STW次数最少,单次稍长,总花费时间保持最短)
  1. 响应时间优先
  • 多线程
  • 堆内存较大,多核 cpu
  • 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5(单次最短,执行STW次数多)

STW是垃圾回收时,所有线程处垃圾回收线程外的暂停时间

串行

开启串行垃圾回收器的VM参数:

代码语言:javascript复制
-XX: UseSerialGC = Serial   SerialOld
 串行垃圾回收器分为两个部分:新生代垃圾回收,采用复制算法   老年代垃圾回收,采用标记加整理算法                   

垃圾回收线程执行垃圾回收时,会阻塞其他线程,因为垃圾回收会涉及到对象地址的改变

吞吐量优先

代码语言:javascript复制
-XX: UseParallelGC ~ -XX: UseParallelOldGC 
新生代垃圾回收器,采用复制算法    老年代垃圾回收,采用标记加整理算法  
这两个垃圾回收器是多线程的,并且只要开启其中一个,另一个就会跟着开启
并且线程数取决于CPU核心数
jdk1.8默认使用的垃圾回收器
代码语言:javascript复制
-XX: UseAdaptiveSizePolicy 
自适应动态调整新生代区域比例,伊甸园和幸存区比例,还有阈值
代码语言:javascript复制
-XX:GCTimeRatio=ratio : 调整垃圾回收和总时间的占比,公式: 1/(1 radio),radio默认值为99,即垃圾回收的时间不能超过总时间的百分之一,如果你工作了一百分钟,那么最多只能有一分钟用于垃圾回收,如果超过这个时间,便会动态调整堆的大小达到这个目标,一般是增大堆内存
代码语言:javascript复制
-XX:MaxGCPauseMillis=ms : 每一次垃圾回收的暂停时间限制,这个配置和上面的互斥,因为radio越小,一般堆会分配的更大,那么单次垃圾回收的时间会变长
代码语言:javascript复制
-XX:ParallelGCThreads=n :控制运行时的线程数

响应时间优先(CMS)

代码语言:javascript复制
开启响应时间优先的垃圾回收期的VM参数:
-XX: UseConcMarkSweepGC ~ -XX: UseParNewGC ~ SerialOld
基于并发标记清除的老年代垃圾回收器  新生代使用的还是标记整理算法  
如果并发失败了,会采取补救措施,从老年代的并发垃圾回收器退化为单线程的标记整理垃圾回收器
  • 这里并发指的是垃圾回收线程在某几个阶段可以和用户线程一起执行
  • 并行指的是多个垃圾回收器并行执行,在此期间用户线程停止执行
  1. 老年代到达内存不足,执行中的线程来到第一个安全点
  2. 此时CMS垃圾回收器开始工作,进行初始化标记,即标记根对象(GC Roots)直接关联的对象,此时会阻塞其他用户线程(Stop the world)的执行
  3. 用户线程恢复运行,CMS进行并发标记;
  4. Stop the world,重新标记,因为用户线程上一步也在执行,可能会改变对象的引用,对垃圾回收造成干扰;
  5. 用户线程恢复运行,CMS进行清理工作
代码语言:javascript复制
 -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads 
 并行执行的垃圾回收器线程数        并发执行的垃圾回收器线程数,一般设置为并行数的1/4

CMS在最后执行并发清理的过程中,其他用户线程可能会产生新的垃圾,这些垃圾只有等待下一次垃圾清理的时候,才能被回收,这些垃圾我们称为浮动垃圾

垃圾回收过程中会产生新垃圾,这些垃圾无法等待堆内存不足了再进行回收,这些新垃圾没地放了, 因此我们需要提前预留一些空间存放这些浮动垃圾

代码语言:javascript复制
设置当老年代的内存占用达到多少百分比时,执行垃圾回收,为的是预留空间存放浮动垃圾,百分比越小,触发CMS垃圾回收的时机越早
 -XX:CMSInitiatingOccupancyFraction=percent 

进行重新标记的时候,新生代的对象可能会引用老年代的对象,因此需要重新扫描整个堆,但是新生代中的垃圾对象比较多,如果从新生代的对象引用去做可达性分析找到老年代对象,本来这些新生代对象就要被回收掉,会造成额外扫描资源浪费

代码语言:javascript复制
在进行重新标记之前,先进行一次新生代的垃圾回收操作
 -XX: CMSScavengeBeforeRemark

因为CMS基于标记加清除算法,会产生内存碎片,如果老年代产生了较多的内存碎片,同时新生代也产生了很多内存碎片,此时新创建的对象无法放入新生代,也无法放入老年代,这样就造成了并发失败。

此时CMS垃圾回收器无法正常工作,会退化为串行的垃圾回收器,会对碎片进行整理,碎片减少了,才能恢复工作,但是这样垃圾回收的时间会飙升

G1

定义:Garbage First

  • 2004 论文发布
  • 2009 JDK 6u14 体验
  • 2012 JDK 7u4 官方支持
  • 2017 JDK 9 默认

适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region
  • 整体上是 标记 整理 算法,两个区域之间是 复制 算法

相关 JVM 参数

  • -XX: UseG1GC —启用G1
  • -XX:G1HeapRegionSize=size —设置Region大小
  • -XX:MaxGCPauseMillis=time

G1开创了面向局部收集的设计思路和基于Region的布局设计,并且还提供并发的类卸载支持,被称为全能垃圾收集器

G1 垃圾回收阶段

Young Collection

会 STW

对于G1来说,他会把堆内存划分为大小相等的一个个区域,每个区域都可以独立作为伊甸园,新生代和老年区,程序运行时会加载一些类对象到伊甸园区域中保存,当伊甸园区域逐渐被占满,会触发一次新生代的垃圾回收,会触发一个STW

G1是以Region作为单次回收的最小单元,并且每个Region都有自己回收优先级,每次先回收哪些优先级大的region区域

新生代垃圾回收,会将幸存的对象以复制的算法放入幸存区中

幸存区的对象比较多或者幸存区对象存回年龄超过一定时间,再下一次新生代内存分配失败触发Minor GC后,会将幸存区中满足条件的对象移动到老年代; 不满足年龄的幸存区对象会再次拷贝到另一个幸存区中

Young Collection CM

在 Young GC 时会进行 GC Root 的初始标记

初始标记仅仅是标记一下GC ROOTS能直接关联到的对象,并且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿

老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定

代码语言:javascript复制
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

Mixed Collection

会对 E、S、O 进行全面垃圾回收

最终标记(Remark)会 STW

拷贝存活(Evacuation)会 STW

代码语言:javascript复制
-XX:MaxGCPauseMillis=ms

G1可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收效益最大,这就是G1收集器的Mixed GC模式

G1虽然仍然保留新生代和老年代的概念,但新生代和老年代不再固定,他们都是一系列区域的动态集合,G1收集器能够建立可预测的停顿时间模型,是因为他将Region作为单词回收的最小单元,即每次回收到的内存空间都是Region大小的整数倍,这样可以避免在整个java堆中进行全区域的垃圾收集。

G1会去跟踪各个region里面的垃圾堆积的价值大小,价值即回收所获得的空间大小及回收需要的时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定的收集停顿时间来优先处理回收价值收益最大的那些region

在决定进行回收的时候,g1会对region按照回收价值和成本排序,根据用户期望的停顿时间来指定回收计划,可以自由选择任意多个region组成回收集,然后把决定回收的那一部分region的存活对象复制到空的region中,再清理掉整个旧的region的全部空间

Full GC

FULL GC是整堆收集,收集整个java堆和方法区的垃圾收集

  • SerialGC

新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足发生的垃圾收集 - full gc

  • ParallelGC

新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足发生的垃圾收集 - full gc

  • CMS

新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足

会触发cms的垃圾回收,如果在cms的并发清理过程中,垃圾回收的速度赶不上新对象分配的速度,会导致内存分配失败,进而触发STW,临时启用serial old单线程垃圾收集器来重新进行老年代的垃圾收集,进行的是FULL GC

  • G1

新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足

对于G1来说,如果老年代堆空间(所有是老年代的region占用总控)占比默认达到百分之45时,会触发并发标记阶段,然后是最终标记,最后进行筛选回收,即混合收集 G1为每一个region设计了两个名为TAMS(Top at Mark Start)的指针,把region中的一部分空间划分出来用户并发回收过程中的新对象的分配,并发回收时新对象的分配必须在这两个指针位置以上。 G1默认在这个地址以上的对象是被隐式标记过的,即默认存活,不纳入回收范围 如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程,导致FULL GC而产生长时间的STW,这一点和CMS中FULL GC类似

Young Collection 跨代引用

新生代回收的跨代引用(老年代引用新生代)问题

  • 卡表与 Remembered Set
  • 在引用变更时通过 post-write barrier dirty card queue
  • concurrent refinement threads 更新 Remembered Set

g1中每个region都维护了一个属于自己的记忆集,这些记忆集会记录下别的region指向自己的指针并标记这些指针分别在哪些卡页范围内。

g1的记忆集本质是一个哈希表,key是别的region的起始地址,value是一个集合,里面存放的元素是卡表的索引号,这种双向的卡表结构(卡表是我指向谁,这种数据结构还记录了谁指向我)

卡表和记忆集的关系,参考hashMap与map的关系

卡表对应的字节数组,每一个元素都对应内存区域中的一个特定的内存块,这个内存块被称为卡页。

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个或对个对象的字段存在跨代指针,那就将对应卡表的数组元素标识为1,称这个元素变脏了。

在垃圾回收时,只要筛选出卡表中变脏的元素,就可以得出哪些卡页中的内存卡中包含跨代指针,把他们加入GC ROOTS中扫描

Remark

pre-write barrier satb_mark_queue

Remark是重新标记阶段

我们知道现在主流的垃圾回收器都是依靠可达性分析算法来判定对象是否存活,但是可达性分析算法要求全过程必须基于一个能够保障一致性快照中才能进行分析,这意味着必须全程冻结用户线程进行。

在GC ROOTS枚举过程中,由于GC ROOTS的对象数量相对较少,并且在OopMap等技术的加持下,枚举GC ROOTS的时间相对短暂且固定。

但是从GC ROOTS玩下继续遍历对象图,这个耗费时间就和java堆容量成正比例关系了,堆越大,存储对象越多,对象图结构越复杂,表示对象产生停顿时间就越长。

我们知道GC ROOTS的枚举必须是STW的,但是如果从GC ROOTS往下继续标记的过程也STW,未免时间也太长了,必须想办法让这个过程和用户线程并发执行,但是如果在进行标记过程中,用户线程改变了某个已经被标记过的对象引入,就会造成不一致性,我们需要解决这个问题,这就引入了三色标记法:

  • 白色:对象还没有被垃圾回收器访问过,如果在标记结束阶段,对象还是白色,代表不可达
  • 黑色:被访问过,是存活对象
  • 灰色:被访问过,但是中途被用户线程改变了引用

如果在标记过程中用户线程改变了某个对象的引用,就将该对象标记为灰色,并将其加入stab队列中,等待重新标记阶段,垃圾收集器会去访问该队列里面所有对象,判断是否存活,即重新扫描一遍这些灰色对象

**如何在用户线程改变了某个对象的引用,就将该对象标记为灰色的呢? **

这是通过写屏障实现的,即对某个对象进行修改的时候,会在修改前加入一个写屏障,将当前对象标记为灰色,即放入stab队列中去

JDK 8u20 字符串去重

优点:节省大量内存 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

代码语言:javascript复制
-XX: UseStringDeduplication
代码语言:javascript复制
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个 char[]
  • 注意,与 String.intern() 不一样
  • String.intern() 关注的是字符串对象
  • 而字符串去重关注的是 char[]
  • 在 JVM 内部,使用了不同的字符串表

JDK9已经对字符串底层的char数组做了改变,底层换成了byte[]数组

JDK 8u40 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

-XX: ClassUnloadingWithConcurrentMark 默认启用

判定一个类型是否属于不在被使用的类的条件比较苛刻:

  • 该类所有实例都被回收
  • 加载该类的类加载器被回收
  • 该类对应的class对象没有被引用

一般来说,只有在大量使用反射,动态代理,CGLIB等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通过需要java虚拟机具备类卸载的能力,保证不会对方法区造成大的内存压力

方法区的垃圾回收主要涉及废弃的常量和不再使用的类型

JDK 8u60 回收巨型对象

一个对象大于 region 的一半时,称之为巨型对象

G1 不会对巨型对象进行拷贝

回收时被优先考虑

G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉

JDK 9 并发标记起始时间的调整

并发标记必须在堆空间占满前完成,否则退化为 FullGC

JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent

JDK 9 可以动态调整

  • -XX:InitiatingHeapOccupancyPercent 用来设置初始值,当老年代占堆空间容量达到百分之多少时,触发并发标记
  • 进行数据采样并动态调整,动态调整百分比
  • 总会添加一个安全的空档空间,每个region都有一个空间,来存放并发标记过程中产生的浮动垃圾

0 人点赞