垃圾回收

垃圾回收

文章发布于 2021-01-21 21:18:13,最后更新于 2021-01-25 21:14:25

垃圾收集器

src=http___attach.bbs.miui.com_forum_201205_15_152011zser9o5oa9ee9xx6.jpgrefer=http___attach.bbs.miui.jpg

前言:

上一章学习了关于JVM的一些基础概念,包括JAVA虚拟机的内存结构、对象存活算法等等,接下来就开始学习了解一些关于垃圾收集的算法和一些常见的垃圾收集器。这部分设计大量的理论知识,和一些算法实现的细则,为了方便理解,打算抛开细则,学习思路和发展过程。

f61f4f6e343742a43b82915b97581d18_t

一、垃圾收集算法

从如何判断对象消亡的角度出发,垃圾收集算法可以分为两大类“引用计数式垃圾收集”和”链路追踪式垃圾收集“两大类。由于目前主流的JAVA虚拟机中为涉及,所以我们暂时不了解,主要学习基于追踪式垃圾收集。

1.1、分代收集理论

设计原则:收集器应该将Java堆分出不同的区域,然后将回收对象依据其年龄,分配到不同的区域中存储。

  1. 显而易见如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把他们集中放在一起,每次回收只关注如何保留少量存活,而不是去标记那些大量要回收的对象,这样就能低代价回收到大量空间。
  2. 如果剩下的都是难以消亡的对象,那么把他们集中在一起,虚拟机便可以用较低的频率来回收这个区域,同时兼顾了时间开销、内存空间的有效使用。
  3. 跨代引用假说

跨代引用假说

存在互相引用关系的两个对象,是应该倾向于同时生存同时消亡的。可以在新生代上建立一个全局的 数据结构(记忆集),这个结构把老年代划分成若干个小块,标识出老年代哪一块内存存在跨代引用,以后发生minor Gc时,只有包含了跨代引用的小块内存才会被加到GC Roots中扫描。(相比于扫描老年代全部更划算。

1.2、标记-清除算法

算法思路:

首先标记所有需要回收的对象,标记完成后统一回收所有被标记的对象,或者也可以标记存活对象,统一回收未标记的对象。

缺陷:

  • 执行效率不稳定。标记和清除的效率会随着对象数量的增多而降低。
  • 会导致内存空间碎片化。标记、清除会产生大量不连续的内存碎片,空间碎片太多会导致程序运行过程中需要分配大对象时,找不到足够的连续内存空间而不得不提前触发另一次垃圾收集动作。

1.3、标记-复制算法

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

算法思路:
将内存按照容量划分为大小相等的两块,每次只是要一块。当这块用完后,将活得对象复制到另一块去,然后再把使用过的内存空间一次清理掉。

缺陷:

显而易见,虽然简单,运行高效,但是他的代价就是将可用内存缩小为了原来的一半。

小知识

Appel式回收:

新生代分为较大的Eden区和两块较小的Survivor,每次分配内存只是用Eden和其中一块Survivor。发生垃圾回收的时候,将Eden和其中一块Survivor中存活 的对象一次性复制到另外的Survivor中,然后直接清理Eden和使用过的Survivor。如果Survivor空间不足以容纳一次MinorGC的存活对象,这时候就需要依赖其他空间(老年代)来进行分配担保(类似于银行借款)。

1.4、标记-整理算法

标记复制算法,在对象存活率搞的时候就要进行较多的复制,而且如果不想牺牲50%的空间,就需要有额外空间进行分配担保,以应对被使用的对象内存中所有对象都100%存活的极端情况,所以一般老年代不能用这种算法。

算法思路:

标记过程和标记清除算法一样,但后续不是直接对可回收对象进行清除,而是让所有存活对象都向内存空间的一端移动,然后直接清理掉边界以为的内存。

缺陷:

如果移动存活对象,对于老年代这种有大量对象存活的区域,移动操作是一种极其负担的操作,而这种操作必须暂停应用程序才能进行。

但如果跟标记清除算法一样,不考虑移动和整理存活对象,又会导致空间碎片化,这样就只能考虑复杂的内存分配器和内存访问器来解决。而内存访问是应用程序最为频繁的操作,如果这个环节上增加了负担,那么势必影响应用程序吞吐量。

因此基于以上两点,移动对象和不移动对象都存在弊端,移动则内存回收会更复杂,不移动分配内存会更复杂。

从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至没有,但是从整个系统的吞吐量来看,移动对象会更划算。(因为内存分配和访问相比于垃圾收集频率要高的多这部分耗时增加,整体吞吐量任然是下降的)

因此基于不同的特性也有不同的收集器,关注吞吐量的就是Parallel Scavenge 它是基于标记整理,而关注于延迟的CMS则是标记清除。
e0f9661c9cb957d61aca7304280a8c24_t

二、经典垃圾回收器

收集算法是内存回收的方法论、垃圾收集器就是内存回收的实践者。

image20210119211850792.png

2.1、Serial收集器

标记-复制算法 、串行

image20210119211941628.png

特点:

单线程工作的收集器:单线程不是指只会使用一个处理器或者一个手机线程完成垃圾收集,更重要的是强调他在垃圾回收的时候,必须暂停其他所有工作线程,直到垃圾收集结束。

目前仍是HotSpot虚拟机在客户端模式下默认的新生代收集器。因为它简单高效,额外内存消耗最小。

2.2、ParNew收集器

image20210119211952126.png

标记-复制算法、并行

使用-XX:+UseParNewGC参数可以设置新生代使用这个并行回收器

ParNew收集器实际上就是Serial收集器的多线程版本,除了同时使用多条线程进行垃圾收集,其他的基本都一致。

ParNew是激活CMS后的默认新生代收集器。ParNew是最早推出历史舞台的垃圾回收器。

2.3、Paraller Scavenge收集器

标记-复制算法

使用-XX:+UseParallelGC参数可以设置新生代使用这个并行回收器

是能够并行收集的多线程收集器。CMS等收集器关注的是如何尽可能的缩短垃圾收集时用户线程的等待时间,而Paraller Scavenge收集器的目标时如何达到一个可控制的吞吐量。(吞吐量时运行用户代码时间和处理器总耗时的比)

停顿时间越短,越是和需要频繁和用户交互保证响应速度的程序

高吞吐量则可以高效的利用处理器资源,尽快完成运算任务

参数:-XX:MaxGCPauseMills(最大垃圾收集器停顿时间)-XX:GCTimeRatio(吞吐量大小)

2.4、Serial Old收集器

标记-整理算法

image20210119212004343.png

他是Serial的一个老年版本,也是单线程的。

2.5、Paraller Old收集器

标记-整理算法

是Paraller Scavenge的老年代版本,支持并发收集。

2.6、CMS收集器

标记-清除算法

image20210119212028349.png

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。

它有四个步骤

  1. 初始标记(Stop The World):标记GC Roots能直接关联到的对象,速度快。
  2. 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程。耗时长,但是不需要停顿用户线程
  3. 重新标记(Stop The World):修正并发标记期间,因用户继续运作导致标记变动的那一部分对象的标记记录
  4. 并发清除:清除掉不需要的,因为不需要移动存活对象,所有可以和用户线程并发。

缺陷:

  • CMS收集器对处理资源十分敏感。并发阶段,虽然不会导致用户线程阻塞,但是会因为占用了一部分线程导致应用程序变慢,降低总吞吐量。
  • 无法处理浮动垃圾,就是在重新标记期间产生的垃圾,他不会再当初垃圾收集的时候被回收,只能等下次再清理。
  • 会产生空间碎片

2.7、Garbage First(G1)收集器

思维突破

G1出现之前,所有的垃圾回收器的目标范围要么是整个新生代,要么就是整个老年代,要么就是整个堆。而G1跳出了这个思维,它可以面向堆内存任何部分来组成回收集,进而回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。

基于Region的堆内存布局,是实现这个目标的关键。G1不在坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为大小相等的独立区域,每个区域都可以根据需要扮演不同的角色(Eden、surivor、老年代)。收集器能够堆扮演不同角色的Region采用不同的 策略去处理从而达到更好的效果。

四个步骤:

  1. 初始标记:标记GC Roots能够直接关联到的对象。并且修改TAMS指针,让下一阶段用户并发执行时能正确的在可以的Region中分配新对象。(需要停顿线程,耗时短)
  2. 并发标记:从GC Root开始对堆中对象进行可达性分析,递归找出要回收的对象。耗时长,但是可以并发执行。对象图扫描完成后,还要重新处理SATB记录小的在并发时有变动的对象。
  3. 最终标记:对用户做一个短暂的暂停,用于处理并发阶段结束后遗留下的STAB记录。
  4. 筛选回收:负责更新Region的统计数据,对Region的回收价值和成本排序,根据用户期望和停顿时间来制定回收计划。可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理整个就Region的全部空间。(暂停用户线程)

image20210119212028349.png

2.8、汇总

image20210121214613964.png

三、低延迟垃圾收集器

从Serial到CMS再到G1垃圾收集器经过了持久的发展,但是他们距离“完美”还是很遥远。怎样的垃圾收集器是完美的呢?虽然还未实现,但是我们可以把它描述出来。

衡量垃圾收集器的三项重要指标:

  • 内存占用
  • 吞吐量
  • 延迟

这就像一个不可能三角很难同时达到完美。就类似于ACID。但是从计算机发展的角度来讲,随着硬件的不断发展,越来越能容忍收集器占用多一点内存,硬件的性能增长,吞吐量也会提升,但是对于延迟则不是。

image20210120202733965.png

从上图可以看出,CMS和G1之前的全部收集器,工作都会产生Stop the world,CMS和G1实现了标记阶段的并发,但对于标记后的为解决。而最后两款收集器几乎没有停顿,全部并发执行。

这部分仅作一个知识的展望

7bc38267411b3329a05a6587e228303f_t

四、内存分配和回收策略

开始之前呢先介绍一下GC的打印日志

image20210121222846792.png

4.1、对象优先在Eden分配

大多数情况对象优先在Eden区分配。当Eden区没有足够的空间分配时,虚拟机会发起一次MinorGC。

测试代码:

public class part01 {
    private static final int _1MB = 1024 * 1024;
    /**
     * VM参数
     * -verbose:gc
     * -Xms20M
     * -Xmx20M
     * -Xmn10M
     * -XX:+PrintGCDetails
     * -XX:SurvivorRatio=8
     * -XX:+UseSerialGC
     */
    public static void main(String[] args) {
        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];
    }
}

java -XX:+PrintCommandLineFlags -version 查看默认的垃圾收集器

结果分析:
image20210120214007632.png

分析1:

执行main函数中,分配给allocation4对象时候发生了一次Minor GC(新生代回收),这次GC的结果是新生代内存8164->688,然而堆上总内存的占用几乎没有改变,因为allocation1、allocation2、allocation3都存活,本次回收基本上没有找到可回收的对象。分析如下:

  1. 新生代一共被分配10M,其中Enden:8M,survivor:2M(From:1M,To:1M);
  2. 给allocation4分配内存时,Eden已经被占用6M(allocation1、2、3共6M,所以剩下2M),所以内存已经不够用了(发生GC);
  3. 然而,6M放不进Survivor的From(只有1M),所以只能通过分配担保机制提前转移到老年代。

这次GC结束后,Eden中有4M的allocation4对象(一共8M,被占用50%左右),survivor为空闲,老年代为6M(被allocation1、2、3占用),日志中显示为6144K。

分析2:

至于from区多出来的东西应该是类的一些加载消息。

重点:如果不没有发生MinorGC有可能时没有指定垃圾回收器,java默认的使用Paraller Scavenge收集器,因为不同的垃圾收集器的收集规则不同,所以为了更好的展示分配担保机制,这里指定了Serial收集器

4.2、大对象直接进入老年代

  • 大对象是指需要大量连续内存空间的 Java 对象。
  • 经常出现大对象容易导致内存还有不少空间时,就提前触发 GC 以获取足够的连续空间来安置它们。
  • 由于新生代采用复制算法收集内存,因此为了避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制,大对象将直接进入老年代。

大对象就是需要大量连续内存空间的JAVA对象。典型的就是很长的字符串,或者元素很大的数组。

大对象对于虚拟机来讲就是一个不折不扣的坏消息。因此写代码的时候要注意避免,再分配空间的时候他容易导致内存明明还有不少空间就提前触发垃圾收集,以获取足够的连续的空间来安放他,而当复制大对象的时候也要高额的内存开销。jvm提供了

-XX:PretenureSizeThreshold参数来指定大于设置的对象直接在老年代分配,这样做是为了避免对象来回在eden和两个survivor直接复制。

测试代码:

public class part02 {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        byte[] allocation1;
        allocation1 = new byte[4 * _1MB];
    }
}

结果分析

执行main函数后,我们看到Eden空间几乎没有被使用,而老年代的10MB空间被使用了40%,也就是4MB的allocation对象直接就分配在老年代中,这是因为PretenureSizeThreshold被设置为3MB(就是3145728,这个参数不能像-Xmx之类的参数一样直接写3MB)。因此超过3MB的对象都会直接在老年代进行分配。

设置参数 -XX:PretenureSizeThreshold=3145728

image20210121220520879.png

设置参数 -XX:PretenureSizeThreshold=5145728

image20210121220645292.png

注意:PrctcnurcSizeThreshold参教只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。如果遇到必须使用此参数的场合。可以考虑ParNew加CMS的收集器组合。

4.3、长期存活的对象进入老年代

  • 虚拟机给每个对象定义了一个对象年龄计数器。
  • 对象在 Eden 出生并经过一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将移入 Survivor 中,并且对象年龄设为 1。
  • 对象在 Survivor 中每“熬过”一次 Minor GC,则年龄加 1,当对象年龄增加到一定程度(默认 15 岁),将会晋升到老年代。
//-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=6 -XX:+PrintTenuringDistribution -XX:TargetSurvivorRatio=90

public class Main {
    private static final int _1MB = 1024 * 1024;
    public static void testTenuringThreshold() {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
    }

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

以 -XX:MaxTenuringThreshold=1执行(-XX:+PrintTenuringDistribution 打印年龄分布)

image20210121114905368.png

以 -XX:MaxTenuringThreshold=15   -XX:TargetSurvivorRatio=80 执行:

image20210121114643763.png

以 -XX:MaxTenuringThreshold=15  执行:

image20210121115053575.png

4.4、动态对象年龄判定

  • 为了更好地适应不同程序的内存状况,虚拟机并不要求对象必须达到某个年龄才能晋升老年代。
  • 如果 Survivor 中相同年龄的对象大小总和,大于 Survivor 空间的一半,则大于等于该年龄的对象直接进入老年代。
public class AllocationTest {
    private static final int _1MB = 1024 * 1024;
    
    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:MaxTenuringThreshold=15
        -XX:+PrintTenuringDistribution
     * */
    
    public static void testTenuringThreshold2() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }
    
    public static void main(String[] args) {
        testPretenureSizeThreshold2();
    }
}

结果分析:
image20210121220225619.png

4.5、空间分配担保

  • 当出现大量对象在 Minor GC 后仍然存活的情况,就需要老年代进行分配担保,让 Survivor 无法容纳的对象直接进入老年代。

image20210120225022881.png

private static final int _1MB = 1024 * 1024;
 
/**
 * VM参数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure
 */
@SuppressWarnings("unused")
public static void testHandlePromotion() {
    byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;
    allocation1 = new byte[2 * _1MB];
    allocation2 = new byte[2 * _1MB];
    allocation3 = new byte[2 * _1MB];
    allocation1 = null;
    allocation4 = new byte[2 * _1MB];
    allocation5 = new byte[2 * _1MB];
    allocation6 = new byte[2 * _1MB];
    allocation4 = null;
    allocation5 = null;
    allocation6 = null;
    allocation7 = new byte[2 * _1MB];
​

以 HandlePromotionFailure = false参数来运行的结果:

[GC [DefNew: 6487K->152K(9216K), 0.0040346 secs] 6487K->4248K(19456K), 0.0040639 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [DefNew: 6546K->6546K(9216K), 0.0004896 secs] 10642K->4248K(19456K), 0.0005141 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

以 HandlePromotionFailure = true参数来运行的结果:

[GC [DefNew: 6487K->152K(9216K), 0.0040346 secs] 6487K->4248K(19456K), 0.0040639 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [DefNew: 6546K->152K(9216K), 0.0004896 secs] 10642K->4248K(19456K), 0.0006143 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

从日志可看出,设置HandlePromotionFailure 参数不同的值,影响到虚拟机的空间分配担保原则,当参数为true时,即允许担保失败,会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,来决定后续是 Minor GC 还是 Full GC。

5、参数汇总

5.1、堆大小设置

JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

配置案例
  1. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
    • Xmx3550m :设置JVM最大可用内存为3550M。
    • -Xms3550m :设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
    • -Xmn2g :设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小 。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
    • -Xss128k : 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内 存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
  2. java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
    • -XX:NewRatio=4 :设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
    • -XX:SurvivorRatio=4 :设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
    • -XX:MaxPermSize=16m :设置持久代大小为16m。
    • -XX:MaxTenuringThreshold=0 :设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代 。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间 ,增加在年轻代即被回收的概论。

5.2、回收器的选择

JVM给了三种选择:串行收集器、并行收集器、并发收集器 ,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

5.2.1、吞吐量优先的并行收集器

配置案例:

  1. java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
    • -XX:+UseParallelGC :选择垃圾收集器为并行收集器。 此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
    • -XX:ParallelGCThreads=20 :配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
  2. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
    • -XX:+UseParallelOldGC :配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
  3. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
    • -XX:MaxGCPauseMillis=100 : 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
  4. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
    • -XX:+UseAdaptiveSizePolicy :设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

5.2.2、响应时间优先 的并发收集器

配置案例:

  1. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

    • -XX:+UseConcMarkSweepGC :设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
    • -XX:+UseParNewGC :设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
  2. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5

    -XX:+UseCMSCompactAtFullCollection

    • -XX:CMSFullGCsBeforeCompaction :由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
    • -XX:+UseCMSCompactAtFullCollection :打开对年老代的压缩。可能会影响性能,但是可以消除碎片

5.3、辅助信息

JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:

  • -XX:+PrintGC

    输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]

    ​ [Full GC 121376K->10414K(130112K), 0.0650971 secs]

  • -XX:+PrintGCDetails

    输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]

    ​ [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs 121376K->10414K(130112K), 0.0436268 secs]

  • -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps 可与上面两个混合使用
    输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

  • -XX:+PrintGCApplicationConcurrentTime: 打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
    输出形式:Application time: 0.5291524 seconds

  • -XX:+PrintGCApplicationStoppedTime :打印垃圾回收期间程序暂停的时间。可与上面混合使用
    输出形式:Total time for which application threads were stopped: 0.0468229 seconds

  • -XX:PrintHeapAtGC :打印GC前后的详细堆栈信息

  • -Xloggc:filename :与上面几个配合使用,把相关日志信息记录到文件以便分析。

5.4、常见配置汇总

  1. 堆设置
    • -Xms :初始堆大小
    • -Xmx :最大堆大小
    • -XX:NewSize=n :设置年轻代大小
    • -XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
    • -XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
    • -XX:MaxPermSize=n :设置持久代大小
  2. 收集器设置
    • -XX:+UseSerialGC :设置串行收集器
    • -XX:+UseParallelGC :设置并行收集器
    • -XX:+UseParalledlOldGC :设置并行年老代收集器
    • -XX:+UseConcMarkSweepGC :设置并发收集器
  3. 垃圾回收统计信息
    • -XX:+PrintGC
    • -XX:+PrintGCDetails
    • -XX:+PrintGCTimeStamps
    • -Xloggc:filename
  4. 并行收集器设置
    • -XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。
    • -XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间
    • -XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
  5. 并发收集器设置
    • -XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况。
    • -XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

结束语

关于JVM的学习其实远远不止于此,学习了JVM垃圾回收,还需要了解类文件结构、虚拟机类的加载机制、虚拟机字节码执行引擎、前后端编译优化等等,这些在后面也会学习到,写一些相关笔记。

最后老话,欢迎有问题,有建议,有好资料的哥们和我分享,当然我也很乐意分享我的资料。只要你主动我们就有故事!!!

微信截图_20210122212100

我是星宇,一个满头黑发,渴望秃头的开发,我们下期见!

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://mxyblogs.club/archives/jvm内存调优-垃圾收集器

Buy me a cup of coffee ☕.