Java性能优化(九)-多线程调优-垃圾回收机制优化

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Java性能优化(九)-多线程调优-垃圾回收机制优化

我们知道,在Java开发中,开发人员是无需过度关注对象的回收与释放的,JVM的垃圾回收机制可以减轻不少工作量。但完全交由JVM回收对象,也会增加回收性能的不确定性。在一些特殊的业务场景下,不合适的垃圾回收算法以及策略,都有可能导致系统性能下降。

面对不同的业务场景,垃圾回收的调优策略也不一样。例如,在对内存要求苛刻的情况下,需要提高对象的回收效率;在CPU使用率高的情况下,需要降低高并发时垃圾回收的频率。可以说,垃圾回收的调优是一项必备技能。

这讲我们就把这项技能的学习进行拆分,看看回收(后面简称GC)的算法有哪些,体现GC算法好坏的指标有哪些,又如何根据自己的业务场景对GC策略进行调优?

垃圾回收机制

掌握GC算法之前,我们需要先弄清楚3个问题。第一,回收发生在哪里?第二,对象在什么时候可以被回收?第三,如何回收这些对象?

1. 回收发生在哪里?

JVM的内存区域中,程序计数器、虚拟机栈和本地方法栈这3个区域是线程私有的,随着线程的创建而创建,销毁而销毁;栈中的栈帧随着方法的进入和退出进行入栈和出栈操作,每个栈帧中分配多少内存基本是在类结构确定下来的时候就已知的,因此这三个区域的内存分配和回收都具有确定性。

那么垃圾回收的重点就是关注堆和方法区中的内存了,堆中的回收主要是对象的回收,方法区的回收主要是废弃常量和无用的类的回收。

2. 对象在什么时候可以被回收?

那JVM又是怎样判断一个对象是可以被回收的呢?一般一个对象不再被引用,就代表该对象可以被回收。目前有以下两种算法可以判断该对象是否可以被回收。

引用计数算法:这种算法是通过一个对象的引用计数器来判断该对象是否被引用了。每当对象被引用,引用计数器就会加1;每当引用失效,计数器就会减1。当对象的引用计数器的值为0时,就说明该对象不再被引用,可以被回收了。这里强调一点,虽然引用计数算法的实现简单,判断效率也很高,但它存在着对象之间相互循环引用的问题。

可达性分析算法:GC  Roots  是该算法的基础,GC Roots是所有对象的根对象,在JVM加载时,会创建一些普通对象引用正常对象。这些对象作为正常对象的起始点,在垃圾回收时,会从这些GC Roots开始向下搜索,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。目前HotSpot虚拟机采用的就是这种算法。

以上两种算法都是通过引用来判断对象是否可以被回收。在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为了以下四种:

3. 如何回收这些对象?

了解完Java程序中对象的回收条件,那么垃圾回收线程又是如何回收这些对象的呢?JVM垃圾回收遵循以下两个特性。

自动性:Java提供了一个系统级的线程来跟踪每一块分配出去的内存空间,当JVM处于空闲循环时,垃圾收集器线程会自动检查每一块分配出去的内存空间,然后自动回收每一块空闲的内存块。

不可预期性:一旦一个对象没有被引用了,该对象是否立刻被回收呢?答案是不可预期的。我们很难确定一个没有被引用的对象是不是会被立刻回收掉,因为有可能当程序结束后,这个对象仍在内存中。

垃圾回收线程在JVM中是自动执行的,Java程序无法强制执行。我们唯一能做的就是通过调用System.gc方法来"建议"执行垃圾收集器,但是否可执行,什么时候执行?仍然不可预期。

GC算法

JVM提供了不同的回收算法来实现这一套回收机制,通常垃圾收集器的回收算法可以分为以下几种:

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现,JDK1.7 update14 之后Hotspot虚拟机所有的回收器整理如下(以下为服务端垃圾收集器):

其实在JVM规范中并没有明确GC的运作方式,各个厂商可以采用不同的方式实现垃圾收集器。我们可以通过JVM工具查询当前JVM使用的垃圾收集器类型,首先通过ps命令查询出进程ID,再通过jmap -heap ID查询出JVM的配置信息,其中就包括垃圾收集器的设置类型。

GC性能衡量指标

一个垃圾收集器在不同场景下表现出的性能也不一样,那么如何评价一个垃圾收集器的性能好坏呢?我们可以借助一些指标。

吞吐量:这里的吞吐量是指应用程序所花费的时间和系统总运行时间的比值。我们可以按照这个公式来计算GC的吞吐量:系统总运行时间=应用程序耗时+GC耗时。如果系统运行了100分钟,GC耗时1分钟,则系统吞吐量为99%。GC的吞吐量一般不能低于95%。

停顿时间:指垃圾收集器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。

垃圾回收频率:多久发生一次指垃圾回收呢?通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们只要适当地增大堆内存空间,保证正常的垃圾回收频率即可。

查看&分析GC日志

已知了性能衡量指标,现在我们需要通过工具查询GC相关日志,统计各项指标的信息。首先,我们需要通过JVM参数预先设置GC日志,通常有以下几种JVM参数设置:

-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径

这里使用如下参数来打印日志:

-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs

打印后的日志为:

上图是运行很短时间的GC日志,如果是长时间的GC日志,我们很难通过文本形式去查看整体的GC性能。此时,我们可以通过GCViewer工具打开日志文件,图形化界面查看整体的GC性能,如下图所示:

通过工具,我们可以看到吞吐量、停顿时间以及GC的频率,从而可以非常直观地了解到GC的性能情况。

GCeasy

这里我再推荐一个比较好用的GC日志分析工具,GCeasy是一款非常直观的GC日志分析工具,我们可以将日志文件压缩之后,上传到GCeasy官网即可看到非常清楚的GC日志分析结果:

GC调优策略

找出问题后,就可以进行调优了,下面介绍几种常用的GC调优策略。

1. 降低Minor GC频率

通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,因此我们可以通过增大新生代空间来降低Minor GC的频率。

可能你会有这样的疑问,扩容Eden区虽然可以减少Minor GC的次数,但不会增加单次Minor GC的时间吗?如果单次Minor GC的时间增加,那也很难达到我们期待的优化效果呀。

我们知道,单次Minor GC时间是由两部分组成:T1(扫描新生代)和T2(复制存活对象)。假设一个对象在Eden区的存活时间为500ms,Minor GC的时间间隔是300ms,那么正常情况下,Minor GC的时间为 :T1+T2。

当我们增大新生代空间,Minor GC的时间间隔可能会扩大到600ms,此时一个存活500ms的对象就会在Eden区中被回收掉,此时就不存在复制存活对象了,所以再发生Minor GC的时间为:两次扫描新生代,即2T1。

可见,扩容后,Minor GC时增加了T1,但省去了T2的时间。通常在虚拟机中,复制对象的成本要远高于扫描成本。

如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加Minor GC的时间。如果堆中的短期对象很多,那么扩容新生代,单次Minor GC时间不会显著增加。因此,单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小。

2. 降低Full GC的频率

通常情况下,由于堆内存空间不足或老年代对象太多,会触发Full GC,频繁的Full GC会带来上下文切换,增加系统的性能开销。我们可以使用哪些方法来降低Full GC的频率呢?

减少创建大对象:在平常的业务场景中,我们习惯一次性从数据库中查询出一个大对象用于web端显示。例如,我之前碰到过一个一次性查询出60个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过Minor GC之后也会进入到老年代。这种大对象很容易产生较多的Full GC。

我们可以将这种大对象拆解出来,首次只查询一些比较重要的字段,如果还需要其它字段辅助查看,再通过第二次查询显示剩余的字段。

增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低Full GC的频率。

选择合适的GC回收器

假设我们有这样一个需求,要求每次操作的响应时间必须在500ms以内。这个时候我们一般会选择响应速度较快的GC回收器,CMS(Concurrent Mark Sweep)回收器和G1回收器都是不错的选择。

而当我们的需求对系统吞吐量有要求时,就可以选择Parallel Scavenge回收器来提高系统的吞吐量。

总结

今天的内容比较多,最后再强调几个重点。

垃圾收集器的种类很多,我们可以将其分成两种类型,一种是响应速度快,一种是吞吐量高。通常情况下,CMS和G1回收器的响应速度快,Parallel Scavenge回收器的吞吐量高。

在JDK1.8环境下,默认使用的是Parallel Scavenge(年轻代)+Serial Old(老年代)垃圾收集器,你可以通过文中介绍的查询JVM的GC默认配置方法进行查看。

通常情况,JVM是默认垃圾回收优化的,在没有性能衡量标准的前提下,尽量避免修改GC的一些性能配置参数。如果一定要改,那就必须基于大量的测试结果或线上的具体性能来进行调整。

相关文章
|
1月前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
56 0
|
25天前
|
并行计算 算法 安全
面试必问的多线程优化技巧与实战
多线程编程是现代软件开发中不可或缺的一部分,特别是在处理高并发场景和优化程序性能时。作为Java开发者,掌握多线程优化技巧不仅能够提升程序的执行效率,还能在面试中脱颖而出。本文将从多线程基础、线程与进程的区别、多线程的优势出发,深入探讨如何避免死锁与竞态条件、线程间的通信机制、线程池的使用优势、线程优化算法与数据结构的选择,以及硬件加速技术。通过多个Java示例,我们将揭示这些技术的底层原理与实现方法。
78 3
|
1月前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
1月前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
1月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
2月前
|
机器学习/深度学习 监控 算法
Java虚拟机(JVM)的垃圾回收机制深度剖析####
本文深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法、性能调优策略及未来趋势。通过实例解析,为开发者提供优化Java应用性能的思路与方法。 ####
62 1
|
1月前
|
存储 监控 算法
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
53 0
|
24天前
|
算法 网络协议 Java
【JVM】——GC垃圾回收机制(图解通俗易懂)
GC垃圾回收,标识出垃圾(计数机制、可达性分析)内存释放机制(标记清除、复制算法、标记整理、分代回收)
|
3月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
136 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
4月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。