JVM之垃圾回收

1. 如何判断对象可以回收

引用计数法

当一个对象被引用时, 该对象的引用值就加1, 当这个对象不再被引用时, 引用值就减1, 当引用值为0时, 就表示该对象可以被垃圾回收器回收。引用计数法有一个弊端, 那就是当两个对象相互引用的时, 两个对象的引用值都为1, 此时这两个对象就算用不到了, 也不会被回收。

在这里插入图片描述

可达性分析算法

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象, 看是否能够沿着 GC Root对象 为起点的引用链找到该对象, 找不到, 表示可以回收
  • 哪些对象可以作为 GC Root
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中 JNI (即一般说的Native方法)引用的对象

我们使用Eclipse Memory Analyzer(内存分析工具)对一下代码进行分析:

public static void main(String[] args) throws IOException {
    ArrayList<Object> list = new ArrayList<>();
    list.add("a");
    list.add("b");
    System.out.println(1);
    System.in.read();

    list = null;
    System.out.println(2);
    System.in.read();
    System.out.println("end");
}
  1. 使用jps命令找到程序进程

    PS D:\JVM\JVM_Study> jps
    14304 Jps
    11060 Demo1
    19532
    26108 Launcher

  2. 使用jmap命令生成内存快照

    PS D:\JVM\JVM_Study> jmap -dump:format=b,live,file=1.bin 11060
    Dumping heap to D:\JVM\JVM_Study\1.bin …
    Heap dump file created

    命令解释:

    • dump: 转储文件
    • format=b: 使用二进制文件存储
    • live: 只抓取存活的对象, 并且抓取快照之前会进行垃圾回收
    • file=1.bin: 文件名
    • 11060: 进程的id
  3. 使用mat打开生成的快照, 选择GC Roots分析

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TGDRqBrY-1650980581520)(C:\Users\86133\Pictures\JVM\垃圾回收\GC_Root.png)]

list=null前:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5lz2uWWi-1650980581521)(C:\Users\86133\Pictures\JVM\垃圾回收\null前.png)]

list=null后:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VGpCSq9M-1650980581521)(C:\Users\86133\Pictures\JVM\垃圾回收\null后.png)]

可以看到, 在第一个快照中找到了ArrayList对象, 并且里面存储的对象也能找到, 第二个快照(也就是list=null后), ArrayList对象找不到了, 说明被回收了。

五种引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WwafWp4C-1650980581522)(C:\Users\86133\Pictures\JVM\垃圾回收\引用类型.png)]

  1. 强引用
    • 只有所有GC Roots对象都不通过【强引用】引用该对象, 该对象才能被垃圾回收
  2. 软引用(SoftReference)
    • 仅有软引用引用该对象时, 在垃圾回收后, 内存仍不足时会再次出发垃圾回收, 回收软引用对象
    • 可以配合引用队列来释放软引用自身
  3. 弱引用(WeakReference)
    • 仅有弱引用引用该对象时, 在垃圾回收时, 无论内存是否充足, 都会回收弱引用对象
    • 可以配合引用队列来释放弱引用自身
  4. 虚引用(PhantomReference)
    • 必须配合引用队列使用, 主要配合ByteBuffer使用, 被引用对象回收时, 会将虚引用入队
    • Reference Handler线程调用虚引用相关方法(如unsafe.freememory)释放直接内存
  5. 终结器引用(FinalReference)
    • 无需手动编码, 但其内部配合引用队列使用, 在垃圾回收时, 终结器引用入队(被引用对象暂时没有被回收)
    • 再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法, 第二次 GC 时才能回收被引用对象

软引用演示:

//虚拟机参数-Xmx20m -XX:+PrintGCDetails -verbose:gc

public static int _4MB = 4 * 1024 * 1024;

public static void main(String[] args) throws IOException {
    //method1();
    method2();
}

// 强引用
public static void method1() throws IOException {
    ArrayList<byte[]> list = new ArrayList<>();
    for(int i = 0; i < 5; i++) {
        list.add(new byte[_4MB]);
    }
    System.in.read();
}

// 演示软引用
public static void method2() throws IOException {
    ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
    for(int i = 0; i < 5; i++) {
        SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
        System.out.println(ref.get());
        list.add(ref);
        System.out.println(list.size());
    }
    System.out.println("循环结束: " + list.size());
    for(SoftReference<byte[]> ref : list) {
        System.out.println(ref.get());
    }
}

强引用会直接因为内存不足报错:

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

软引用分析:

[B@1b6d3586
1
[B@4554617c
2
[B@74a14482
3

在第四次循环之前内存已经吃紧了, 所以触发了一次垃圾回收, 这是一次minor gc
[GC (Allocation Failure) [PSYoungGen: 1900K->488K(6144K)] 14188K->12996K(19968K), 0.0010493 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@1540e19d
4

第五次循环之前内存已经彻底不够用的, 而且此时一个minor gc也释放不了多少内存, 所以触发了一次full fc
[GC (Allocation Failure) --[PSYoungGen: 4696K->4696K(6144K)] 17204K->17220K(19968K), 0.0005897 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 4696K->4529K(6144K)] [ParOldGen: 12524K->12477K(13824K)] 17220K->17006K(19968K), [Metaspace: 3225K->3225K(1056768K)], 0.0039704 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) --[PSYoungGen: 4529K->4529K(6144K)] 17006K->17038K(19968K), 0.0006478 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 4529K->0K(6144K)] [ParOldGen: 12509K->604K(8704K)] 17038K->604K(14848K), [Metaspace: 3225K->3225K(1056768K)], 0.0052899 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@677327b6
5

可以看到这次full gc将前面的软引用的几个byte数组全部回收, 只留下最后一个byte数组
循环结束: 5
null
null
null
null
[B@677327b6
Heap
PSYoungGen total 6144K, used 4263K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
eden space 5632K, 75% used [0x00000000ff980000,0x00000000ffda9f70,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 8704K, used 604K [0x00000000fec00000, 0x00000000ff480000, 0x00000000ff980000)
object space 8704K, 6% used [0x00000000fec00000,0x00000000fec971a8,0x00000000ff480000)
Metaspace used 3231K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 351K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

上面的代码可以看到, 虽然软引用的对象已经被回收了, 但是软引用本身还没有被回收, 虽然软引用只占用很少的内存, 但是也不能留下它, 可以使用引用队列来清理软引用:

// 演示软引用搭配引用队列
public static void method3() throws IOException {
    ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
    // 引用队列
    ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

    for(int i = 0; i < 5; i++) {
        // 关联了引用队列, 当软引用所关联的 byte[] 被回收时, 软引用自己会加入到 queue 中去
        SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
        System.out.println(ref.get());
        list.add(ref);
        System.out.println(list.size());
    }

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

    System.out.println("=====================");
    for(SoftReference<byte[]> ref : list) {
        System.out.println(ref.get());
    }
}

[B@1b6d3586
1
[B@4554617c
2
[B@74a14482
3
[B@1540e19d
4
[B@677327b6

5

[B@677327b6

Process finished with exit code 0

可以看到, 软引用本身也被回收了

弱引用示例:

//虚拟机参数-Xmx20m -XX:+PrintGCDetails -verbose:gc

public static int _4MB = 4 * 1024 * 1024;

public static void main(String[] args) {
    //method1();
    method2();
}

// 演示弱引用
public static void method1() {
    List<WeakReference<byte[]>> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB]);
        list.add(weakReference);

        for (WeakReference<byte[]> wake : list) {
            System.out.print(wake.get() + ",");
        }
        System.out.println();
    }
}

// 演示弱引用搭配引用队列
public static void method2() {
    List<WeakReference<byte[]>> list = new ArrayList<>();
    ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

    for (int i = 0; i < 9; i++) {
        WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB], queue);
        list.add(weakReference);
        for (WeakReference<byte[]> wake : list) {
            System.out.print(wake.get() + ",");
        }
        System.out.println();
    }
    System.out.println("===========================================");
    Reference<? extends byte[]> poll = queue.poll();
    while (poll != null) {
        list.remove(poll);
        poll = queue.poll();
    }
    for (WeakReference<byte[]> wake : list) {
        System.out.print(wake.get() + ",");
    }
}

[GC (Allocation Failure) [PSYoungGen: 5632K->488K(6144K)] 5632K->4760K(19968K), 0.0041741 secs] [Times: user=0.00 sys=0.03, real=0.02 secs]
[B@1b6d3586,
[B@1b6d3586,[B@4554617c,
[B@1b6d3586,[B@4554617c,[B@74a14482,
[B@1b6d3586,[B@4554617c,[B@74a14482,[B@1540e19d,

第五次循环前, 触发full gc, 回收弱引用对象
[Full GC (Ergonomics) [PSYoungGen: 4951K->0K(6144K)] [ParOldGen: 12464K->595K(13824K)] 17415K->595K(19968K), [Metaspace: 3207K->3207K(1056768K)], 0.0052947 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
null,null,null,null,[B@677327b6,
null,null,null,null,[B@677327b6,[B@14ae5a5,
null,null,null,null,[B@677327b6,[B@14ae5a5,[B@7f31245a,
null,null,null,null,[B@677327b6,[B@14ae5a5,[B@7f31245a,[B@6d6f6e28,

第九次循环前, 内存再次不够触发full gc, 最后只剩下一个弱引用对象
[Full GC (Ergonomics) [PSYoungGen: 4207K->0K(6144K)] [ParOldGen: 12911K->619K(13824K)] 17118K->619K(19968K), [Metaspace: 3224K->3224K(1056768K)], 0.0061594 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
null,null,null,null,null,null,null,null,[B@135fbaa4,

===========================================
[B@135fbaa4,Heap
PSYoungGen total 6144K, used 4247K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
eden space 5632K, 75% used [0x00000000ff980000,0x00000000ffda5c78,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 13824K, used 619K [0x00000000fec00000, 0x00000000ff980000, 0x00000000ff980000)
object space 13824K, 4% used [0x00000000fec00000,0x00000000fec9ae68,0x00000000ff980000)
Metaspace used 3231K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

2. 垃圾回收算法

标记清除算法

标记清除算法(Mark Sweep):

  • 速度较快
  • 会产生内存碎片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NqPszuLZ-1650980581523)(C:\Users\86133\Pictures\JVM\垃圾回收\标记清除算法.png)]

标记整理算法

标记整理算法(Mark Compact)

  • 速度慢
  • 没有内存碎片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lkf0C8vl-1650980581524)(C:\Users\86133\Pictures\JVM\垃圾回收\标记整理算法.png)]

复制算法

复制(Copy)

  • 不会有内存碎片
  • 需要占用两倍内存空间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qiMtxHKI-1650980581525)(C:\Users\86133\Pictures\JVM\垃圾回收\复制算法.png)]

3. 分代垃圾回收

  • 新创建的对象首先分配在eden
  • 新生代空间不足时, 触发minor gc, eden区和from区存活的对象使用 copy 复制到 to 中, 存活的对象年龄加一, 然后交换 from 和 to
  • minor gc 会引发 stop the world(咋瓦鲁多), 暂停其他线程, 等垃圾回收结束后, 恢复用户线程运行
  • 当幸存区对象的寿命超过阈值时, 会晋升到老年代, 默认最大的寿命是15(4bit)
  • 当老年代空间不足时, 会先触发minor gc, 如果空间仍然不足, 那么就触发full gc , STW的时间更长

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HWgM0iRU-1650980581526)(C:\Users\86133\Pictures\JVM\垃圾回收\分代垃圾回收.png)]

相关JVM参数

含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
Full GC前Minor GC-XX:+ScavengeBeforeFullGC

4. 垃圾回收器

相关概念:

  • 并行收集: 指多条垃圾收集线程并行工作, 但此时用户线程仍处于等待状态
  • 并发收集: 指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行), 用户程序在继续运行, 而垃圾收集程序运行在另一个CPU上
  • 吞吐量: 即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )), 例如, 虚拟机共运行 100 分钟, 垃圾收集器花掉 1 分钟, 那么吞吐量就是 99%

串行

  • 单线程
  • 堆内存较少, 适合个人电脑

-XX:+UseSerialGC = Serial + SerialOld

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qwSCUIIp-1650980581527)(C:\Users\86133\Pictures\JVM\垃圾回收\串行.png)]

安全点: 让其他线程都在这个点停下来, 以免垃圾回收时移动对象地址, 使得其他线程找不到被移动的对象
因为是串行的, 所以只有一个垃圾回收线程。且在该线程执行回收工作时, 其他线程进入阻塞状态。

Serial 收集器
Serial 收集器是最基本的、发展历史最悠久的收集器
特点: 单线程、简单高效(与其他收集器的单线程相比), 采用复制算法。对于限定单个 CPU 的环境来说, Serial 收集器由于没有线程交互的开销, 专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时, 必须暂停其他所有的工作线程, 直到它结束(Stop The World)

ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本
特点: 多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同, 在 CPU 非常多的环境中, 可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题

Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本
特点: 同样是单线程收集器, 采用标记-整理算法

吞吐量优先

  • 多线程
  • 堆内存较大, 多核CPU
  • 让单位时间内, STW 的时间最短

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC

-XX:+UseAdaptiveSizePolicy

-XX:GCTimeRatio=ratio

-XX:MaxGCPauseMillis=ms

-XX:ParallelGCThreads=n

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6LHaOmm9-1650980581528)(C:\Users\86133\Pictures\JVM\垃圾回收\吞吐量优先.png)]

Parallel Scavenge 收集器
与吞吐量关系密切, 故也称为吞吐量优先收集器
特点: 属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区), 又是并行的多线程收集器(与 ParNew 收集器类似)
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是: GC自适应调节策略(与 ParNew 收集器最重要的一个区别)

**GC自适应调节策略: **
Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。
当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等, 虚拟机会根据系统的运行状况收集性能监控信息, 动态设置这些参数以提供最优的停顿时间和最高的吞吐量, 这种调节方式称为 GC 的自适应调节策略。

Parallel Scavenge 收集器使用两个参数控制吞吐量:

  • XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)

  • XX:GCTimeRatio=rario 直接设置吞吐量的大小(默认值为99, 但是不容易达到, 一般设置为19)

Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本
特点: 多线程, 采用标记-整理算法(老年代没有幸存区)

响应时间优先

  • 多线程
  • 堆内存较大, 多核CPU
  • 尽可能让 STW 的单次时间最短

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w94YEmXv-1650980581529)(C:\Users\86133\Pictures\JVM\垃圾回收\响应时间优先.png)]

CMS 收集器
Concurrent Mark Sweep, 一种以获取最短回收停顿时间为目标的老年代收集器
特点: 基于标记-清除算法实现。并发收集, 低停顿, 但是会产生内存碎片
应用场景: 适用于注重服务的响应速度, 希望系统停顿时间最短, 给用户带来更好的体验等场景下。如 web 程序、b/s 服务
CMS 收集器的运行过程分为下列4步:
初始标记: 标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
并发标记: 进行 GC Roots Tracing 的过程, 找出存活对象且用户线程可并发执行。
重新标记: 为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
并发清除: 对标记的对象进行清除回收, 清除的过程中, 可能任然会有新的垃圾产生, 这些垃圾就叫浮动垃圾, 如果当用户需要存入一个很大的对象时, 新生代放不下去, 老年代由于浮动垃圾过多, 就会退化为 serial Old 收集器, 将老年代垃圾进行标记-整理, 当然这也是很耗费时间的!

CMS 收集器的内存回收过程是与用户线程一起并发执行的, 可以搭配 ParNew 收集器(多线程, 新生代, 复制算法)与 Serial Old 收集器(单线程, 老年代, 标记-整理算法)使用。

G1

定义: Garbage First

适用场景:

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

相关参数:
JDK8 并不是默认开启的, 所需要参数开启
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time

垃圾回收阶段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WqcfUTm4-1650980581530)(C:\Users\86133\Pictures\JVM\垃圾回收\G1垃圾回收阶段.png)]

Young Collection: 对新生代垃圾收集
Young Collection + Concurrent Mark: 如果老年代内存到达一定的阈值了, 新生代垃圾收集同时会执行一些并发的标记
Mixed Collection: 会对新生代 + 老年代 + 幸存区等进行混合收集, 然后收集结束, 会重新进入新生代收集

Young Collection

新生代存在 STW:
分代是按对象的生命周期划分, 分区则是将堆空间划分连续几个不同小区间, 每一个小区间独立回收, 可以控制一次回收多少个小区间, 方便控制 GC 产生的停顿时间!
E: 伊甸园, S: 幸存区, O: 老年代

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2YFfvY16-1650980581531)(C:\Users\86133\Pictures\JVM\垃圾回收\Young Collection.gif)]

Young Collection + CM
  • 在 Young GC 时会进行 GC Root 的初始标记

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

    -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-paI6xtfx-1650980581532)(C:\Users\86133\Pictures\JVM\垃圾回收\Young Collection + CM.png)]

Mixed Collection

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

  • 最终标记(Remark), 会 STW
  • 拷贝存活(Evacuation), 会 STW

-XX:MaxGCPauseMillis=ms 用于指定最长的停顿时间

为什么有的老年代被拷贝了, 有的没拷贝?
因为指定了最大停顿时间, 如果对所有老年代都进行回收, 耗时可能过高, 为了保证时间不超过设定的停顿时间, 会回收最有价值的老年代(回收后, 能够得到更多内存)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eXGvgsg8-1650980581532)(C:\Users\86133\Pictures\JVM\垃圾回收\Mixed Collection.png)]

Full GC
  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足
  • G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足
    • 如果垃圾产生速度慢于垃圾回收速度, 不会触发 Full GC, 还是并发地进行清理
    • 如果垃圾产生速度快于垃圾回收速度, 便会触发 Full GC, 然后退化成 serial Old 收集器串行的收集, 就会导致停顿的时间较长
Young Collection 跨代引用
  • 新生代回收的跨代引用(老年代引用新生代)问题

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sWVr0pCV-1650980581533)(C:\Users\86133\Pictures\JVM\垃圾回收\跨代引用.png)]

  • 卡表 与 Remembered Set

    • 卡表: O 被划分为多个区域(一个区域512K)

    • 脏卡: 如果卡表中有区域引用了新生代对象, 则该区域被称为脏卡

    • Remembered Set 存在于E中, 用于保存新生代对象对应的脏卡

  • 在引用变更时通过 post-write barried + dirty card queue

  • concurrent refinement threads 更新 Remembered Set

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YML75Qqy-1650980581534)(C:\Users\86133\Pictures\JVM\垃圾回收\脏卡.png)]

Remark

pre-write barrier + satb_mark_queue
重新标记阶段
在垃圾回收时, 收集器处理对象的过程中

  • 黑色: 已被处理, 需要保留的
  • 灰色: 正在处理中的
  • 白色: 还未处理的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PbxTTwy0-1650980581535)(C:\Users\86133\Pictures\JVM\垃圾回收\Remark.png)]

看一下案例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZiiAuaO4-1650980581538)(C:\Users\86133\Pictures\JVM\垃圾回收\Remark-1.png)]

  • 在处理B的过程中, B断开了对C的引用, 这样B会变成黑色, C最终会标记为白色

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDK8q8Pv-1650980581539)(C:\Users\86133\Pictures\JVM\垃圾回收\Remark-2.png)]

  • 但是此时A又引用了C, 可是C已经被标记为白色, C最终会被回收

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g8Y7YKOm-1650980581542)(C:\Users\86133\Pictures\JVM\垃圾回收\Remark-3.png)]

  • 这样就有问题了, C还在被A引用, 不该被回收, 这时就要用到Remark, 在C的引用改变时, JVM会给加上一个pre-write barrier(写屏障)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QFTQgYOa-1650980581544)(C:\Users\86133\Pictures\JVM\垃圾回收\Remark-4.png)]

  • 当A引用C时, C的引用发生变化, 写屏障指令触发, 将C放入一个队列(satb_mark_queue)中, 并将C的状态修改为处理中状态

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lV9taoFt-1650980581545)(C:\Users\86133\Pictures\JVM\垃圾回收\Remark-5.png)]

  • 在并发标记阶段结束以后, 重新标记阶段会 STW , 然后将放在该队列中的对象重新处理, 发现有A强引用C, 就会处理它, 由灰色变成黑色

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4zUilY3p-1650980581547)(C:\Users\86133\Pictures\JVM\垃圾回收\Remark-6.png)]

JDK 8u20 字符串去重

-XX:+UseStringDeduplication

  • 优点: 节省大量内存
  • 缺点: 略微多占用了 cpu 时间, 新生代回收时间略微增加
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}

过程:

  • 将所有新分配的字符串(底层是 char[])放入一个队列
  • 当新生代回收时, G1 并发检查是否有重复的字符串
  • 如果字符串的值一样, 就让他们引用同一个char[]
  • 注意, 其与 String.intern() 的区别
    • String.intern() 关注的是字符串对象
    • 字符串去重关注的是 char[]
    • 在 JVM 内部, 使用了不同的字符串标
JDK 8u40 并发标记类卸载

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

JDK 8u60 回收巨型对象
  • 一个对象大于 region 的一半时, 称之为巨型对象
  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1 会跟踪老年代所有 incoming 引用, 这样老年代 incoming 引用为0 的巨型对象就可以在新生 代垃圾回收时处理掉

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2K8TUFDt-1650980581549)(C:\Users\86133\Pictures\JVM\垃圾回收\回收巨型对象.png)]

JDK 9 并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成, 否则退化为 Full GC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空挡空间

垃圾回收调优

查看虚拟机参数命令:

java -XX:+PrintFlagsFinal -version | findstr “GC”

调优领域
  • 内存
  • 锁竞争
  • cpu 占用
  • io
  • gc
确定目标
  • 低延迟还是高吞吐量, 选择合适的回收器
  • CMS, G1, ZGC
  • ParallelGC
  • Zing(低延迟JVM, 宣称0停顿)
最快的 GC

首先排除减少因为自身编写的代码而引发的内存问题

查看 Full GC 前后的内存占用, 考虑以下几个问题

  • 数据是不是太多?
    • resultSet = statement.executeQuery(“select * from 大表 limit n”)
  • 数据表示是否太臃肿
    • 对象图
    • 对象大小 16 Integer 24 int 4
  • 是否存在内存泄漏
    • static Map map …
    • 第三方缓存实现
新生代调优

新生代的特点

  • 所有的 new 操作的内存分配非常廉价
    • TLAB thread-local allocation buffer
  • 死亡对象的回收代价为零
  • 大部分对象用过即死
  • Minor GC 的时间远低于 Full GC

新生代内存越大越好吗?

-Xmn

Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.

设置新生代(托儿所)堆的初始大小和最大值(字节)。GC在该区域的执行频率高于其他区域。如果新生代的内存太小,则会执行大量Minor GC。如果内存太大,则只执行Full GC,这可能需要很长时间才能完成。Oracle建议将新生代的堆内存大小保持在总堆大小的25%以上,50%以下。

  • 新生代能容纳所有【并发量 * (请求-响应)】的数据

  • 幸存区大到能保留【当前活跃对象+需要晋升对象】

  • 晋升阈值配置得当,让长时间存活对象尽快晋升

    -XX:MaxTenuringThreshold=threshold

    -XX:+PrintTenuringDistribution

    Desired survivor size 48286924 bytes, new threshold 10 (max 10)
    - age 1: 28992024 bytes, 28992024 total
    - age 2: 1366864 bytes, 30358888 total
    - age 3: 1425912 bytes, 31784800 total
    ...
    
老年代调优

以CMS为例:

  • CMS 的老年代内存越大越好

  • 先尝试不做调优, 如果没有 Full GC, 那么已经OK了, 否则先尝试调优新生代

  • 观察发生 Full GC 时老年代内存占用, 将老年代内存预设调大1/4 ~ 1/3

    -XX:CMSInitiatingOccupancyFraction=percent

我的个人主页: www.ayu.link
本文连接: ┏ (゜ω゜)=☞

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值