架构师必备技能之JVM调优

JVM介绍:

JVM(Java虚拟机)是Java平台的关键组成部分之一。它是一个在操作系统和Java应用程序之间充当中间层的虚拟机。JVM的主要目标是提供Java程序的平台无关性和安全性。

  1. Java字节码执行:Java源代码经过编译器编译后生成字节码(Bytecode),而不是直接生成本地机器码。JVM负责解释和执行这些字节码,以实现Java程序的跨平台性。
  2. 内存管理:JVM管理Java应用程序的内存使用。它将内存划分为不同的区域,包括堆(Heap)、栈(Stack)、方法区(Method Area)等。堆用于存储对象实例,栈用于存储方法调用和局部变量,方法区用于存储类信息和常量池等。
  3. 垃圾收集:JVM提供自动的垃圾收集机制,用于回收不再使用的内存对象。它通过识别不可达对象,并释放其占用的内存。垃圾收集过程可以减少开发人员手动管理内存的负担,并提高应用程序的可靠性。
  4. 即时编译器(Just-In-Time Compiler,JIT):JVM中包含即时编译器,用于将热点代码(Hotspot Code)转换为本地机器码以提高执行速度。即时编译器在运行时根据代码的执行情况进行优化,将频繁执行的代码编译为本地机器码,以替代解释执行。
  5. 类加载和动态链接:JVM负责加载Java类文件,并在运行时解析和链接类的依赖关系。类加载器(ClassLoader)负责在需要时加载类文件,并生成对应的类对象。动态链接在运行时解析和连接类之间的引用关系,以实现类的方法调用和字段访问。

JVM区域划分:

JVM将内存划分为不同的区域,每个区域具有特定的功能和用途。不同的垃圾收集器和JVM版本可能会有不同的区域划分方式和命名,以下是JVM常见的区域划分。

  1. 堆(Heap):堆是JVM管理的最大的内存区域,用于存储对象实例。所有通过new关键字创建的对象都分配在堆上。堆被进一步划分为新生代(Young Generation)和老年代(Old Generation)。
  2. 新生代:新生代用于存放新创建的对象。它又分为Eden区、Survivor区(通常是两个)。
  3. Eden区:新创建的对象首先分配在Eden区。
  4. Survivor区:当Eden区满时,存活的对象会被移到Survivor区。在Survivor区中,对象经过多次垃圾收集后仍然存活的对象,最终会被移到老年代或其他Survivor区。
  5. 老年代:老年代用于存放长时间存活的对象。通常情况下,老年代的对象比较稳定,不容易被垃圾收集回收。
  6. 方法区(Method Area):方法区用于存储类信息、常量、静态变量、即时编译器编译后的代码等。它是所有线程共享的内存区域。
  7. 栈(Stack):栈用于存储线程的方法调用和局部变量等信息。每个线程在运行时都会创建一个对应的栈帧(Stack Frame),用于存储方法的局部变量、操作数栈、方法返回值等。
  8. 本地方法栈(Native Method Stack):本地方法栈与栈类似,但是它用于执行本地方法(Native Method)。
  9. PC寄存器(Program Counter Register):PC寄存器用于存储当前线程执行的字节码指令地址。
  10. 直接内存(Direct Memory):直接内存并不是JVM内存区域的一部分,但是在内存管理中起到重要作用。它是通过使用ByteBuffer等类直接操作系统内存而分配的内存,而不是通过JVM堆分配的。直接内存的分配和释放由JVM管理,但是它使用的是操作系统的内存。

JVM OOM 原因

  1. JVM设置的内存过小:如果业务需要较大的内存空间,而给JVM设置的内存过小,会导致内存不足的情况。解决方案是增加JVM的堆内存大小,通过调整-Xmx和-Xms选项来提高最大堆和初始堆大小,以满足应用程序的内存需求。
  2. GC回收速度跟不上内存消耗速度:当程序向List、Map等集合中填充大量数据,而GC回收速度跟不上内存消耗速度时,会导致内存紧张。解决方案包括:
  3. 分页查询:对于查询结果集较大的情况,应该使用分页查询,限制返回的数据量,避免一次性加载大量数据到内存中。
  4. 优化数据结构:使用合适的数据结构,如ArrayList替代LinkedList,可以减少内存消耗和提高性能。
  5. 内存泄漏:内存泄漏是指应用程序不再使用的对象仍然被保持引用,导致这些对象无法被垃圾收集器回收,从而占用了大量的内存。常见的内存泄漏情况包括:
  6. 打开文件不释放:在使用文件资源时,确保在不需要的时候及时关闭文件流,释放资源。
  7. 创建网络连接不关闭:在使用网络连接资源时,要确保在不再需要连接时及时关闭连接,释放资源。
  8. 不再使用的对象未断开引用关系:确保及时将不再使用的对象的引用置为null,使其成为不可达对象,以便垃圾收集器可以回收其占用的内存。
  9. 使用静态变量持有大对象引用:静态变量的生命周期很长,如果静态变量持有大对象的引用,会导致这些对象无法被回收。应该避免使用静态变量来持有大对象的引用,或者及时将其设置为null,释放引用。

优化建议

  1. 增加JVM堆内存:如果业务需要较大的内存空间来正常运行,可以考虑增加JVM的堆内存大小。通过调整-Xmx和-Xms选项来增加最大堆和初始堆大小,以满足应用程序的内存需求。
  2. 分页查询:当查询结果集较大时,应考虑分页查询来限制返回的数据量。只获取需要的数据,避免一次性加载大量数据到内存中。可以通过数据库的分页查询语句或在应用程序中实现分页逻辑来实现。
  3. 释放资源:确保及时释放使用的资源,如打开的文件、网络连接等。使用try-finally或try-with-resources语句块来确保资源的正常释放,避免资源泄漏导致内存占用不断增加。
  4. 断开对象引用:对于不再使用的对象,及时断开其引用关系,使其成为不可达对象,以便垃圾收集器可以回收其占用的内存。确保对象的生命周期与其引用关系相匹配,避免长时间持有对象引用导致内存泄漏。
  5. 避免使用静态变量持有大对象引用:静态变量的生命周期很长,如果静态变量持有大对象的引用,会导致这些对象无法被垃圾收集器回收。在设计时,尽量避免使用静态变量来持有大对象引用,或者及时将其设置为null,释放引用。
  6. 使用合适的数据结构和算法:对于需要存储大量数据的集合,如List、Map等,可以考虑使用合适的数据结构,如ArrayList替代LinkedList,以减少内存消耗和提高性能。同时,在操作数据时,选择高效的算法,避免不必要的遍历和操作。
  7. 进行内存泄漏检测和分析:使用工具或进行代码审查来检测潜在的内存泄漏问题。可以使用内存分析工具(如Eclipse Memory Analyzer)来分析内存快照,并查找不再使用的对象和可能的泄漏路径。

JVM 问题定位步骤

  1. 查看错误日志,查找蛛丝马迹
  2. 使用阿里的arthas,查看是否有线程阻塞;登陆数据库看看是否有死锁等
  3. 打印推内存日志,JVM 参加上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath= /savaPath,使用工具进行堆分析。

上面介绍的是JVM出问题后,怎么进行问题定位,下面介绍下JVM怎么调优。

JVM调优

以下是一些常用的JVM调优策略:

  1. 调整堆内存大小:通过调整堆内存大小,可以适应应用程序的内存需求。可以使用-Xmx和-Xms选项设置最大堆和初始堆大小。增加堆内存可以减少频繁的垃圾收集,但过大的堆内存也可能导致长时间的垃圾收集暂停。因此,需要根据应用程序的内存需求和系统资源进行适当的调整。
  2. 选择合适的垃圾收集器:JVM提供了多种垃圾收集器,如Serial、Parallel、CMS(Concurrent Mark Sweep)、G1(Garbage First)等。选择合适的垃圾收集器可以根据应用程序的特点和需求来提高垃圾收集性能。例如,对于具有大内存和低延迟要求的应用程序,可以考虑使用G1垃圾收集器。
  3. 调整垃圾收集器参数:对于选择的垃圾收集器,可以根据应用程序的需求进行相应的参数调整。例如,可以调整新生代和老年代的比例、堆的分代大小、垃圾收集的并行度等。
  4. 监控和分析垃圾收集:通过启用JVM的垃圾收集日志,可以监控和分析垃圾收集的行为和性能。垃圾收集日志提供了有关内存使用、垃圾收集时间、内存分配速率等的详细信息。根据日志的分析结果,可以进一步优化垃圾收集器的参数配置。
  5. 优化代码和数据结构:通过优化应用程序的代码和数据结构,可以减少内存消耗和提高执行效率。避免不必要的对象创建和持久化、使用合适的数据结构和算法、及时释放资源等,都可以对JVM的性能产生积极影响。
  6. 并发和线程调优:合理配置JVM的线程池参数,如线程数、队列大小等,以充分利用系统资源。避免线程过多或过少导致的性能问题和资源浪费。同时,可以使用并发集合和锁机制来提高多线程应用程序的性能和稳定性。

堆内存大小调优

jstat -gcutil命令用于监视并输出垃圾收集器的统计信息,包括堆内存使用情况和垃圾收集的相关信息。

以下是jstat -gcutil命令的用法:

jstat -gcutil   br

其中,是Java进程的进程ID,是采样间隔时间(以毫秒为单位),是采样次数。

执行该命令后,会输出类似以下的统计信息:

S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT                      0.00   0.00  27.79  62.88  62.33  1214   21.429  32     5.151   26.580br

其中,S0和S1表示Survivor区的使用情况,E表示Eden区的使用情况,O表示Old区的使用情况,P表示Permanent区的使用情况。YGC表示Young Generation的垃圾收集次数,YGCT表示Young Generation的垃圾收集时间,FGC表示Full Generation的垃圾收集次数,FGCT表示Full Generation的垃圾收集时间,GCT表示总的垃圾收集时间。

  1. S0和S1:表示Survivor区的使用情况。观察这两个值可以判断Survivor区的空间利用率。如果S0和S1的使用率过高,接近100%,则可能存在内存压力,需要调整堆内存大小。
  2. E:表示Eden区的使用情况。观察Eden区的使用率可以判断新生代的空间利用率。如果Eden区的使用率持续高于90%或接近100%,说明新生代的空间不足,需要增加堆内存或调整新生代大小。
  3. O:表示Old区(或Tenured区)的使用情况。观察Old区的使用率可以判断老年代的空间利用率。如果Old区的使用率持续高于90%或接近100%,说明老年代的空间不足,可能需要增加堆内存或调整老年代大小。
  4. P:表示Permanent区(或Metaspace)的使用情况。观察Permanent区的使用率可以判断元数据空间的利用率。如果Permanent区的使用率过高,可能存在大量的类加载、字符串常量等数据,需要增加Metaspace的大小。
  5. YGC和YGCT:分别表示Young Generation的垃圾收集次数和垃圾收集时间。观察YGC和YGCT可以判断新生代垃圾收集的频率和耗时。如果YGC频繁发生且YGCT时间较长,说明新生代的垃圾收集无法跟上对象的分配速度,可能需要调整新生代大小或垃圾收集策略。
  6. FGC和FGCT:分别表示Full Generation的垃圾收集次数和垃圾收集时间。观察FGC和FGCT可以判断老年代垃圾收集的频率和耗时。如果FGC频繁发生且FGCT时间较长,说明老年代的垃圾收集无法有效回收内存,可能需要增加堆内存或调整老年代大小。

另外,Survivor区的空间利用率在不同时间点有不同的值,由于Survivor区是用于存放幸存的对象的中转区域,它们在不断地被回收和重新分配。单个采样点的S0和S1的使用率并不能直接反映Survivor区的空间利用率是否高因此。

要判断Survivor区的空间利用率是否高,可以观察Survivor区的使用情况的变化趋势。以下是一些指标和观察方法:

  1. Survivor区的使用率的波动:观察Survivor区的使用率在一段时间内的变化,如果出现频繁的波动,即由低到高、高到低的变化,可能意味着Survivor区空间不足以容纳幸存的对象,需要调整Survivor区的大小。
  2. 平均使用率:使用多个采样点的S0和S1使用率的平均值来评估Survivor区的平均利用率。如果平均使用率持续高于一定阈值(例如80%或90%),则可能表示Survivor区空间不足。
  3. 使用率的持续高值:观察Survivor区的使用率是否持续高于一定阈值(例如80%或90%)超过一定时间。如果Survivor区的使用率长时间保持在高值附近,可能意味着Survivor区空间不足,需要调整Survivor区的大小。

Eden区 和Survivor区的区别

Eden区和Survivor区都属于Java堆中的新生代,它们是用于对象分配和垃圾收集的不同区域。以下是Eden区和Survivor区的主要区别:

Eden区:

  1. Eden区是新创建对象的初始分配区域,大多数对象最初都被分配在Eden区。
  2. Eden区是一个较大的内存空间,在对象分配时,先尝试在Eden区分配内存。
  3. 当Eden区无法容纳新创建的对象时,会触发一次Minor GC(新生代垃圾收集),将存活的对象复制到Survivor区或者Old区。
  4. 在Minor GC后,Eden区中未被回收的对象将被清除,空间将被重新分配给新的对象。

Survivor区:

  1. Survivor区是用于存放在Eden区进行了一次Minor GC后仍然存活的对象。
  2. Survivor区由两个大小相等的区域组成,通常被称为S0和S1(或From区和To区)。
  3. 在进行Minor GC时,存活的对象会被从Eden区复制到其中一个Survivor区,而另一个Survivor区是空的。
  4. 在下一次Minor GC时,存活的对象将被从当前的Survivor区和Eden区复制到另一个空的Survivor区。
  5. 这样,Survivor区中的对象会在两个区域之间进行交换,每次Minor GC都会将存活的对象复制到空的Survivor区

垃圾收集器选择

选择适合的垃圾收集器取决于多个因素,包括应用程序的性质、内存需求、吞吐量要求和响应时间要求等。以下是一些常见的垃圾收集器及其适用场景的概述:

Serial收集器:

  1. 单线程的垃圾收集器,只使用一个线程进行垃圾收集。
  2. 适用于小型或简单的应用程序,对吞吐量要求不高,更注重最小化垃圾收集的停顿时间。

Parallel收集器:

  1. 使用多个线程进行垃圾收集,可充分利用多核处理器的优势。
  2. 适用于吞吐量要求较高的应用程序,可以在减少垃圾收集停顿时间的同时,提高垃圾收集的吞吐量。

CMS(Concurrent Mark Sweep)收集器:

  1. 采用并发标记和并发清除的方式,减少垃圾收集的停顿时间。
  2. 适用于对响应时间要求较高的应用程序,通过减少垃圾收集停顿时间来提高应用程序的响应性能。

G1(Garbage-First)收集器:

  1. 基于分区的垃圾收集器,将Java堆划分为多个大小相等的区域。
  2. 适用于大内存应用程序,具有较高的吞吐量要求和可控制的停顿时间。

选择垃圾收集器时,可以参考以下进行决定:

  1. 分析应用程序的性质和需求,包括内存需求、吞吐量要求和响应时间要求等。
  2. 根据应用程序的性质和需求,选择适合的垃圾收集器。可以通过查看JVM文档和垃圾收集器的特性来了解每个收集器的优缺点。
  3. 运行应用程序时,可以使用命令行参数或JVM配置来指定所选的垃圾收集器。
  4. 监控和调整垃圾收集器的参数,以优化垃圾收集的性能和内存利用。

特别注意,不同的垃圾收集器适用于不同的场景,并且某些垃圾收集器可能需要额外的配置和调优才能发挥最佳性能。因此,在选择和配置垃圾收集器时,建议根据具体的应用程序需求和性能特点进行测试和调优,以找到最适合的垃圾收集策。

下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/14914,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?