Skip to content

垃圾回收算法

垃圾回收概述

在JVM运行期间,会对内存中不再被使用的对象进行分配和管理。若不及时对内存中的垃圾进行清理,会导致被保留的空间无法被其他对象使用,从而导致内存溢出

内存溢出:系统无法分配给程序所需要的指定大小内存

内存泄漏:当对象不再使用或无法继续使用时,因为强引用的存在导致本该会回收的内存无法被回收,常见于:Map用对象作为key不重写hashcode & equals & ThreadLocal内存泄漏

对象是否存活

JVM垃圾回收器会对对象中不再使用(死去)的对象进行回收,那么垃圾回收器是如何进行判断的呢。

引用计数法

对于一个对象A,只要有一个对象引用了A,那么A的计数器增加1,当引用失效的时候就减1。该算法会产生对象之间循环引用问题,会导致内存泄漏

可达性算法

通过一系列称为"GC Roots"的根对象作为起点,根据引用关系向下搜索,搜索过程走过的路称为"引用链"。如果某个对象到"GC Roots"没有任何引用链相连,就说明该对象需要被回收。

图中绿色为可达对象,灰色为不可达对象

GC Roots包括但不限于以下:

  1. 栈帧中引用的对象(局部变量、临时变量等)
  2. 类中的引用型静态变量
  3. 字符串常量池中的引用
  4. Synchronized锁持有的对象。

并发的可达性

我们知道大部分的收集器都是使用可达性算法判断标记对象是否要被回收,其中又被分为中断用户线程与用户线程并发执行两种。

中断用户线程进行标记时,对象图中的对象或引用不会被修改,但堆中存储的对象越多,带来的STW时间也会越长。因此为了减少STW时长,标记与用户线程同时运行能有效减少STW时长,但会带来并发可达性问题:

  1. 被标记完毕的对象又新引用了未被收集器访问的对象
  2. 正在被标记的对象直接或间接删除了未被收集器访问的对象的引用。

基于上述两个问题产生了两种解决方案:

  1. 增量更新

基于问题1,当被标记完毕对象又引用了未被收集器访问的对象时,将这些被标记完毕对象记录下来,等并发标记阶段结束后,以这些被标记完毕对象为根再次进行扫描。CMS收集器采用此策略实现并发标记。

  1. 原始快照

基于问题2,当正在被标记的对象直接或间接删除了未被收集器访问的对象的引用时,将这些正在被标记的对象记录下来,等并发标记结束后,以这些正在被标记的对象为根重新扫描。G1收集器采用此策略实现并发标记。


Java中的引用

传统的引用概念:若reference类型的数据中存储的数值是另一块内存的起始地址,就说明该reference数据是某个内存、某个对象的引用。

JDK1.2开始,Java对引用的概念进行补充,将引用分为了:强引用、软引用、弱引用和虚引用四种。

java
public abstract class Reference<T> {
    // 引用本身
	private T referent;  
    // 存储reference本身的链表队列
    volatile ReferenceQueue<? super T> queue;
}

当垃圾回收器准备回收一个对象时,发现它还有软、弱、虚引用,就会在回收对象之前,将该引用加入到与之关联的引用队列ReferenceQueue中去,这样就可以实现在引用对象回收前的相关操作。

  • 强引用

即最传统引用的体现,比如Object obj = new Object(),只要强引用关系存在,那么垃圾回收器永远不会回收掉被引用的对象。

  • 软引用

用于描述还有用、但非必须的对象,只被软引用关联(指向)的对象,在OOM之前,会将这些对象进行二次回收,如果回收后仍没有足够内存,才会抛出OOM。Java中用SoftReference实现。

  • 弱引用

相比软引用,只被弱引用关联(指向)的对象只能生存到下一次垃圾收集,只要垃圾回收器工作,弱引用就会被回收。Java中用WeakReference实现。ThreadLocal.ThreadLocalMap<k,v>中key就继承了弱引用

java
public class Weak {
    private static WeakReference<String> weakReference;

    public static void main(String[] args) {
        test();
        System.out.println(weakReference.get());// hello
        System.gc(); // test作用域结束,gc会清理weakReference
        System.out.println(weakReference.get());// null
    }

    static void test() {
        // str 作为test方法的本地变量
        String str = new String("hello");
        weakReference = new WeakReference<>(str);
        System.gc();// 不会被清理
        System.out.println(weakReference.get());// hello
    }
}

和软引用一样,弱引用也适合保存可有可无的数据,当系统内存不足的时候会被回收,内存充足的时候,缓存数据存在相当长的时间,达到让系统加速的作用。

  • 虚引用

引用中最弱的一种,一个对象是否有虚引用的存在,对其生存时间不会产生影响,并且无法通过虚引用获取对象实例。唯一的作用就是为了在该对象被回收时收到通知

java
public class Phantom {
    // 实现 包含虚引用的对象在回收时接受通知
	public static void main(String[] args) {
        String hello = new String("hello");
        // 创建引用Queue
        ReferenceQueue<String> queue = new ReferenceQueue<>();
        PhantomReference<String> reference = new PhantomReference<>(hello, queue);
        new Thread(() -> {
            while (true) {
                Reference<? extends String> poll = queue.poll();
                if (poll != null) {
                    try {
                        // 此时说明hello对象被回收了
                        Field referent = Reference.class
                            .getDeclaredField("referent");
                        referent.setAccessible(true);
                        String str = (String) referent.get(poll);
                        System.out.println("GC Will Collect " + str);
                        break;
                    } catch (IllegalAccessException | NoSuchFieldException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        // 去除hello的引用
        hello = null;
        // 调用垃圾回收对象
        System.gc();
    }
}

对于虚引用,它的get()方法只会返回null,因为虚引用是不可达的

java
public class PhantomReference<T> extends Reference<T> {
 public T get() { return null; }
}

对象的回收

我们知道HotSpot采用的是可达性算法判断对象是否存活,那么不再存活的对象垃圾回收器是如何回收的呢?

  1. 垃圾回收器对对象进行回收时,先通过可达性算法判断该对象是否可达。
  2. 如果对象不可达并且复写了finalize方法且该对象的finalize方法之前没被调用过
  3. 垃圾回收器会将对象放置到ReferenceQueue<Finalizer>队列中,稍后由JVM启动一个低优先级的Finalizer线程去执行Queue中对象的finalize方法
  4. 但JVM不一定会等待finalize执行结束,因为如果finalize方法卡顿,会导致队列中后续的对象处于等待,甚至导致整个内存回收系统的崩溃
  5. 若该对象的finalize方法不能将对象与引用链建立连接,该对象会被垃圾回收器清理。

java
public class ReachabilityAnalysis {
    
    // 创建GC Roots
    private static ReachabilityAnalysis REACHABILITY_ANALYSIS = null;

    private void isAlive() {
        System.out.println("i'm still alive ...");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("execute finalize method ...");
        // 试图和引用链建立联系
        REACHABILITY_ANALYSIS = this;
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建对象
        REACHABILITY_ANALYSIS = new ReachabilityAnalysis();
        // 去除引用链的联系, 便于测试
        REACHABILITY_ANALYSIS = null;
        // 调用gc时 对象第一次尝试自救
        System.gc();
        // 因为finalizer线程的低优先级, 需要休眠一会。
        // JVM会先判断是否有必要执行finalizer方法, 并执行相应的finalize()方法
        Thread.sleep(1_000);

        if (null != REACHABILITY_ANALYSIS) {
            REACHABILITY_ANALYSIS.isAlive();
        } else {
            System.out.println("i'm dead ...");
        }

        // 第二次自救 用于判断是否会执行finalize方法两次
        REACHABILITY_ANALYSIS = null;
        System.gc();
        Thread.sleep(1_000);
        if (null != REACHABILITY_ANALYSIS) {
            REACHABILITY_ANALYSIS.isAlive();
        } else {
            System.out.println("i'm dead ...");
        }
        // 结论: 任何对象的finalize()方法只会被系统调用一次
    }
}

对象finalize方法只会被JVM调用一次,只要执行finalize方法重新与引用链建立联系,就不会被清理。不建议使用finalize进行释放资源,因为可能发生引用外泄,无意中复活对象。并且finalize调用时间不确定,相比之下更推荐finally释放资源


垃圾回收算法

标记清除法(Mark-Sweep)

包含标记阶段清除阶段。标记阶段通过可达性算法标记分析可达对象,清除阶段会清除所有未被标记的对象。 此方法会产生空间碎片。并且回收后的空间是不连续的,会导致工作效率低于连续空间。该算法更关注垃圾回收器的耗时操作。

复制算法(Copying)

内存空间分为两份,在进行垃圾回收时,将正在使用的那一份内存中的活对象复制到另一份内存中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。 相对于标记清除算法复制算法不会产生空间碎片,但会导致使用的内存只有一半

新生代串行垃圾回收器使用了该算法,它将新生代分为eden、from(s0)、to(s1)三个区域,GC在回收对象时,会先将eden & s0区域的对象复制到s1(大对象、老年对象、s1区域满时对象会直接进入老年代),然后清空eden & s0区域,再将s0 & s1互换,保证s1永远为空

标记压缩算法(Mark-Compact)

在标记阶段,使用可达性算法对所有可达对象进行标记。在清除阶段,将所有的存活对象压缩到内存的一端,然后清除边界外的所有空间。相比之前的算法,避免了内存碎片的产生且不需要内存折半,但是移动大对象会给系统带来较长时间的STW。该算法更关注垃圾回收器的吞吐量操作。

STW:垃圾回收器工作时,Java程序需要暂停工作,等待垃圾回收完成,这种现象叫做Stop The World

分代算法(Generational Collecting)

基于两个分代假说之上:

弱分代假说:绝大多数的对象都是朝生夕灭的。

强分代假说:熬过多次垃圾回收的对象就越难以消亡。

奠定了垃圾收集器的一致设计原则:收集器应将Java堆分成不同的区域,然后将回收对象依据其年龄(对象熬过垃圾回收的次数)分配到不同的区域中存储

但因为对象会存在跨代引用,即新生代对象完全可能被老年代对象引用,因此除了必要的可达性分析外,还需要遍历老年代对象来保证所有对象可达性分析结果的准确性。基于此理论提出了跨代引用假说:

跨代引用相对于同代引用来说仅占极少部分。

并基于此假说采用了如下设计:在新生代上建立一个全局数据结构记忆集(Remembered Set),该结构将老年代划成若干小块,标识出老年代哪一块内存会存在跨代引用,当发生新生代垃圾回收时,会对记忆集中的记录加入GC Roots进行扫描,相比扫描整个老年代来说大大的减少了运行时开销。

分区算法(Region)

分区算法将整个堆空间分成连续的不同小区间,每个小区间都独立使用,独立回收。控制一次回收小区间的数量,能够有效减少GC产生的停顿。

分区回收算法带来的跨代引用问题:

因为对象会存在新生代、老年代间的跨代引用问题,垃圾收集器建立了名为记忆集(Remember Set)的数据结构,用于记录非收集区域指向收集区域的指针集合的抽象数据结构。继而避免对整个老年代进行扫描

卡表(Card Table)是最常见的实现记忆集结构的方式。可以是字节数组哈希表,存储的是跨代引用的对象的内存地址,这样只需要筛选出跨代引用的对象,将其加入GC Roots中一起扫描即可。


垃圾回收期器

JVM中经典的垃圾回收器

下图是来自oracle官方博客中介绍垃圾回收器之间的关系图。

黄色代表新生代,灰色代表老年代,两个垃圾回收器之间相连表示这两个垃圾回收器组合使用

Serial & CMSParNew & Serial Old两组 在JDK8中已过期,JDK9中已移除。

我们用并行并发来形容不同的收集器:

并行:描述的是多条垃圾回收器线程之间的关系,默认此时的用户线程处于等待状态。

并发:描述垃圾回收器线程与用户线程间的关系,说明同一时间垃圾回收器线程与用户线程都在运行


新生代收集器

以下三种收集器都采用的是标记-复制算法来实现收集器的回收逻辑。

Serial收集器

使用单线程工作的收集器,除了只会用一个处理器或一个收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程(STW),直到收集结束。是客户端模式下默认新生代收集器。

客户端/服务端区别:client比server模式启动速度更快,当server比client模式运行速度更快

相比其他垃圾收集器,Serial收集器时所有垃圾回收器里面额外内存消耗最小的,但STW耗时是最长的;对于单核处理器或处理器核心较少环境来说,由于没有线程交互的开销,Serial收集器可以获得最高的单线程收集效率

bash
-XX:+UseSerialGC 新生代 & 老年代都使用串行收集器

ParNew收集器

ParNew收集器本质上是Serial收集器多线程并行版本。除了同时使用多条线程进行垃圾收集外,其余的和Serial收集器一致。

这里的并行指的是:同一个时间有多个这样的收集线程在协调工作,用户线程此时处于等待状态。

除了Serial收集器外,只有ParNew收集器能与CMS收集器配合工作。

bash
# 新生代ParNew & 老年代CMS 是开启CMS下新生代默认收集器
-XX:+UseConcMarkSweepGC
# 新生代ParNew & 老年代SerialOld(JDK8后已过期)
-XX:+UseParNewGC

因为线程交互的开销,在单核处理器下性能低于Serial,但是多核心ParNew收集器还是很高效的。

bash
# 垃圾收集的线程数为8
-XX:ParallelGCThreads=8

不设置此参数时,当Cpu Cores < 8时,Threads=Cpu Cores,否则 Threads=3+(5*cores)/8)

Parallel Scavenge收集器

相比ParNew收集器目标是减少用户线程的停顿时间Paraller收集器关注则是可控制的吞吐量

=/(+

假设JVM执行完成某个请求共需要100分钟,其中垃圾收集花费1分钟,那么吞吐量就是99%

低停顿时间适合用户交互或保证服务响应的程序。高吞吐量适合最高效率利用处理器资源,尽快完成程序的运算任务

停顿时间缩短是以牺牲吞吐量和新生代空间为代价的。如果我们将新生代设置的较小,虽然会减少每次回收的时间,但是会导致垃圾回收更加频繁,虽然停顿时间在减少,但是吞吐量在下降。

bash
# 允许设置一个大于0的毫秒数,收集器尽量保证内存回收时间不超过该值
-XX:MaxGCPauseMillis
# 允许设置一个大于0小于100的整数n
# 系统将花费不超过 1/(1+n)的时间进行回收 假设n=99,那么不超过1%时间进行回收。
-XX:GCTimeRatio
# 自适应GC策略,自动调整新生代大小,老年代晋年龄等 区别于ParNew是Paraller独有
-XX:+UseAdaptiveSizePolicy

垃圾回收器

老年代收集器

Serial Old收集器

Serial收集器的老年代版本,基于标记-整理单线程收集器,用于客户端模式下的HotSpot虚拟机使用。

服务端模式下,有两个用途:JDK5及之前版本中与Parallel Scavenge配合使用;作为CMS收集器发生失败时的备用收集器。

Parallel Old收集器

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

Parallel Old配合Parallel Scavenge的组合,用于注重吞吐量和处理器资源较为稀缺的情况。

bash
# Parallel Scavenge + Parallel Old,JDK8默认组合
-XX:+UseParallelGC

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法。

收集流程
  • 初始标记

此阶段仅标记GC Roots能直接关联到的对象。需要停顿用户线程(STW)。

  • 并发标记(并发)

基于初始标记阶段标记的从GC Roots可直接关联的对象开始遍历整个对象图的过程。不需要停顿用户线程,与垃圾回收线程一起工作。

  • 重新标记(并行)

该阶段是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那部分对象的标记记录。CMS是使用增量更新来解决并发标记产生的问题。

  • 并发清除(并发)

清理删除掉标记阶段的已死亡对象,此阶段不需要移动存活对象。

CMS收集器缺点
  • 对处理器资源敏感

因为CMS的并发阶段会占用一部分线程会导致应用程序变慢,降低吞吐量。默认回收线程数是(Cpu Cores + 3 ) / 4,若Cpu Cores越小,那么对程序运行的影响较大。

  • 无法处理浮动垃圾

在CMS并发标记和并发清理阶段,用户线程还在继续执行,就会有新的垃圾对象不断产生,但这些对象出现在初始标记阶段后,只能在下次垃圾回收中再处理这部分垃圾。

因为CMS收集器并发标记和并发清理的特性,必须预留一些空间提供给用户线程使用,不能等老年代满再工作。

bash
# 当老年代使用了68%后CMS开始工作
# JDK5默认68%,JDK6默认92%
-XX:CMSInitiatingOccupancyFraction=68

如果预留的内存不够用户线程分配新对象,会启用Serial Old进行Major GC。会带来较长的停顿时间。

  • 产生大量空间碎片

因为CMS基于比较-清除算法,易产生大量的空间碎片,在无法给大对象分配内存时导致一次Full Gc

bash
# 默认开启,当CMS进行Full GC时开启内存碎片合并整理的过程 JDK9废弃
-XX:+UseCMSCompactAtFullCollection
# CMS在执行若干次不整理空间的Full GC后,下一次进行碎片整理 JDK9废弃
-XX:CMSFullGCBeforeCompaction

Full GC:对整个Java堆进行回收,包含新生代和老年代

Minor GC:对新生代进行回收。

Major GC:对老年代进行回收。

G1收集器

概念

G1(Garbage First)收集器开创了面向局部收集的设计思路基于Region的内存布局形式。目的是为了实现支持停顿时间模型的收集器。基于标记-整理算法。

停顿时间模型:支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。

相比于其他收集器要么面向新生代,要么面向老年代,而G1面向堆内存任何部分来组成回收集(Collection Set)进行回收。衡量标准由属于哪个分代变为哪块内存垃圾最多,回收收益最大。

相比于之前的固定大小和数量的区域划分的收集器,G1将堆内存分为多个大小相等的独立区域(Region,默认分成2048份),每个Region可以根据需要扮演Eden、Survivor或老年代空间

Region中还存在一类特殊的Humongous区域,用于存储大对象,G1认为只要大小超过一个Region容量一半的对象即为大对象,对于超过Region大小的对象,将会被存放在N个连续的Humongous区域中。

bash
# 设置Region的大小(单位: B),在[1MB,32MB]必须为2的幂次方
# 设置Region的大小=2097152B=2MB,不设置默认是1MB
-XX:G1HeapRegionSize=2097152

G1仍然保留新生代、老年代的概念,当它们不再是固定的区域了,改为一系列区域(不需要连续)的动态集合。G1在后台维护一个优先级列表,每次根据用户通过-XX:MaxGCPauseMillis指定的停顿时长(默认200ms),优先回收价值收益最大的Region,以达到最大的收集效率。

收集流程

  • 初始标记

和CMS类似,标记GC Roots直接关联的对象,此阶段会产生STW

  • 并发标记(并发)

基于初始标记,从GC Roots开始对堆中对象进行可达性分析,并递归扫描整个对象图,找出可回收对象,此过程可与用户程序并发执行。

  • 最终标记(并行)

对用户线程做另一个短暂的暂停,通过原始快照SATB处理并发标记导致的并发可达性问题(上章分析过)。

  • 筛选回收(并行)

负责更新Region的统计数据,并按照回收价值和成本进行排序,根据用户期望的停顿时间来指定回收计划。可见多个Region合并成回收集(Collection Set),将回收的Region中的对象移到空的Region,再清理旧的Region,设计到对象的移动(体现标记-整理算法),此阶段用户线程暂停多条收集器线程并行完成。

G1vsCMS

  • 优势
    • 指定最大停顿时间
    • 不会产生内存空间碎片
  • 劣势
    • 并发执行带来的较高的内存占用和负载
    • 每个Region都持有一份卡表导致堆内存的消耗。

对象在堆中的分配

前面我们了解到,大部分的对象都是在堆中进行内存分配,但堆中又存在多个逻辑区域(新生代、老年代),所以这章我们就要讨论下,对象在堆中的进行内存分配的基本原则。

TLAB

在讨论对象分配前,我们需要对之前引入的TLAB的概念进一步解析。TLAB(本地线程缓冲),其存在的目的是为了加速对象的分配,即每个线程都拥有自己的专属区域进行对象分配,来避免多线程冲突,默认是启动的。

bash
# 开/关TLAB
-XX:+/-UseTLAB
# 设置TLAB大小
-XX:TLABSize
# 查看TLAB信息
-XX:+PrintTLAB
# 对象占TLAB空间的比例,大于此比例堆中分配,小于就废弃当前
## TLAB区域,并新建一个TLAB存放,默认64
-XX:TLABRefillWasteFraction=64
# 默认情况TLAB和refill_waste是动态的,关闭TLAB动态调整
-XX:-ResizeTLAB

我们假设TLAB大小为100KB,第一次分配给对象80KB,此时还剩20KB,如果第二次有30KB大小的对象需要分配,此时有两种选择:

  1. 废弃所剩的20KB区域,新建一个TLAB存放30KB的对象。
  2. 将30KB对象分配在堆上,保留所剩的20KB区域,等到下次有小于20KB对象分配时再使用该区域。

-XX:TLABRefillWasteFraction=64,即允许TLAB空间浪费的比例,当对象/TLAB的比例大于64,对象在堆中分配,小于64则会开辟新TLAB存放。

一般在eden中分配

大部分情况下,对象在Eden区中进行分配,如果Eden区空间不够,JVM会发起一次Minor GC

大对象进入老年代

大对象:需要大量连续内存空间的Java对象或新生代已无足够空间分配的对象直接进入老年代。

bash
# 将大于此大小的对象直接分配到老年代
-XX:PretenureSizeThreshold=5242880(5mb)

只适用于Serial、Serial Old、ParNew三种收集器。

长期存活对象进入老年代

长期存活的对象将进入老年代。对象通常在eden区诞生,如果经历了一次Minor Gc后仍然存活,并能够被s0容纳,该对象会被移动到s0区并将其对象头中的对象年龄 + 1。当年龄达到阈值,就会进入老年代。

bash
# 对象晋升到老年代的年龄阈值
-XX:MaxTenuringThreshold=15

动态对象年龄判断

JVM不是永远要求对象年龄达到-XX:MaxTenuringThreshold指定的值才能晋升老年代:

如果s0中相同年龄的对象大小总和大于s0区域的一半(-XX:TargetSurvivorRatio决定,默认50),那么大于等于该年龄的对象就会进入老年代。

总结

对象的内存分配流程需要经历栈上分配 -> TLAB分配 -> 是否进入老年代 -> 最终eden分配


5. 虚拟机参数概述

基本参数

参数作用
-XX:+PrintGCDetails打印详细的GC日志
-XX:+PrintGCTimeStampsGC开头的时间为虚拟机启动时间的偏移量
-XX:+PrintGCApplicationStoppedTime打印引用程序由于GC而产生停顿的时间
-Xloggc:D://log.txt输出GC日志到D盘下log.txt文件中
-XX:+PrintVMOptions打印显示传递的参数
-XX:+PrintCommandLineFlags打印传递给虚拟机的显式和隐式参数
-XX:+PrintFlagsFinal打印全部参数(包括虚拟机自身的参数)
-Xss1m指定栈大小为1m
-Xms10m初始堆空间大小
-Xmx20m最大堆空间大小
-Xmn2m新生代大小
-XX:SurvivorRatio新生代中eden/s0/s1比例,默认8:1:1
-XX:NewRatio老年代/新生代的比例,默认2:1
-XX:NewSize新生代初始大小
-XX:MaxNewSize新生代大小最大值
-XX:+HeapDumpOnOutOfMemoryError堆OOM时导出堆的信息
-XX:HeapDumpPath=D://log.dump将OOM信息导入到D盘下log.dump文件中
-XX:MetaspaceSize=1m设置元数据区初始大小为1m
-XX:MaxMetaspaceSize=2m设置元数据区大小最大为2m
-XX:MaxDirectMemorySize=2m本机直接内存(堆外内存)最大2m,默认等于-Xmx
-XX:+UseTLAB开启TLAB,默认开启
-XX:+PrintTLAB打印TLAB信息
-XX:TLABSize=1024设置TLAB大小为1kb
-XX:TLABRefillWasteFraction=64允许TLAB空间浪费的比例
-XX:-ResizeTLAB禁止TLAB自动调整大小和浪费比例
-XX:PretenureSizeThreshold=5242880大于5m对象直接进入老年代,只对Serial、ParNew有用
-XX:MaxTenuringThreshold=15晋升到老年代的年龄大小
-XX:TargetSurvivorRatio=50用于动态对象年龄判断的s0的使用率参数,默认50

收集器选择参数

参数新生代老年代
-XX:+UseSerialGCSerialSerial Old
-XX:+UseParallelGCParallel ScavengeParallel Old
-XX:+UseParNewGCParNewSerial Old
-XX:+UseConcMarkSweepGCParNewCMS
-XX:UseG1GCG1G1

收集相关参数

收集器相关参数注释
ParNew
Parallel
CMS
G1
Parallel Old
-XX:ParallelGCThreads=n指定并行回收线程数
n=cores<8?cores:3+((5*cores))/8)
Parallel-XX:MaxGCPauseMillis=n
-XX:GCTimeRatio=n
-XX:+UseAdaptiveSizePolicy
最大回收停顿时长
不超过1/1+n时间进行回收
自适应GC策略
CMS-XX:CMSInitiatingOccupancyFraction=n
-XX:CMSFullGCBeforeCompaction=n
-XX:+UseCMSCompactAtFullCollection
老年代容量到n时CMS开始工作,默认92
CMSn次FullGC后开启碎片整理,默认0
CMS进行FullGC时开启碎片整理
G1-XX:G1HeapRegionSize
-XX:MaxGCPauseMillis
-XX:InitiatingHeapOccupancyPercent
指定Region大小,默认1MB,最大32MB
垃圾收集时停顿时长,默认200ms
堆使用率达到n后开启并发标记,默认45