Java JVM 调优总结与思考
主题 1:JVM 参数分类
HotSpot JVM 提供了三类参数
标准参数
顾名思义,标准参数中包括功能和输出的参数都是很稳定的,很可能在将来的JVM版本中不会改变。你可以用java命令(或者是用 java -help)检索出所有标准参数
X参数
非标准化的参数在将来的版本中可能会改变。所有的这类参数都以-X开始,并且可以用java -X来检索。注意,不能保证所有参数都可以被检索出来,其中就没有-Xcomp。
XX参数
在实际情况中X参数和XX参数并没有什么不同。X参数的功能是十分稳定的,然而很多XX参数仍在实验当中(主要是JVM的开发者用于debugging和调优JVM自身的实现)。
对于布尔类型的参数,我们有”+”或”-“,然后才设置JVM选项的实际名称。例如,-XX:+name用于激活name选项,而-XX:-name用于禁用name选项。 对于需要非布尔值的参数,如string或者integer,我们先写参数的名称,后面加上”=”,最后赋值。例如, -XX:name=value给name赋值value。
主题 2:解释器与编译器优化
概述
-
在 Java 程序中,编译成 class 字节码文件后,当程序需要快速启动和执行的时候,解释器会首先发挥作用,立即执行。
-
当虚拟机发现某个方法或者代码块运行特别频繁时,就会把这些代码看作是“热点代码”(Hotspot code,这也是Hotspot VM的来由),将这些代码编译成与本地平台相关的机器码,并进行各个层次的优化。负责这个过程的编译器称为即时编译器(JIT)
-
HotSpot 虚拟机中内置了两个JIT编译器,分别为 Client Compiler 和 Server Compiler,也称为 C1 编译器和 C2 编译器。程序使用哪个编译器,取决于虚拟机的运行模式,运行模式默认与宿主机器的硬件性能和版本有关。也可以通过-server 和 -client 参数来指定。
模式选择
-
混合模式(Mixed):解释器和编译器混合搭配的方式,默认开启,可以使用 -Xmixed 参数手动开启
-
解释模式(Interpreted):用户可以使用 -Xint 强制不使用编译器,此时全部的代码都是使用解释方式执行
-
编译模式(Compiled):用户可以使用 -Xcomp 强制执行改模式,这时将优先使用编译方式执行,但是编译无法执行的情况下,还是要使用解释器执行
通常的情况下,解释器可以提高程序的启动速度,而编译器可以提高程序运行效率,而混合模式刚好两者的一种平衡。
主题 3:内存调优
堆内存是按照新生代、老年代和永久代这一经典策略划分的。
-Xms 和 -Xmx
-
-Xms和-Xmx可以说是最常用的JVM参数,它们可以允许我们指定JVM的初始堆和最大堆内存大小。一般来说,这两个参数的数值单位是Byte,但同时它们也支持使用速记符号,比如“k”或者“K”代表“kilo”,“m”或者“M”代表“mega”,“g”或者“G”代表“giga”。如
java -Xms128m -Xmx2g
-
-Xms和-Xmx实际上是-XX:InitialHeapSize和-XX:MaxHeapSize的缩写。我们也可以直接使用这两个参数,它们所起得效果是一样的,如
java -XX:InitialHeapSize=128m -XX:MaxHeapSize=2g
-XX:PermSize and -XX:MaxPermSize
永久代在堆内存中是一块独立的区域,它包含了所有JVM加载的类的对象表示。为了成功运行应用程序,JVM会加载很多类(因为它们依赖于大量的第三方库,而这又依赖于更多的库并且需要从里面将类加载进来)这就需要增加永久代的大小。我们可以使用-XX:PermSize 和-XX:MaxPermSize 来达到这个目的。其中-XX:MaxPermSize 用于设置永久代大小的最大值,-XX:PermSize 用于设置永久代初始大小。如 java -XX:PermSize=128m -XX:MaxPermSize=256m
请注意,这里设置的永久代大小并不会被包括在使用参数-XX:MaxHeapSize 设置的堆内存大小中。也就是说,通过-XX:MaxPermSize设置的永久代内存可能会需要由参数 -XX:MaxHeapSize 设置的堆内存以外的更多的一些堆内存。
-XX:+HeapDumpOnOutOfMemoryError and -XX:HeapDumpPath
- -XX:+HeapDumpOnOutOfMemoryError 让JVM在发生内存溢出时自动的生成堆内存快照。有了这个参数,当我们不得不面对内存溢出异常的时候会节约大量的时间。默认情况下,堆内存快照会保存在JVM的启动目录下名为java_pid[pid].hprof 的文件里(在这里[pid]就是JVM进程的进程号)。也可以通过设置-XX:HeapDumpPath=path来改变默认的堆内存快照生成路径,path可以是相对或者绝对路径。
-XX:InitialCodeCacheSize and -XX:ReservedCodeCacheSize
JIT 编译器编译的本地代码存储在“代码缓存”中,这是一个特殊的内存区域,如果代码缓存被占满,JVM会打印出一条警告消息,并切换到interpreted-only 模式:JIT编译器被停用,字节码将不再会被编译成机器码。因此,应用程序将继续运行,但运行速度会降低一个数量级。相关的参数是-XX:InitialCodeCacheSize 和-XX:ReservedCodeCacheSize,它们的参数和上面介绍的参数一样,都是字节值。
-XX:+UseCodeCacheFlushing
如果代码缓存不断增长,例如,因为热部署引起的内存泄漏,那么提高代码的缓存大小只会延缓其发生溢出。为了避免这种情况的发生,我们可以尝试一个有趣的新参数:当代码缓存被填满时让JVM放弃一些编译代码。通过使用-XX:+UseCodeCacheFlushing 这个参数,我们至少可以避免当代码缓存被填满的时候JVM切换到interpreted-only 模式。不过,我仍建议尽快解决代码缓存问题发生的根本原因,如找出内存泄漏并修复它。
主题 4:新生代 GC 调优
为什么要有新生代?
- 很多对象的生存状态都很短
- 新生对象很少引用生存时间长的对象
鉴于以上两个特点,如果每次 GC 都针对整个堆进行,时间将会特别长,程序整体性能就会下降(gc 是一个后台线程)。
把内存划分为新生代和老年代:
- 简化了新对象的分配(只在新生代分配内存)
- 可以更有效的清除不再需要的对象(即死对象)(新生代和老年代使用不同的GC算法)
新生代的特点
SUN/Oracle 的HotSpot JVM 又把新生代进一步划分为3个区域:一个相对大点的区域,称为”伊甸园区(Eden)”;两个相对小点的区域称为”From 幸存区(survivor)”和”To 幸存区(survivor)”。按照规定,新对象会首先分配在 Eden 中(如果新对象过大,会直接分配在老年代中)。在GC中,Eden 中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代。
基于大多数新生对象都会在GC中被收回的假设。新生代的GC 使用复制算法。在GC前To 幸存区(survivor)保持清空,对象保存在 Eden 和 From 幸存区(survivor)中,GC运行时,Eden中的幸存对象被复制到 To 幸存区(survivor)。针对 From 幸存区(survivor)中的幸存对象,会考虑对象年龄,如果年龄没达到阀值(tenuring threshold),对象会被复制到To 幸存区(survivor)。如果达到阀值对象被复制到老年代。复制阶段完成后,Eden 和From 幸存区中只保存死对象,可以视为清空。如果在复制过程中To 幸存区被填满了,剩余的对象会被复制到老年代中。最后 From 幸存区和 To幸存区会调换下名字,在下次GC时,To 幸存区会成为From 幸存区。
-
对象优先在 eden 区分配
-
大对象直接进入老年代
-
长期存活的对象将进入老年代
-
对象动态年龄绑定
-XX:NewSize and -XX:MaxNewSize
和 -Xms 和 -Xmx 指定堆内存大小一样,-XX:NewSize 和 -XX:MaxNewSize 可以指定新生代的大小。
设置 XX:MaxNewSize 参数时,应该考虑到新生代只是整个堆的一部分,新生代设置的越大,老年代区域就会减少。一般不允许新生代比老年代还大,因为要考虑GC时最坏情况,所有对象都晋升到老年代。 -XX:MaxNewSize 最大可以设置为-Xmx/2 .
-XX:NewRatio
可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与新生代的比例。例如 -XX:NewRatio=3 指定老年代/新生代为3/1. 老年代占堆大小的 3/4 ,新生代占 1/4 .
如果针对新生代,同时定义绝对值和相对值,绝对值将起作用。
-XX:SurvivorRatio
参数 -XX:SurvivorRatio 与 -XX:NewRatio 类似,作用于新生代内部区域。-XX:SurvivorRatio 指定伊甸园区(Eden)与幸存区大小比例. 例如, -XX:SurvivorRatio=10 表示伊甸园区(Eden)是 幸存区To 大小的10倍(也是幸存区From的10倍).所以,伊甸园区(Eden)占新生代大小的10/12, 幸存区From和幸存区To 每个占新生代的1/12 .注意,两个幸存区永远是一样大的..
设定幸存区大小有什么作用? 假设幸存区相对伊甸园区(Eden)太小, 相应新生对象的伊甸园区(Eden)永远很大空间, 我们当然希望,如果这些对象在GC时全部被回收,伊甸园区(Eden)被清空,一切正常.然而,如果有一部分对象在GC中幸存下来, 幸存区只有很少空间容纳这些对象.结果大部分幸存对象在一次GC后,就会被转移到老年代 ,这并不是我们希望的.考虑相反情况, 假设幸存区相对伊甸园区(Eden)太大,当然有足够的空间,容纳GC后的幸存对象. 但是过小的伊甸园区(Eden),意味着空间将越快耗尽,增加新生代GC次数,这是不可接受的。
-XX:InitialTenuringThreshold, -XX:MaxTenuringThreshold and -XX:TargetSurvivorRatio
通过 -XX:InitialTenuringThreshold 和 -XX:MaxTenuringThreshold 可以设定老年代阀值的初始值和最大值。另外,可以通过参数 -XX:TargetSurvivorRatio 设定幸存区的目标使用率.例如 , -XX:MaxTenuringThreshold=10 -XX:TargetSurvivorRatio=90 设定老年代阀值的上限为10,幸存区空间目标使用率为90%。
- 如果从年龄分布中发现,有很多对象的年龄持续增长,在到达老年代阀值之前。这表示 -XX:MaxTenuringThreshold 设置过大
- 如果 -XX:MaxTenuringThreshold 的值大于1,但是很多对象年龄从未大于1.应该看下幸存区的目标使用率。如果幸存区使用率从未到达,这表示对象都被GC回收,这正是我们想要的。 如果幸存区使用率经常达到,有些年龄超过1的对象被移动到老年代中。这种情况,可以尝试调整幸存区大小或目标使用率。
-XX:+NeverTenure and -XX:+AlwaysTenure
对应2种极端的新生代GC情况.设置参数 -XX:+NeverTenure , 对象永远不会晋升到老年代.当我们确定不需要老年代时,可以这样设置。这样设置风险很大,并且会浪费至少一半的堆内存。相反设置参数 -XX:+AlwaysTenure, 表示没有幸存区,所有对象在第一次GC时,会晋升到老年代。