JVM中垃圾收集器的简单练习-刘宇

作者:刘宇
CSDN博客地址:https://blog.csdn.net/liuyu973971883
有部分资料参考,如有侵权,请联系删除。如有不正确的地方,烦请指正,谢谢。

一、分析垃圾收集日志

JVM运行参数:

  • -verbose:gc //详细输出垃圾回收相关的日志
  • -Xms20m //初始化堆空间大小
  • -Xmx20m //最大的堆空间大小
  • -Xmn10m //最大的年轻代的空间大小
  • -XX:+PrintGCDetails //打印垃圾回收后堆的详细信息
  • -XX:SurvivorRatio=8 //新生代中Eden和Survivor比例为8:1:1

代码:

public class MyTest1 {

    public static void main(String[] args) {
        int size = 1024*1024;
        byte[] myAlloc1 = new byte[2*size];
        byte[] myAlloc2 = new byte[2*size];
        byte[] myAlloc3 = new byte[2*size];
        byte[] myAlloc4 = new byte[2*size];
        System.out.println("Hello world");
    }
}

运行结果及分析:

  • GC:表示Minor GC,一般会在新对象生产时,Eden空间满时触发
  • (Allocation Failure),触发这次GC的原因,分配内存失败
  • PSYoungGen:表示使用Parallel Scavenge收集器收集年轻代
  • 8064K->1016K(9216K):表示年轻代空间中原先使用8064K,收集完成后存活对象占1016K,一共回收了7048K,其实回收的7048K分为了两部分,一部分是真的被回收了,还有一部分晋升到了老年代中去了,年轻代一共占9216K,这里为什么是9216K(9M),而不是我们设置的10M,这是因为我们还有一个不能使用的Survivor空间,9M就是Eden大小+另一个可用的Survivor空间
  • 8064K->5336K(19456K):表示堆空间中原先使用8064K,收集完成后存活对象占5336K,堆的总大小为19456K
  • 0.0038142 secs:表示此次垃圾回收所用时间
  • Times: user=0.00 sys=0.00, real=0.00 secs:用户空间使用了0.00秒,内核空间使用了0.00秒,实际使用了0.00秒
  • PSYoungGen:使用的是Parallel Scavenge收集器
  • ParOldGen:使用的是Parallel Old收集器
  • PSOldGen:使用的是Serial Old收集器

注:其实老年代中的占用空间就是垃圾回收之后整个堆中剩余占用空间大小 - 垃圾回收之后年轻代剩余占用空间大小,如下面案例中:5336K-1016K=4320K


[GC (Allocation Failure) [PSYoungGen: 8064K->1016K(9216K)] 8064K->5336K(19456K), 0.0038142 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Hello world
Heap
 PSYoungGen      total 9216K, used 5351K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 52% used [0x00000000ff600000,0x00000000ffa3bd60,0x00000000ffe00000)
  from space 1024K, 99% used [0x00000000ffe00000,0x00000000ffefe010,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4320K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 42% used [0x00000000fec00000,0x00000000ff038208,0x00000000ff600000)
 Metaspace       used 3582K, capacity 4536K, committed 4864K, reserved 1056768K
  class space    used 395K, capacity 428K, committed 512K, reserved 1048576K

二、对象分配及阈值设置

2.1、对象分配何时在老年代进行分配

当新生代中的内存GC后分配不下该对象时,则该对象会直接在老年代进行分配

JVM运行参数:
-verbose:gc
-Xms20m
-Xmx20m
-Xmn10m
-XX:+PrintGCDetails
-XX:SurvivorRatio=8

代码:

public class MyTest1 {

    public static void main(String[] args) {
        int size = 1024*1024;
        byte[] myAlloc1 = new byte[6*size];
    }
}

运行结果及分析:

  • 从下方的输出可以得知,老年代中使用的空间为6144K,正好就是我们创建的对象大小,从而得知该对象是直接在老年代中分配的。
  • 疑问1:从代码中得知,我们明明创建的是6M大小的对象,并没有大于新生代中的eden space大小呀,为什么还是直接进入到老年代中分配了呢?

    因为JVM自身也会创建一些对象,而它们占用了新生代的一部分内存。

Heap
 PSYoungGen      total 9216K, used 4830K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 58% used [0x00000000ff600000,0x00000000ffab7910,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 6144K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 60% used [0x00000000fec00000,0x00000000ff200010,0x00000000ff600000)
 Metaspace       used 3409K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 367K, capacity 392K, committed 512K, reserved 1048576K

2.2、阈值设置

通过参数设置新建对象如果大于指定大小则直接在老年代中创建,使用参数:-XX:PretenureSizeThreshold=字节大小,注:这个参数只在新生代和老年代都是串行垃圾收集器时才能生效。

JVM运行参数:
-verbose:gc
-Xms20m
-Xmx20m
-Xmn10m
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=4194304 //阈值设置4M
-XX:+UseSerialGC //新生代使用Serial收集器,老年代使用Serial Old收集器

代码:

public class MyTest2 {

    public static void main(String[] args) {
        int size = 1024*1024;
        byte[] myAlloc1 = new byte[5*size];

    }
}

运行结果及分析:

  • 从输出结果我们可以看出,老年代的占用大小为5120K(5M),就是我们新创建的对象大小。
  • def new:使用的是Serial收集器
  • tenured:使用的是Serial Old收集器
Heap
 def new generation   total 9216K, used 4830K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  58% used [0x00000000fec00000, 0x00000000ff0b7940, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5120K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  50% used [0x00000000ff600000, 0x00000000ffb00010, 0x00000000ffb00200, 0x0000000100000000)
 Metaspace       used 3409K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 367K, capacity 392K, committed 512K, reserved 1048576K

三、设置对象的晋升阈值及阈值动态调整策略

3.1、设置对象的晋升阈值

JVM运行参数:
-verbose:gc
-Xms20m
-Xmx20m
-Xmn10m
-XX:+PrintGCDetails
-XX:+PrintCommandLineFlags //打印JVM运行参数
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=5 //设置对象晋升老年代的最大阈值(即年龄),每次GC,年龄+1。
-XX:+PrintTenuringDistribution //打印出幸存区中对象的年龄分布

MaxTenuringThreshold作用:在可以自动调节对象晋升(Promote)到老年代的阈值的GC中,设置其阈值的最大值,及时设置了最大阈值,但是JVM是可以自动调节阈值的,如:设置最大阈值5,但是JVM可能会自动调节成2,最大不会超过我们设定的值。它的默认值为15,CMS中默认值为6,G1中默认值为15(在JVM中,该数值是由4个bit来表示的,所以最大是1111,即15)

自动调节阈值:

经历过多次GC后,存活的对象会在From Survivor与To Survivor之间来回存放,而这里面的一个前提则是这两个空间有足够大小来存放这些数据,在GC算法中,会计算每个对象年龄的大小,如果达到某个年龄后发现总大小已经大于其中一个Survivor大小的50%,那么这时JVM就需要自动调节阈值,不能再继续等到默认值15此GC后才完成晋升。因为这样会导致Survivor空间不足,所有需要调整阈值,让这些存活对象尽快完成晋升。

代码:

public class MyTest3 {
    public static void main(String[] args) {
        int size = 1024*1024;
        byte[] myAlloc1 = new byte[2*size];
        byte[] myAlloc2 = new byte[2*size];
        byte[] myAlloc3 = new byte[2*size];
        byte[] myAlloc4 = new byte[2*size];
        System.out.println("Hello world");
    }
}

运行结果及分析:

  • Desired survivor size 1048576 bytes, new threshold 5 (max 5):

    期望survivor大小为1M,可以通过-XX:TargetSurvivorRatio来设置百分百,如果大于这个期望大小,则JVM会自动调整阈值,当前阈值为5(最大阈值5),注:其中当前阈值JVM是会自动调节的,但不会超过最大阈值

[GC (Allocation Failure) 
Desired survivor size 1048576 bytes, new threshold 5 (max 5)
[PSYoungGen: 7194K->813K(9216K)] 7194K->4917K(19456K), 0.0050814 secs] [Times: user=0.01 sys=0.01, real=0.01 secs] 
Hello world
Heap
 PSYoungGen      total 9216K, used 5315K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 54% used [0x00000007bf600000,0x00000007bfa658e8,0x00000007bfe00000)
  from space 1024K, 79% used [0x00000007bfe00000,0x00000007bfecb410,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 4104K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 40% used [0x00000007bec00000,0x00000007bf002020,0x00000007bf600000)
 Metaspace       used 3017K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 319K, capacity 392K, committed 512K, reserved 1048576K

3.2、动态调整对象晋升阈值的策略

JVM运行参数:
-verbose:gc
-Xmx200m
-Xmn50m
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps //打印GC的时间
-XX:MaxTenuringThreshold=3
-XX:+PrintTenuringDistribution
-XX:TargetSurvivorRatio=60 //当达到Survivor空间的60%就自动调整阈值
-XX:+UseConcMarkSweepGC //老年代使用CMS垃圾收集器
-XX:+UseParNewGC //新生代使用ParNew垃圾收集器

代码:

package com.brycen.demo.gc;

public class MyTest4 {
    public static void main(String[] args) throws InterruptedException {
        byte[] byte_1 = new byte[512 * 1024];
        byte[] byte_2 = new byte[512 * 1024];
		
		//循环创建40M的对象,使其触发GC
        myGc();
        Thread.sleep(1000);
        System.out.println("11111");

        myGc();
        Thread.sleep(1000);
        System.out.println("22222");

        myGc();
        Thread.sleep(1000);
        System.out.println("33333");

        myGc();
        Thread.sleep(1000);
        System.out.println("44444");

        myGc();
        Thread.sleep(1000);
        System.out.println("44444");

		//这里主要的作用是超出Survivor空间的60%,Survivor空间为5M,60%就是3M
        byte[] byte_3 = new byte[1024 * 1024];
        byte[] byte_4 = new byte[1024 * 1024];
        byte[] byte_5 = new byte[1024 * 1024];

        myGc();
        Thread.sleep(1000);
        System.out.println("55555");

        myGc();
        Thread.sleep(1000);
        System.out.println("66666");
        System.out.println("Hello world");
    }

    private static void myGc(){
        for (int i=0; i<40; i++){
            byte[] byte_array = new byte[1024 * 1024];
        }
    }
}

运行结果及分析:
注意看第五次的GC,它将阈值自动调整为了1。因为我们代码中第五次myGc之前创建了3M的对象,随后myGc方法中不断循环创建对象,随着Eden空间占满,随即会调用GC,回收不断循环中的废弃对象,而这3M对象不会被GC掉,就会进入Survivor空间,3M+程序开头的1M,即超过了Survivor空间的60%,就会触发自动调整阈值机制。

  • Desired survivor size 3145728 bytes, new threshold 3 (max 3)

    期望的survivor大小为3M,按照我们的参数设置,即survivor空间的60%,当前阈值为3,最大阈值3.

  • age 1: 1786704 bytes, 1786704 total

    年龄为1的对象大小

  • age 2: 1562152 bytes, 1562384 total

    年龄为2的对象大小

  • age 3: 1561544 bytes, 1561832 total

    年龄为3的对象大小

2021-11-14T20:53:59.455-0800: [GC (Allocation Failure) 2021-11-14T20:53:59.455-0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age   1:    1786704 bytes,    1786704 total
: 40269K->1763K(46080K), 0.0029305 secs] 40269K->1763K(60416K), 0.0030062 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
11111
2021-11-14T20:54:00.468-0800: [GC (Allocation Failure) 2021-11-14T20:54:00.468-0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age   1:        232 bytes,        232 total
- age   2:    1562152 bytes,    1562384 total
: 42291K->1884K(46080K), 0.0017464 secs] 42291K->1884K(60416K), 0.0017989 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
22222
2021-11-14T20:54:01.478-0800: [GC (Allocation Failure) 2021-11-14T20:54:01.478-0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age   1:         56 bytes,         56 total
- age   2:        232 bytes,        288 total
- age   3:    1561544 bytes,    1561832 total
: 41996K->1662K(46080K), 0.0011186 secs] 41996K->1662K(60416K), 0.0011718 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
33333
2021-11-14T20:54:02.488-0800: [GC (Allocation Failure) 2021-11-14T20:54:02.488-0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age   1:         56 bytes,         56 total
- age   2:         56 bytes,        112 total
- age   3:        232 bytes,        344 total
: 42391K->310K(46080K), 0.0049811 secs] 42391K->1850K(60416K), 0.0050418 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
44444
2021-11-14T20:54:03.500-0800: [GC (Allocation Failure) 2021-11-14T20:54:03.500-0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 1 (max 3)
- age   1:    3145832 bytes,    3145832 total
- age   2:         56 bytes,    3145888 total
- age   3:         56 bytes,    3145944 total
: 41042K->3097K(46080K), 0.0027982 secs] 42582K->4638K(60416K), 0.0028757 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
55555
2021-11-14T20:54:04.511-0800: [GC (Allocation Failure) 2021-11-14T20:54:04.511-0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age   1:         56 bytes,         56 total
: 43832K->12K(46080K), 0.0046212 secs] 45373K->4625K(60416K), 0.0046923 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
66666
Hello world
Heap
 par new generation   total 46080K, used 18016K [0x00000007b3800000, 0x00000007b6a00000, 0x00000007b6a00000)
  eden space 40960K,  43% used [0x00000007b3800000, 0x00000007b4994f30, 0x00000007b6000000)
  from space 5120K,   0% used [0x00000007b6000000, 0x00000007b60033c0, 0x00000007b6500000)
  to   space 5120K,   0% used [0x00000007b6500000, 0x00000007b6500000, 0x00000007b6a00000)
 concurrent mark-sweep generation total 14336K, used 4612K [0x00000007b6a00000, 0x00000007b7800000, 0x00000007c0000000)
 Metaspace       used 3018K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 319K, capacity 392K, committed 512K, reserved 1048576K
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM (Java Virtual Machine) G1 (Garbage-First) 垃圾收集是一种用于 Java 应用程序的垃圾收集算法。它是自JDK 7u4版本后引入的一种全新的垃圾收集。 G1垃圾收集的设计目标是为了解决传统的分代垃圾收集可能遇到的一些问题,如停顿时间长、内存碎片化等。它采用了一种基于区域的垃圾收集方式,可以将内存划分为多个大小相等的区域,每个区域可以是Eden、Survivor或Old区。 G1垃圾收集的工作原理如下: 1. 初始标记(Initial Mark):标记所有从根对象直接可达的对象。 2. 并发标记(Concurrent Mark):在并发执行程序的同时,标记那些在初始标记阶段无法访问到的对象。 3. 最终标记(Final Mark):为并发标记阶段发生改变的对象进行最终标记。 4. 筛选回收(Live Data Counting and Evacuation):根据各个区域的回收价值来优先回收价值低的区域。 G1垃圾收集具有以下特点: - 并发执行:在执行垃圾收集过程时,尽可能减少应用程序的停顿时间。 - 分区回收:将整个堆划分为多个区域,可以根据需要优先回收垃圾较多的区域,从而避免全堆回收带来的长时间停顿。 - 内存整理:G1垃圾收集会对内存进行整理,减少内存碎片化,提高内存利用率。 需要注意的是,G1垃圾收集并不适用于所有情况。在特定的场景下,如大堆情况下的长时间运行、对延迟要求非常高的应用等,可能需要考虑其他垃圾收集的使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值