代码改变世界

JVM分析及调优总结-干货

2020-07-28 12:12  ☆野生架构师☆  阅读(987)  评论(0编辑  收藏  举报

零、JAVA虚拟机

  JRockit VM是Oracle公司的虚拟机

  HotSpot VM是Sun公司的虚拟机

  Oracle在JDK8中完成整合JRockit和HotSpot两大虚拟机。整合的方式是在HotSpot的基础上,移植JRockit的优秀特性。 

 

  IBM JVM是IBM公司的虚拟机

  IBM Java虚拟机OpenJ9正式开源,贡献给 Eclipse 基金会管理,所以又叫 eclipse openj9

  经过测试Openj9在内存管理上已经超过HotSpot,降低了50%左右的内存,但在密集运算上某些稍逊于HotSpot。

  (注:这是真实的笔者已经在项目中使用openj9打包docker镜像使用)

  这是园子里兄弟做的测试,可以参考下:https://www.cnblogs.com/muzishanhe/p/13218476.html 

 

一、JVM空间说明

 基本概念:

      JVM把内存区分为堆区(heap)、栈区(stack)和方法区(method)。由于本文主要讲解JVM调优,因此我们可以简单的理解为,JVM中的堆区中存放的是实际的对象,是需要被GC的。其他的都无需GC。

 JVM的内存模型:

 

 从图中可以看到,

  1、JVM实质上分为三大块,年轻代(YoungGen),年老代(Old Memory),及持久代(Perm,在Java8中被取消,我们不做深入介绍)。

  2、垃圾回收GC,分为2种,一是Minor GC,可以可以称为YGC,即年轻代GC,当Eden区,还有一种称为Major GC,又称为FullGC。

  3、GC原理:

    我们可以看到年轻代包括Eden区(对象刚被new出来的时候,放到该区),S0和S1,是幸存者1区和幸存者2区,从名字可以看出,是当发生YGC,没有被任何其他对象所引用的对象将会从内存中被清除,还被其他对象引用的则放到幸存者区。当发生多次YGC,在S0、S1区多次没有被清楚的对象,则会被移到老年代区域。当老年代区域被占满的时候,则会发送FullGC。

    无论是YGC或是FullGC,都会导致stop-the-world,即整个程序停止一些事务的处理,只有GC进程允许以进行垃圾回收,因此如果垃圾回收时间较长,部分web或socket程序,当终端连接的时候会报connetTimeOut或readTimeOut异常,

  4、从JVM调优的角度来看,我们应该尽量避免发生YGC或FullGC,或者使得YGC和FullGC的时间足够的短。

参数介绍:

  1、在JDK1.7及以前,HotSpot虚拟机将java类信息、常量池、静态变量、即时编译器编译后的代码等数据,存储在Perm(永久代)里(对于其他虚拟机如BEA JRockit、IBM J9等是不存在永久带概念的),类的元数据和静态变量在类加载的时候被分配到Perm里,当常量池回收或者类被卸载的时候,垃圾收集器会回收这一部分内存,但效果不太理想。

  2、JDK1.8时,HotSpot虚拟机对JVM模型进行了改造,将类元数据放到了本地内存中,将常量池和静态变量放到了Java堆里,HotSpot VM 将会为类的元数据明确的分配与释放本地内存。

  在这种架构下,类元数据就突破了-XX:MaxPermSize的限制,所以此配置已经失效,现在可以使用更多的本地内存。这样一定程度上解决了原来在运行时生成大量的类,从而经常Full GC的问题——如运行时使用反射、代理等。

总结:

  1、可以发现最明显的一个变化是元数据空间从虚拟机转移到了本地内存。默认情况下,元数据空间大小仅受限于本地内存, 这意味着以后不会因为永久代大小不够而抛出OOM异常了。

  2、JDK1.8以前,HotSpot VM将class和类的jar包数据存储在PermGen里, PermGen大小是固定的,而且项目之间无法公用公有的class,所以很容易碰到OOM异常。

  3、改成MateSpace后, 各个项目会共享同样的class空间。比如多个项目都引用了apache-common包, 在MateSpace中只会存储一份的apache-common的class,提高了内存的利用率,垃圾回收更有效。

调优:  

  对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。 

  1.Full GC 

    会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。 

  2.导致Full GC的原因 

    1)年老代(Tenured)被写满 

      调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。 

    2)持久代Pemanet Generation空间不足 

      增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例 

    3)System.gc()被显示调用 

      垃圾回收不要手动触发,尽量依靠JVM自身的机制 

  在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。 

二、GC(Garbage Collection)算法

在JVM里的内存空间,从大的层面划分,主要有新生代空间(Young)和老年代空间(Old),其中Young空间,又被分为2个部分和3个板块,分别是1个Egen区,和2个Survivor区S0和S1,看下图:

 

 

OK,下面来具体看下,每部分都是干啥的 
(1)Eden区域是用来存放使用new或者newInstance等方式创建的对象,默认都是存放在Eden区,除非这个对象太大,或者超过了设定的阈值-XX:PretenureSizeThresold,这样的对象会被直接分配到Old区域。 

(2)2个Survivor(幸存)区,一般称S0,S1,理论上他们是一样大的,解释一下,他们是如何工作的: 
<1> 在不断创建对象的过程中,Eden区会满,这时候会开始做Young G也叫Minor GC。过程:找出Eden区中,幸存活着的对象,然后将这些对象,放到S0,或S1区中的其中一个, 假设第一次选择了S0,它会逐步将活着的对象拷贝到S0区域,但是如果S0区域满了,剩下活着的对象只能放old区域了,然后将Eden区域 清空,此时S1区域是空的。 


<2> 当第二次Eden区域满的时候,就将Eden区域中活着的对象 + S0区域中活着的对象,迁移到S1中,如果S1放不下,就会将剩下的部分,放到Old区域中,只是这次对象来源区域增加了S0,最后会将Eden区+S0区域,清空。

注:【原理上随时保持S0和S1有一个是空的,用来存下一次的对象】

<3> 第三次和第四次依次类推,始终保证S0和S1有一个是空的,用来存储临时对象,用于交换空间的目的。除了放不下放到old区域的,反复多次没有被淘汰的对象,将会放入old区域中,默认是15次。具体的交换过程就和上图中的信息相似。

<4> 直到Old区也快满的时候,Eden区也快满的时候,会对整个这一块内存区域进行一次大清洗(FullGC),腾出内存,为之后的对象创建,程序运行腾地方。

注:

  新生代GC(Minor GC):指发生在新生代的垃圾回收动作,因为java对象大多具备朝生夕灭的特征,所以Minor GC发生的特别频繁, 一般回收速度也很快。 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,至少会伴随一次的MinorGC(但非绝对, 在Parallel Scavenge收集器的收集策略里就有直接进行Minor GC的策略选择过程)。Major GC的速度一般比Minor GC慢。

三、JVM参数配置

Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。

Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。

Xss 是指设定每个线程的堆栈大小。这个就要依据你的程序,看一个线程大约需要占用多少内存,可能会有多少线程同时运行等。

以上三个参数的设置都是默认以Byte为单位的,也可以在数字后面添加[k/K]或者[m/M]来表示KB或者MB。而且,超过机器本身的内存大小也是不可以的,否则就等着机器变慢而不是程序变慢了。

-Xms 为jvm启动时分配的内存,比如-Xms200m,表示分配200M
-Xmx 为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存
-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

1、在jdk1.8以前,生产环境一般有如下配置

-XX:PermSize=512M -XX:MaxPermSize=1024M

表示在JVM里存储Java类信息,常量池和静态变量的永久代区域初始大小为512M,最大为1024M。在项目启动后,这个值是固定的,如果项目class过多,很可能遇到OutOfMemoryError: PermGen异常。

2、升级JDK1.8后,上面的perm配置已经变成

-XX:MetaspaceSize=512M XX:MaxMetaspaceSize=1024M

MetaspaceSize如果不做配置,通过jinfo查看默认MetaspaceSize大小(约21M),MaxMetaspaceSize很大很大,前面说过MetaSpace只受本地内存大小限制。

jinfo -flag MetaspaceSize 1234 #结果为:-XX:MetaspaceSize=21807104
jinfo -flag MaxMetaspaceSize 1234 #结果为:-XX:MaxMetaspaceSize=18446744073709547520

总结: MetaspaceSize为触发FullGC的阈值,默认约为21M,如做了配置,最小阈值为自定义配置大小。空间使用达到阈值,触发FullGC,同时对该值扩大。当然如果元空间实际使用小于阈值,在GC的时候也会对该值缩小。
MaxMetaspaceSize为元空间的最大值,如果设置太小,可能会导致频繁FullGC,甚至OOM。 

四. JVM参数配置指南

前面三个部分对JVM进行了整体的了解,接下来是本文的重点。

-XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=256M -Xms256m -Xmx256m

 

文章看下来上面这段配置的意思很简单,设置元空间的初始值和最大值,设置堆空间的初始值和最大值。

为什么MetaspaceSize要设置为128M?为什么堆内存初始值Xms设置为256M而不是512M?

按照Java官方的指导:

 

Java堆大小设置,Xms 和 Xmx设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍

  • MaxPermSize(元空间)设置为老年代存活对象的1.2-1.5倍。
  • 年轻代Xmn的设置为老年代存活对象的1-1.5倍。
  • 老年代的内存大小设置为老年代存活对象的2-3倍。

可以让系统运行一段时间后查看系统的各个指标,然后在进行配置。如下用jstat工具查看jvm的情况:

jstat -gc 12345
###
S0C        S1C         S0U         S1U    EC             EU               OC               OU            MC             MU           CCSC      CCSU     YGC   YGCT   FGC FGCT    GCT 
13824.0  22528.0  13377.0    0.0     548864.0   535257.2    113152.0     46189.3     73984.0     71119.8     9728.0    9196.2    14       0.259    3      0.287    0.546

OU表示老年代所占用的内存为 46189.3 K(大约45M);那么jvm相应的配置参数应该做如下修改:

堆内存是老年代的3~4倍:45*3=135,45*4=180

元数据空间是老年代的1.2~1.5倍:45*1.2=54,45*1.5=67.5

配置如下:

-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M -Xms180m -Xmx180m

五、JVM调优工具

  Jconsole,jProfile,VisualVM

  Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。 

  JProfiler:商业软件,需要付费。功能强大。

  VisualVM:JDK自带,功能强大,与JProfiler类似。推荐