打印信息如下。
Heap def new generation total 9216K, used 2311K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 28% used [0x00000000fec00000, 0x00000000fee41d50, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000) Metaspace used 3268K, capacity 4496K, committed 4864K, reserved 1056768K class space used 347K, capacity 388K, committed 512K, reserved 1048576K
观察到我们分配的新生代内存是10M,但是打印的只有9M,这是因为伊甸园占用8M,幸存区From和To各占用1M,JVM认为幸存区中的内存始终有一块空间是需要空着的,不能存放内容,所以这部分空间没有被计算进来。
新生代的伊甸园只有8M内存,其中28%还已经被占用了,新增以下代码。
ArrayList<byte[]> list = new ArrayList<>(); list.add(new byte[_7MB]); 1 2
果然触发了Minor GC。垃圾回收前新生代占用2147k,垃圾回收后占用749k,新生代总大小9216K。堆空间回收前占用2147K,垃圾回收后占用749K,总大小19456K。由于数组被放入了list集合中,而list集合被根GC Root所访问,不会被垃圾回收,所以byte[]数组被移到了幸存区中。垃圾回收后放入了7M的对象。伊甸园占用率93%。
[GC (Allocation Failure) [DefNew: 2147K->749K(9216K), 0.0128891 secs] 2147K->749K(19456K), 0.0129487 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] Heap def new generation total 9216K, used 8327K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 92% used [0x00000000fec00000, 0x00000000ff366830, 0x00000000ff400000) from space 1024K, 73% used [0x00000000ff500000, 0x00000000ff5bb4d8, 0x00000000ff600000) to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
再新增以下代码,创建一个1M大小的数组。
list.add(new byte[_1MB]);
1
打印信息如下。触发了两次GC操作,在第二次GC操作时,幸存区已经无法容纳这个1M的byte[]对象了,因此部分对象从幸存区晋升到了老年代中。
[GC (Allocation Failure) [DefNew: 2147K->748K(9216K), 0.0039741 secs] 2147K->748K(19456K), 0.0040840 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] [GC (Allocation Failure) [DefNew: 8244K->26K(9216K), 0.0096121 secs] 8244K->7932K(19456K), 0.0096617 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] Heap def new generation total 9216K, used 1216K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 14% used [0x00000000fec00000, 0x00000000fed29758, 0x00000000ff400000) from space 1024K, 2% used [0x00000000ff400000, 0x00000000ff406bb8, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 7905K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 77% used [0x00000000ff600000, 0x00000000ffdb8508, 0x00000000ffdb8600, 0x0000000100000000) Metaspace used 3314K, capacity 4496K, committed 4864K, reserved 1056768K class space used 353K, capacity 388K, committed 512K, reserved 1048576K
下面介绍一种大对象直接晋升老年代的情况。将之前的代码注释,直接在list集合中添加8M的byte[]数组。
这种情况伊甸园肯定放不下这个数组,幸存区也放不下,JVM经过计算,发现即使触发了垃圾回收也无法在新生代存放这个对象,这种情况不会触发垃圾回收,如果老年代空间足够这个大对象就会直接晋升老年代。
Heap def new generation total 9216K, used 2478K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 30% used [0x00000000fec00000, 0x00000000fee6bbe8, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000) Metaspace used 3333K, capacity 4496K, committed 4864K, reserved 1056768K class space used 357K, capacity 388K, committed 512K, reserved 1048576K
如果新生代,老年代都不足以存放了,就会Out of Memory。
思考一个问题。如果一个非主线程的其他线程发生内存溢出,会导致整个java进程退出
吗?实验下。 public static void main(String[] args) throws InterruptedException { new Thread(() -> { ArrayList<byte[]> list = new ArrayList<>(); list.add(new byte[_8MB]); list.add(new byte[_8MB]); }).start(); System.out.println("sleep...."); Thread.sleep(1000L); System.out.println("I'm alive,Haha");
结果如下。一个非主线程的其他线程发生内存溢出,不会导致整个java进程退出。
sleep.... [GC (Allocation Failure) [DefNew: 4796K->990K(9216K), 0.0038712 secs][Tenured: 8192K->9179K(10240K), 0.0052058 secs] 12988K->9179K(19456K), [Metaspace: 4269K->4269K(1056768K)], 0.0094779 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] [Full GC (Allocation Failure) [Tenured: 9179K->9124K(10240K), 0.0038569 secs] 9179K->9124K(19456K), [Metaspace: 4269K->4269K(1056768K)], 0.0039093 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space at cn.itcast.jvm.t2.Demo2_1.lambda$main$0(Demo2_1.java:20) at cn.itcast.jvm.t2.Demo2_1$$Lambda$1/1023892928.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) I'm alive,Haha Heap def new generation total 9216K, used 349K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 4% used [0x00000000fec00000, 0x00000000fec57530, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) tenured generation total 10240K, used 9124K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 89% used [0x00000000ff600000, 0x00000000ffee9060, 0x00000000ffee9200, 0x0000000100000000) Metaspace used 4294K, capacity 4708K, committed 4992K, reserved 1056768K class space used 467K, capacity 528K, committed 640K, reserved 1048576K
6.8 垃圾回收器
6.8.1 垃圾回收器分类
有三类垃圾回收器。
1、串行垃圾回收器
单线程
适合堆内存较小场景,适合个人电脑。
2、吞吐量优先
多线程
适合堆内存较大场景,需要多核CPU支持
让单位时间内,SWT时间最短
3、响应时间优先
多线程
适合堆内存较大场景,需要多核CPU支持
尽可能使单次响应SWT时间最少
注:吞吐量优先追求的是单位时间的STW时间最短,响应时间优先是追求每次响应的速度最快。举例如下,算法1:0.2s/次 * 2次 =0.4s,算法2:0.1s/次 * 5次 =0.5s,算法1进行垃圾回收的总时间最短,吞吐量更大。算法2的单次垃圾回收时间更短,响应速度更快。
6.8.2 串行垃圾回收器
使用-XX:+UseSerialGC = Serial + SerialOld可以开启串行垃圾回收器。其中新生代采用的算法是复制算法,老年代采用的是标记整理算法。在垃圾回收线程运行前,会先阻塞其他线程。
6.8.3 吞吐量优先
开启-XX:+UseParrallelGC或者-XX:+UseParrallelGC(开启一个另外一个会自动开启)使用吞吐量优先的垃圾回收器,其新生代算法仍为复制算法,老年代算法仍为标记整理算法。不过其特别之处在于:在垃圾回收前,用户线程会暂停,但是垃圾回收时会开启多个线程同时执行垃圾回收操作,开启的线程数量与cpu核数相同。当然,我们也可以使用-XX:ParallelGCThreads=n来指定进行垃圾回收的线程数量。参数-XX:+UseAdaptiveSizePolicy可以使用自适应的策略来调整堆的大小,这里主要是新生代空间的调整。XX:GCTimeRatio=n用于设置除垃圾回收时间外的时间占比,假设-XX:GCTimeRatio=19 ,则垃圾收集时间为1/(1+19),默认值为99,即1%时间用于垃圾收集。-XX:ParrallelGCMills=ms用于调整每一次垃圾回收的暂停时间。但是XX:GCTimeRatio=n和
-XX:ParrallelGCMills=ms这两个参数其实是有冲突的。当GCTimeRatio设置的更大,就要调整堆使堆更大,以增加吞吐量,而堆更大则每次垃圾回收的暂停时间就会更长。两者要进行合理取舍。(注:JVM的堆大小有起始值和最大值,堆在这个范围内进行大小调节)
6.8.4 响应时间优先
ConcMarkSweepGC是工作在老年代的垃圾回收器。望文生义,响应时间优先垃圾回收器采取的垃圾回收策略是标记清除法(快,无需内存移动)。其中Con是concurrent的缩写,这表示响应时间优先的垃圾回收器在某些阶段采用的是并发策略(在某些阶段仍需STW):垃圾回收线程和其他用户线程并行执行,这样显然有利于提高程序的响应性能,但是也会牺牲吞吐量,与CMS垃圾回收器配合的垃圾回收器为ParNewGC。不过,CMS垃圾回收器有时会并发失败,这时会采取补救措施,将CMS退化为SerialOld。
在老年代快满时,将会阻塞其他线程,然后由垃圾回收线程对于GC Root进行快速的标记,由于只标记GC Root,这个过程很短。然后其他线程就可以恢复执行了,同时垃圾回收与其他用户线程并发执行,垃圾回收并发标记除根对象外的其他要被回收的对象。在并发标记结束后再次STW,然后重新标记,防止由于用户线程的活动导致对象的地址发生变化。重新标记结束后用户线程又可以执行了,垃圾回收线程进行并发清理。
可以设置并行线程数和并发线程数,并行线程数一般与cpu的核数相同,一般建议将并发线程数设置为并发线程数的1/4,即垃圾回收线程与用户线程按照1:3来抢占cpu,并发执行。
在进行并发清理的过程中,不能把这个过程新产生的垃圾清理掉,这些垃圾需要下一次垃圾回收时进行清理,称为浮动垃圾。因为清理是并发的,可能还没有清理出足够的空间存放这部分浮动垃圾,因此不能够像其它垃圾回收器一样,等到堆内存不足了再进行垃圾回收,必须为他们预留空间,参数-XX:CMSInitiatingOccupancyFraction=percent可以用来设置执行垃圾回收的时机:当内存的占比达到设置值就执行垃圾回收。
有可能新生代的对象引用老年代的对象,在进行重新标记时,要对整个堆的对象进行扫描,包括新生代的对象,然后根据这个新生代的对象扫描整个老年区的对象,做可达性的分析。这样无疑会消耗时间。使用参数-XX:CMSScanvengeBeforeRemark会先使用ParNewGC对新生代进行扫描,将其中可以回收的对象进行回收。