JVM02——JVM垃圾回收与性能调优(上)(三)

简介: 6.垃圾回收6.1 判断垃圾6.1.1 引用计数法

打印信息如下。

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对新生代进行扫描,将其中可以回收的对象进行回收。

相关文章
|
4天前
|
监控 算法 Java
Java虚拟机(JVM)的垃圾回收机制深度解析####
本文深入探讨了Java虚拟机(JVM)的垃圾回收机制,旨在揭示其背后的工作原理与优化策略。我们将从垃圾回收的基本概念入手,逐步剖析标记-清除、复制算法、标记-整理等主流垃圾回收算法的原理与实现细节。通过对比不同算法的优缺点及适用场景,为开发者提供优化Java应用性能与内存管理的实践指南。 ####
|
18天前
|
算法 JavaScript 前端开发
垃圾回收机制对 JavaScript 性能的影响有哪些?
【10月更文挑战第29天】垃圾回收机制对JavaScript性能有着重要的影响。开发者需要了解不同垃圾回收算法的特点和性能开销,通过合理的代码优化和内存管理策略,来降低垃圾回收对性能的负面影响,提高JavaScript程序的整体性能。
|
12天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
14天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
36 6
|
10天前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
66 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
21天前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
29天前
|
Arthas 监控 数据可视化
JVM进阶调优系列(7)JVM调优监控必备命令、工具集合|实用干货
本文介绍了JVM调优监控命令及其应用,包括JDK自带工具如jps、jinfo、jstat、jstack、jmap、jhat等,以及第三方工具如Arthas、GCeasy、MAT、GCViewer等。通过这些工具,可以有效监控和优化JVM性能,解决内存泄漏、线程死锁等问题,提高系统稳定性。文章还提供了详细的命令示例和应用场景,帮助读者更好地理解和使用这些工具。
|
1月前
|
监控 架构师 Java
JVM进阶调优系列(6)一文详解JVM参数与大厂实战调优模板推荐
本文详述了JVM参数的分类及使用方法,包括标准参数、非标准参数和不稳定参数的定义及其应用场景。特别介绍了JVM调优中的关键参数,如堆内存、垃圾回收器和GC日志等配置,并提供了大厂生产环境中常用的调优模板,帮助开发者优化Java应用程序的性能。