JVM(二)垃圾回收GC

一、如何判断对象可以回收

1.1 引用计数法

当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。
这个引用计数法听起来不错,但是有一个弊端2_垃圾回收,如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。

1.2 可达性分析算法

利用可达性分析算法判定对象是否可以回收

MAT工具--可视化

Eclipse Memory Analyzer是一个快速且功能丰富的Java堆分析器,可帮助您查找内存泄漏并减少内存消耗。使用Memory Analyzer分析具有数亿个对象的高效堆转储,快速计算对象的保留大小,查看谁阻止垃圾收集器收集对象,运行报告以自动提取泄漏嫌疑者。

下载链接:https://www.eclipse.org/mat/downloads.php

  • JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看能否沿着GC Root为起点的引用链找到该对象,如果找不到,则表示可以回收

此算法的核心思想:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。

注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

img

可以作为 GC Root 的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象(局部变量)。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的Native方法)引用的对象
/**
 * 演示GC Roots
 */
public class Demo2_2 {

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

        list1 = null;
        System.out.println(2);
        System.in.read();
        System.out.println("end...");
    }
}

对于以上代码,可以使用如下命令将堆内存信息转储成一个文件,然后使用
Eclipse Memory Analyzer 工具进行分析。
第一步:
使用 jps 命令,查看程序的进程

img

第二步:
img

使用 jmap -dump:format=b,live,file=1.bin 16104 命令转储文件

  • dump:转储文件
  • format=b:二进制文件
  • file:文件名
  • 16104:进程的id

第三步:打开 Eclipse Memory Analyzer 对 1.bin 文件进行分析。
img

分析的 gc root,找到了 ArrayList 对象,然后将 list 置为null,再次转储,那么 list 对象就会被回收。

1.3 四种引用

在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分区强引用、软引用、弱引用和虚引用四种,这四种引用强度依次逐渐减弱。

img

强引用(Strongly Reference)

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

软引用(Soft Reference)

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

弱引用(Weak Reference)

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

虚引用(Phantom Reference)

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

终结器引用(Final Reference)

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

软引用演示

/**
 * 演示 软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
public class Code_08_SoftReferenceTest {

    public static int _4MB = 4 * 1024 * 1024;

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

    // 设置 -Xmx20m , 演示堆内存不足,
    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());
        }
    }
}

method1 方法解析:
首先会设置一个堆内存的大小为 20m,然后运行 mehtod1 方法,会抛异常,堆内存不足,因为 mehtod1 中的 list 都是强引用。

img

method2 方法解析:
在 list 集合中存放了 软引用对象,当内存不足时,会触发 full gc,将软引用的对象回收。细节如图:

img

上面的代码中,当软引用引用的对象被回收了,但是软引用还存在,所以,一般软引用需要搭配一个引用队列一起使用。
修改 method2 如下:

// 演示 软引用 搭配引用队列
    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());
        }
    }

img

弱引用演示

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示弱引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Code_09_WeakReferenceTest {

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

    public static int _4MB = 4 * 1024 *1024;

    // 演示 弱引用
    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() + ",");
        }
    }

}

二、垃圾回收算法

2.1 标记-清除

定义:Mark Sweep

最早出现也是最基础的垃圾回收算法是“标记-清除”算法。

首先标记所有需要回收的对象,在标记完成后,统一回收所有被标记的对象。

2_垃圾回收

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

2.2 标记-整理

定义:Mark Compact

标记的过程同“标记-清除”算法一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界意外的内存。

2_垃圾回收2

  • 速度慢
  • 没有内存碎

2.3 复制

Copy

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

2_垃圾回收

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

三、分代垃圾回收

2_垃圾回收3

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

虚拟机中的共划分为三个代:

  • 新生代(Young Generation)
  • 老年点(Old Generation)
  • 持久代(Permanent Generation)

其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

3.1 年轻代

所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

3.2 年老代

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

3.3 持久代

  用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

3.4 分代垃圾回收过程

  • 新创建的对象首先分配在 eden 区

  • 新生代空间不足时,触发Minor GC ,eden 区 和 from 区存活的对象使用 - copy 复制算法到 to 中,存活的对象年龄加1,然后交换 from to 的位置,空出大量连续内存区,放对象,反复。

  • minor gc 会引发 stop the world(SWT),暂停其他线程(垃圾回收时会涉及大量关于对象的复制,其他线程会产生不必要的麻烦,如线程不到对象了,因为地址变化了),等垃圾回收结束后,恢复用户线程运行 当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit)----多次回收还存在,说明价值较高,放入老年代,不用频繁回收。

  • 当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full fc ,STW停止的时间更长!

3.5 相关 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
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

3.6 GC分析

public class Code_10_GCTest {
 
    private static final int _512KB = 512 * 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) {
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
    }
 
}

通过上面的代码,给 list 分配内存,来观察 新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况,使用前需要设置 jvm 参数。

四、垃圾回收器

4.1 串行

  • 单线程
  • 堆内存较小,适合个人电脑(CPU个数少)
-XX:+UseSerialGC= Serial + SerialOld
				新生代 复制	老年代 标记-整理法

2_垃圾回收4

安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。

阻塞:因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

Serial 收集器:

  • 定义:Serial收集器是最基本的、发展历史最悠久的收集器
  • 特点:单线程收集器。采用复制算法。工作在新生代

Serial Old收集器:

  • 定义:Serial Old是Serial收集器的老年代版本
  • 特点:单线程收集器。采用标记-整理算法。工作在老年代

4.2 吞吐量优先

  • 多线程
  • 堆内存较大,多核CPU
  • 让单位时间内,STW的时间最短 0.2 0.2 = 0.4
  • JDK1.8默认使用的垃圾回收器
-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC
-XX:+UseAdaptiveSizePolicy //新生代采用自适应大小调解策略
-XX:GCTimeRatio=ratio // 调整垃圾回收和总时间的占比 1/1+ratio 默认ratio=99
-XX:MaxGCPauseMillis=ms //每次垃圾回收最大暂停毫秒数 默认200ms
-XX:ParallelGCThreads=n //控制ParallelGC运行时的线程数

2_垃圾回收5

Stop一the一World,简称STW,指的是Gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。

Parallel 收集器

  • 定义:与吞吐量关系密切,故也称为吞吐量优先收集器
  • 特点:并行的,工作于新生代,采用复制算法

Parallel Old 收集器

  • 定义:是Parallel 收集器的老年代版本
  • 特点:并行的,工作与老年代,采用标记-整理算法

4.3 响应时间优先

  • 多线程
  • 堆内存较大,多核CPU
  • 尽可能让 STW 的单次时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

2_垃圾回收6

  • 初始标记:标记GC Roots能直接到的对象。速度很快,存在Stop The World
  • 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行
  • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。存在Stop The World
  • 并发清理:对标记的对象进行清除回收

CMS收集器:

  • 定义:Concurrent Mark Sweep(并发,标记,清除)
  • 特点:基于标记-清除算法的垃圾回收器。是并发的。工作在老年代。

ParNew 收集器:

  • 定义:ParNew收集器其实就是Serial收集器的多线程版本
  • 特点:工作在新生代,基于复制算法的垃圾回收器。

4.4 G1

定义:Garbage First

  • 2004 论文发布
  • 2009 JDK6u14体验
  • 2012 JDK7u4官方支持
  • 2017 JDK9 默认,而且替代了CMS 收集器

适用场景:

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

相关参数:JDK8 并不是默认开启的,所需要参数开启

-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time

4.4.1 G1垃圾回收阶段

0bd90ad4ad0bf7d94b25e4a3e610418f

新生代的回收之后,可以在进行新生代回收时,同时并发标记,然后再进行混合垃圾回收,即对新生代、老年代都进行一次较大的垃圾回收。

4.4.2 Young Collection

对新生代垃圾收集

  • 会STW

E:eden,S:幸存区,O:老年代

f401dbcfe20d00a8c048bf5797334da1

4.4.3 Young Collection + Concurrent Mark

如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。

  • 在 Young GC 时会进行 GC Root 的初始化标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的 JVM 参数决定 -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

832e857ef37fdebd16d67a35e000a282

4.4.4 Mixed Collection

会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。
会对 E S O 进行全面的回收

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

-XX:MaxGCPauseMills=xxms 用于指定最长的停顿时间,因为指定了停顿时间。会回收价值高(回收后可以释放更多空间)的老年代,Garbage First名称由来

e85f27d5eadb0c3f8cb1eee7c7dbfa13

4.4.5 Full GC

SerialGC

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

ParallelGC

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

CMS

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足
  • 当垃圾回收速度跟不上垃圾生成速度时,会full gc
  • 并发收集失败前是minor gc,并发失败退化为串行垃圾收集器,触发full gc

G1

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足
  • 当垃圾回收速度跟不上垃圾生成速度时,会full gc
  • 并发收集失败前是minor gc,并发失败退化为串行垃圾收集器,触发full gc

4.4.6 Young Collection 跨代引用

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

8e8080f830afa88c1f402d5563ed1f32

  • 卡表 与 Remembered Set
    Remembered Set 存在于E中,用于保存新生代对象对应的脏卡
    脏卡:O 被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
  • 在引用变更时通过 post-write barried + dirty card queue
  • concurrent refinement threads 更新 Remembered Set(异步 不是立刻更新,先放脏卡队列,将来有一个线程完成更新)
  • 好处是在做GCRoot遍历的时候不需要遍历整个老年代,只要遍历脏卡就行。减少搜索范围,提升搜索范围。

7958dd4ee7c725d3e19334dcb7e0074b

4.4.7 Remark

4487d31ed0d0f9de263e26da6ec28668

重新标记阶段
在垃圾回收时,收集器处理对象的过程中

  • 黑色:已被处理,需要保留的,不会被垃圾回收
  • 灰色:正在处理中的
  • 白色:已处理完成,会被垃圾回收

a57db44078d9fb7fa1e1f505b3506597

B会被标记为黑色

因为是并发标记,其他用户线程在工作,可能将C的引用改变

f6085e591d94229c79a809cb1bca32db

C已经被处理过了,本来应该被直接回收,但是显然C被其他用户线程改变引用了。所以需要对对象的引用进行进一步检查。

当对象的引用发生改变时,jvm会对该对象加上写屏障pre-write barrier,会把对象C加入到一个队列satb_mark_queue,并且标记为灰色,表示正在处理。

等到并发标记结束,进行重新标记,会STW,暂停其他线程,然后将线程里的对象取出来检查,发现对象是灰色,还需要进行处理。在处理的时候发现,有强引用引用该对象,那么标记为黑色,即处理结束且不会被回收。

4.4.8 G1的一些优化

JDK 8u20字符串去重

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

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

JDK 8u40并发标记类卸载

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

-XX:+ClassUnloadWithConcurrentMark # 默认启用

JDK 8u60回收巨型对象

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

9a089be8f5b160000de30a70a2e6e9cd

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

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

目的是尽早地开始垃圾回收,避免Full GC的发生。

JDK 9更高效的回收

4.5 垃圾回收调优

  • 掌握GC相关的VM参数,会基本的空间调整
  • 掌握相关工具
  • 调优跟应用、环境有关,没有放之四海皆准的法则
public class Demo2_8 {
    public static void main(String[] args) {
    
    }
}
# 查看虚拟机运行参数
-XX:+PrintFlagsFinal -version | findstr "GC"

d3aeb3f5df0d20bf0a36d9762aee1ca6

调优领域

  • 内存
  • 锁竞争
  • cpu占用
  • io

确定目标

  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • CMS,G1,ZGC(响应时间优先)
  • ParallelGC(高吞吐量)
  • Zing(号称0停顿)

最快的GC是不发生GC

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

查看FullGC前后的内存占用,考虑下面几个问题

  • 数据是不是太多
    resultSet = statement.executeQuery(“select * from 大表”),加载到堆内存应该limit,避免把不必要的数据加载到java内存中
  • 数据表示是否太臃肿
    对象图(用到对象的哪个属性就查哪个)
    对象大小 至少16字节,Integer包装类型24字节,而int 4字节
  • 是否存在内存泄露
    static Map map作为缓存等,静态的,长时间存活的对象,一直添加,会造成OOM
    可以用软引用、弱引用
    可以使用第三方的缓存实现,redis等,不会给java堆内存造成压力

新生代调优

新生代的特点

  • 所有的new操作的内存分配非常廉价
    TLAB thread-local allocation buffer
  • 死亡对象的回收代价是零
    复制算法,复制之后直接释放空间,不整理
  • 大部分对象用过即死
  • MinorGC的时间远远低于FullGC

新生代越大越好吗

  • 小了容易经常触发MinorGC
  • 新生代太大的话,老年代的空间就不足了,当老年代空间不足会触发FullGC,耗费更多时间

0ff8d6a275df2c8d66929886cb39d2e9

  • 新生代最好能容纳所有【并发量 X (请求响应)】
  • 幸存区大到能保留【当前活跃对象+需要晋升对象】
  • 晋升阈值配置要得当,让长时间存活对象尽快晋升
    因为晋升对象如果长时间存在于幸存区,每次垃圾回收进行复制其实都没必要。应该早点把待晋升对象晋升到幸存区。
    -XX:MaxTenuringThreshold=threshold
    -XX:+PrintTenuringDistribution

955686fbf3be6ecfa01c7fb7d5a83057

老年代调优

以CMS为例

  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有FullGC那么说明老年代空间比较富裕,运行状况还不错。及时出现了FullGC,也可以先尝试调优新生代
  • 观察发生FullGC时老年代内存占用,将老年代内存预设调大1/4~1/3
    • -XX:CMSInitiatingOccupancyFraction=percent
    • 待空间达到了老年代的多少进行垃圾回收,预留空间给浮动垃圾

案例

案例1 FullGC和MinorGC频繁

  • 说明空间紧张
  • 可能是新生代空间小,当高峰期时对象的频繁创建,导致频繁发生MinorGC
  • 由于新生代空间紧张,动态调整晋升寿命,导致存活时间较短的对象也会晋升到老年代,导致触发FullGC
  • 应尝试调节新生代的内存

案例2 请求高峰期发生FullGC,单次暂停时间特别长(CMS)

  • 通过查看GC日志,查看CMS哪个阶段耗时长
  • 当高峰期时,对象频繁创建。在CMS的重新标记阶段,就可能耗费大量时间。
  • 可以在重新标记之前,先进行一次垃圾回收
  • -XX:+CMSScavengeBeforeRemark

案例3 老年代充裕情况下,发生FullGC()

  • 1.7之前是永久代作为方法区的实现,可能会发生永久代不足。
  • 永久代不足会触发堆的FullGC
posted @ 2021-09-30 11:11  王陸  阅读(180)  评论(0编辑  收藏  举报