JVM性能调优

本文详细介绍了JVM内存结构,包括堆、方法区、栈等,并讲解了垃圾回收算法及调优实践,帮助读者理解JVM内存管理。

推荐GameKing的JVM学习笔记系列 和 cprime,更加细致


0.JVM体系结构简介

JVM Specification中的JVM整体架构

  主要包括两个子系统和两个组件,Class Loader(类装载)子系统,Execution Engine(执行引擎)子系统,Runtime Data Area(运行时数据区)组件,Native Interface(本地接口)组件。

  Class loader 子系统的作用 根 据给定的全限定名类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area 中的method area(方法区域)。Java 程序员可以extends java.lang.ClassLoader 类来写自己的Class loader。

  Execution engine 子系统的作用 执 行 classes中的指令。任何 JVM specification实现(JDK)的核心是Execution engine, 换句话说:Sun 的JDK 和IBM的JDK好坏主要取决于他们各自实现的Execution  engine的好坏。每个运行中的线程都有一个 Execution engine的实例。 

  Native interface 组件 与native libraries 交互,是其它编程语言交互的接口。 

  Runtime data area 组件:这个组件就是 JVM中的内存。

 

Runtime data area 的整体架构图

  Runtime data area 主要包括五个部分:Heap (堆), Method Area(方法区域), Java Stack(java 的栈), Program Counter(程序计数器), Native method stack(本地方法栈)。Heap 和Method Area 是被所有线程的共享使用的;而Java stack, Program counter 和 Native method stack 是以线程为粒度的,每个线程独自拥有。

Heap 

   Java程序在运行时创建的所有类实例或数组都放在同一个堆中。而一个Java虚拟实例中只存在一个堆空间,因此所有线程都将共享这个堆。每一个 java程序独占一个JVM实例,因而每个 java程序都有它自己的堆空间,它们不会彼此干扰。但是同一java程序的多个线程都共享着同一个堆空间,就得考虑多线程访问对象(堆数据)的同步问 题。(这里可能出现的异常 java.lang.OutOfMemoryError: Java heap space) 

Method area 

   在Java 虚拟机中,被装载的 class的信息存储在 Method area的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的 class文件,然后读入这个class文件内容并把它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。该类型中的类(静 态)变量同样也存储在方法区中。与Heap 一样,method area 是多线程共享的,因此要考虑多线程访问的同步问题。比如,假设同时两个线程都企图访问一个名为 Lava的类,而这个类还没有内装载入虚拟机,那么,这时应该只有一个线程去装载它,而另一个线程则只能等待。 (这里可能出现的异常 java.lang.OutOfMemoryError: PermGen full)

Java stack 

   Java stack 以帧为单位保存线程的运行状态。虚拟机只会直接对 Java stack执行两种操作:以帧为单位的压栈或出栈。每当线程调用一个方法的时候,就对当前状态作为一个帧保存到 java stack 中(压栈);当一个方法调用返回时,从java stack 弹出一个帧(出栈)。栈的大小是有一定的限制,这个可能出现StackOverFlow 问题,例如递归的层数太深。

Program counter  

  每个运行中的Java程序,每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。  

Native method stack  

   对于一个运行中的Java程序而言,它还能会用到一些跟本地方法相关的数据区。当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限 制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,不止如此,它还可以做任何它想做的事情。比如,可以调用寄存器,或在操作系统中分配 内存等。总之,本地方法具有和JVM 相同的能力和权限。  (这里出现 JVM无法控制的内存溢出问题 native heap OutOfMemory ) 。

Sun JVM 中对 JVM Specification 的实现(内存部分)   

   JVM Specification只是抽象的说明了 JVM 实例按照子系统、内存区、数据类型以及指令这几个术语来描述的,  但是规范并非是要强制规定 Java 虚拟机实现内部的体系结构,更多的是为了严格地定义这些实现的外部特征。  Sun JVM 实现中:Runtime data area(JVM  内存)  五个部分中的 Java Stack , Program Counter, Native method stack 三部分和规范中的描述基本一致;但对 Heap  和  Method Area 进行了自己独特的实现。这个实现和 Sun JVM  的Garbage collector(垃圾回收)机制有关。  

垃圾分代回收算法(Generational Collecting)  

  基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。 

如上图所示,为Java 堆中的各代分布。  

   1. Young(年轻代)JVM specification 中的  Heap的一部分。年轻代分三个区。一个Eden区,两个 Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到 Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个 Survivor区,当这个 Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到年老区(Tenured)。需要注 意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个 Survivor复制过来的对象,而复制到年老区的只有从第一个 Survivor 去过来的对象。而且,Survivor 区总有一个是空的。  

  2. Tenured(年老代)JVM specification中的  Heap的一部分。年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。

   3. Perm(持久代)  JVM specification 中的  Method area 用于存放静态文件,如 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。


1.内存分代

JVM的内存分代管理结构:

JVM Tunning Practice(1) - 内存分代 - Harry - 染出一道彩虹

下面是一些需要关注的常用的JVM内存配置参数,我们来看看它们是如何影响上图中的比例的。

1)Heap Size

-Xmx ---最大Heap Size,即上图的Total size(包括Eden+form+to,Tenured,不包含Perm,见上图),限制了年轻代和年老代的可分配最大值;

-Xms ---初始化分配的Heap Size

生产环境中ms一般设置成跟mx相等,因为若ms不等于mx那么在某些场景下JVM可能需要对Heap Size进行频繁的扩展和收缩,增加处理时间;

2)New/Young Generation Size

-Xmn ---最大年轻代大小,即上图中的Eden+S0+S1+Virtual

-XX:NewSize ---初始化年轻代大小,即上图中的Eden+S0+S1,在只设置了-Xmn不设置-XX:NewSize的情况下,NewSize等于mn。

生产环境中一般只需设置-Xmn或者设置mn和NewSize相等,理由和HeapSize的设置一样,避免容量震荡消耗资源;

3)Old Generation Size (Tenured)

-XX:NewRatio --- Old Size/New Size,通过年老代和年轻代的比例和Heap Size就可以算出年老代的大小。一般默认为8,若Heap Size为1024m,则 NewSize=HeapSize/(NewRatio+1)=114m,OldSize=HeapSize-NewSize=910m;

注意:-Xmn的优先级比-XX:NewRatio高,若-Xmn已指定,则OldSize=HeapSize-NewSize,无需再按比例计算。生产环境中一般只需指定-Xmn就足够了。

4)Eden和S0、S1

-XX:SurvivorRatio --- Eden/S0,即 Eden区和S0的比例,默认为8,若NewSize为114m,则S0=NewSize/(SurvivorRatio+2)=11.4m;

S0==S1,S0、S1的职能是一模一样的,又叫做From space和To space,在每一次minor gc后角色会交换。

注意:-XX类型的选项在不同的JDK版本或实现中定义可能有所区别,在近日的实践中发现,

在Linux jdk_1_5_0_10_x86版本中,SurvivorRatio=(YoungSize/S0),而Linux jdk_1_5_0_20_x64版本中,SurvivorRatio=(Eden/S0)

所以,我们在实际的工程实践中还是应该用jmap -heap输出的jvm内存结构信息为准,不要想当然。

5)Permanent Generation Size

-XX:MaxPermSize ---最大持久代大小,默认为64m;

-XX:PermSize ---初始化持久代大小,默认为16m;

生产环境中一般设置MaxPermSize和PermSize相等,理由和HeapSize的设置一样,避免容量震荡消耗资源;

当应用引用的类比较多或者应用了一些动态类生产技术时应该加大该区的值,一般256m对服务器程序都很足够了。


下面是一些JVM对Native Memory内存的使用:

6)Thread Stack Size

-Xss ---线程堆栈大小,一般用于存放方法入口参数和返回值,以及原子类型的本地变量(即方法内部变量);

一般可设置为128k.

7)Direct Memory Size

-XX:MaxDirectMemorySize ---direct byte buffer用到的本地内存,在本blog的一篇《xsocket内存泄漏》文章中介绍过该参数的作用。默认跟mx相等,所以生产环境中一般不设置mx大于物理内存的一半。

2.GC过程  

在讲述GC过程前我先解释一下JVM的两个控制参数:

-XX:TargetSurvivorRatio --- Survivor Space最大使用率,若存放对象的总大小超过该值,将引起对象向Old区迁移;

-XX:MaxTenuringThreshold --- Young区对象的最大任期阀值,即可经历minor gc的次数,超过该次数的对象直接迁移到Old区;

实际的TenuringThreshold由JVM通过Survivor Space的占用率和TargetSurvivorRatio动态计算,详情请查看参考资料。

《HP-MemoryManagement.pdf》中有对JVM GC过程的形象描述,我借用其中的一些图例来说明一下。

1)Heap在初始状态

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

2)在Eden存放新对象

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

3)Eden空间不足分配新对象,进行第一次minor gc

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

4) Eden区再次被写满,进行第二次minor gc

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

5)Eden再次被写满,进行第3次minor gc

第3次gc,发生了对象从from space提升到old区的迁移,然后也发生了from space到to space的copy

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

以下是Survivor space空间不足但对象的minor gc次数未到达MaxTenuringThreshold时的gc情况:

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC过程 - Harry - 染出一道彩虹

3.GC实战  

在进行GC Tuning时有两个很强大的利器:

jstat:用于查看某java进程的gc情况;

jmap:查看java进程堆栈分配和使用情况,以及dump出当前堆栈内容(可以用Eclipse MAT进行进一步分析)

以上两个利器都是jdk自带,且无需java进程添加任何额外的debug信息输出参数的,直接就可以对任意java进程进行跟踪了。


       我认为GC调优的整体目标是要减缓GC的总体时间增加和降低每次gc引起的应用停顿(大概就是每次执行gc消耗的时间)。但往往我们只能在两者间获取一个平衡,在吞吐量和应用暂停时间之间取得一个平衡。

JVM Tunning Practice(2) - GC Tunning - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC Tunning - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC Tunning - Harry - 染出一道彩虹

调优前配置:-Xmx1024m -Xms1024m

调优前gc情况:jstat -gcutil <pid> 3000

minor gc: 3~6次/3秒

full gc: 1次/30秒


调优后配置:-Xmx2g -Xms2g -Xmn1g -Xss128k -XX:NewSize=1g -XX:PermSize=128m

-XX:MaxPermSize=256m -XX:SurvivorRatio=8 -XX:TargetSurvivorRatio=60 

-XX:MaxTenuringThreshold=20 -XX:+UseParNewGC -XX:ParallelGCThreads=8 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0

调优后gc情况:

minor gc: 1次/15秒

full gc: 1次/数小时到数十小时


UseParNewGC表示对新生代采用并行gc;

ParallelGCThreads表示并行的线程数为8,一般是cpu的核个数,当核个数大于8时可能不是很适用;

UseConcMarkSweepGC表示对full gc采用CMS gc;


另外还有几个跟GC有关的有用参数,这里没有用到:

-XX:+DisableExplicitGC 表示禁止显式gc,System.gc()

-XX:+UseCMSCompactAtFullCollection 适用于CMS gc,表示在进行gc的同时清理内存碎片,但会加长gc的总时间

-XX:CMSInitiatingOccupancyFraction=80 适用于CMS gc,表示在年老代达到80%使用率时马上进行回收


另外下面是在JVM Crash时获heap信息的一些配置参数:

-XX:ErrorFile=./xxx.log   JVM Crash时记录heap信息

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./yyy.log JVM OOM时记录heap信息

拿到heap文件后可以用Eclipse MAT进行分析,找出引起内存泄漏的class。


参考资料:

1)HP-MemoryManagement.pdf

2)http://www.51testing.com/?uid-77492-action-viewspace-itemid-203728

3)http://sunqi.javaeye.com/blog/486048

4)http://fallenlord.blogbus.com/logs/57543373.html


转载于:https://my.oschina.net/OQKuDOtsbYT2/blog/200790

<think>我们正在讨论JVM性能,根据用户的问题,他想要获取JVM性能的相关信息,包括方法和技巧。同时,用户提供了几个引用,但这些引用是关于JVM性能的概述性内容,我们需要结合这些引用以及我们对JVM的知识来回答。 注意:用户之前的问题是关于Java类加载机制,现在转向了JVM性能,因此我们要专注于部分。 回答结构: 1. 引言:简要介绍JVM性能的重要性。 2. 目标:明确的目标指标(如吞吐量、延迟、内存占用等)。 3. 方法:详细描述的常用方法和技巧(包括参数配置、监控工具、代码化等)。 4. 实战步骤:给出一个典型的步骤(参考引用[4]中的步骤)。 5. 注意事项:中需要注意的问题。 6. 总结。 同时,根据要求,我们需要在回答末尾生成相关问题。 由于引用中提供了一些信息,我们可以适当引用,但主要依靠我们的知识库来构建详细内容。 注意:引用标记[^1]、[^2]等需要放在引用内容的后面,并且自然。 </think>### JVM性能方法技巧详解 JVM性能是提升Java应用效率的关键,核心目标包括降低GC频率、减少STW时间、化内存使用和提高吞吐量。以下是系统化的方法和实战技巧: --- #### 一、核心目标 1. **内存化** - 减少堆内存碎片和Full GC次数 - 平衡新生代(Young Gen)和老年代(Old Gen)比例 - 示例参数: ```bash -Xms4g -Xmx4g # 堆内存初始值=最大值,避免动态扩容开销 -XX:NewRatio=2 # 新生代:老年代=1:2 ``` 2. **延迟化** - 缩短GC暂停时间(STW),尤其是CMS/G1的并发阶段 - 关键指标:`GC Pause Time < 200ms`(视业务敏感度整) 3. **吞吐量化** - 提高应用有效工作时间占比 - 目标:`Throughput = (总运行时间 - GC时间) / 总运行时间 > 95%` [^4] --- #### 二、方法技巧 ##### 1. **内存参数** | 参数 | 作用说明 | 场景 | |---------------------|----------------------------------|----------------------------| | `-XX:MaxTenuringThreshold` | 对象晋升老年代年龄阈值 | 减少过早晋升导致的Full GC | | `-XX:SurvivorRatio=8` | EdenSurvivor区比例(默认8:1:1)| 对象存活率高的应用 | | `-XX:+UseG1GC` | 启用G1垃圾收集器 | 大堆(>4GB)或低延迟需求 | **实战技巧**: - 若Minor GC频繁,增加`-Xmn`(新生代大小) - 若Full GC频繁,增大老年代或降低`-XX:MaxTenuringThreshold` ##### 2. **GC日志分析监控** - **启用详细GC日志**: ```bash -XX:+PrintGCDetails -Xloggc:/path/to/gc.log ``` - **关键分析工具**: - `jstat -gcutil <pid> 1000`:实时监控GC各分区使用率 - `jmap -dump:format=b,file=heapdump.hprof <pid>`:生成堆转储文件 - **可视化工具**:GCViewer、MAT(Memory Analyzer)分析内存泄漏[^3] ##### 3. **垃圾收集器选择策略** | 收集器 | 适用场景 | 重点 | |----------------|----------------------------------|----------------------------| | **Parallel GC** | 高吞吐量应用(如批处理) | `-XX:ParallelGCThreads` | | **CMS** | 低延迟Web服务(JDK8及之前) | `-XX:CMSInitiatingOccupancyFraction` | | **G1** | 大内存+均衡吞吐/延迟(JDK9+默认) | `-XX:MaxGCPauseMillis=200` | > **案例**:某电商平台通过G1替换CMS,将99% GC暂停从500ms降至50ms[^3]。 ##### 4. **代码层化** - **减少短命对象**:避免循环内创建临时对象 - **软/弱引用管理**:缓存使用`WeakHashMap`防止内存泄漏 - **堆外内存控制**:Netty等框架需限制`-XX:MaxDirectMemorySize` --- #### 三、标准化流程 [^4] 1. **基线分析**:无参数运行,采集GC日志和性能指标 2. **瓶颈定位**: - 高频Full GC → 检查老年代内存或对象泄漏 - 长暂停时间 → 化GC算法或整`MaxGCPauseMillis` 3. **参数迭代**:每次仅修改1-2个参数,对比测试 4. **监控验证**:使用APM工具(如SkyWalking)跟踪后效果 --- #### 四、注意事项 1. **避免过度**:化代码和架构,JVM参数为最后手段 2. **版本差异**:JDK11+的ZGC/Shenandoah适用于超低延迟场景(暂停<10ms) 3. **监控持续性**:生产环境需持续监控`jstat`和GC日志[^2] > **本质**:在内存、吞吐量、延迟三者间寻找平衡点,公式表达: > $$\text{系统效率} = \frac{\text{有效计算时间}}{\text{总时间}} = 1 - \frac{\text{GC时间} + \text{其他开销}}{\text{总时间}}$$ --- ### 总结 JVM需结合监控数据、GC日志和业务场景迭代推进。先通过`jstat`/堆转储定位瓶颈,再针对性整内存分区或GC算法。对于多数应用,G1+合理堆大小配置即可满足需求,复杂场景可探索ZGC[^4]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值