• JVM — 性能调优


    概念:

    一:堆(Heap)和非堆(Non-heap)内存

      按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。

         可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的。
         所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。 

    二:jvm参数的含义

     例:

      -Xms128m   JVM初始分配的堆内存
      -Xmx512m   JVM最大允许分配的堆内存,按需分配
      -XX:PermSize=64M   JVM初始分配的非堆内存
      -XX:MaxPermSize=128M   JVM最大允许分配的非堆内存,按需分配 

      Xmx(memory max)代表程序最大可以从操作系统中获取的内存数量,Xms(memory start )代表程序启动的时候从操作系统中获取的内存数量。

      比如java -cp . -Xms80m -Xmx256m 说明这个程序启动的时候使用80m的内存,最多可以从操作系统中获取256m的内存。

    三:堆内存分配和非堆内存分配

      堆内存分配:

         JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
     空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。
       说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try...catch捕捉。

      非堆内存分配:

         JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。(还有一说:MaxPermSize缺省值和-server -client选项相关,-server选项下默认MaxPermSize为64m,-client选项下默认MaxPermSize为32m。这个我没有实验。)
       上面错误信息中的PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。还没有弄明白PermGen space是属于非堆内存,还是就是非堆内存,但至少是属于了。
       XX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存益出。 
       说说为什么会内存益出: 
        (1)这一部分内存用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同。 
        (2)GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS 的话,就很可能出现PermGen space错误。
          这种错误常见在web服务器对JSP进行pre compile的时候。 


    (1)JVM内存模型及垃圾收集算法

      1.根据Java虚拟机规范,JVM将内存划分为:  

      • New(年轻代)
      • Tenured(年老代)
      • 永久代(Perm)

      其中New和Tenured属于堆内存,堆内存会从JVM启动参数(-Xmx:3G)指定的内存中分配,Perm不属于堆内存,有虚拟机直接分配,但可以通过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。

    • 年轻代(New):年轻代用来存放JVM刚分配的Java对象
    • 年老代(Tenured):年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代
    • 永久代(Perm):永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关,一般设置为128M就足够,设置原则是预留30%的空间。  

      New又分为几个部分:

    • Eden:Eden用来存放JVM刚分配的对象
    • Survivor1
    • Survivro2:两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到Tenured。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。 

       2.垃圾回收算法

        垃圾回收算法可以分为三类,都基于标记-清除(复制)算法:

      • Serial算法(单线程)
      • 并行算法
      • 并发算法

        JVM会根据机器的硬件配置对每个内存代选择适合的回收算法,比如,如果机器多于1个核,会对年轻代选择并行算法,关于选择细节请参考JVM调优文档。

        稍微解释下的是,并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不停止应用执行。所以,并发算法适用于交互性高的一些程序。经过观察,并发算法会减少年轻代的大小,其实就是使用了一个大的年老代,这反过来跟并行算法相比吞吐量相对较低。

        还有一个问题是,垃圾回收动作何时执行?

    • 当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC
    • 当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代
    • 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载

      另一个问题是,何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出

    • JVM98%的时间都花费在内存回收
    • 每次回收的内存小于2%

      满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。


     (2)内存泄漏及解决方法  

    1.系统崩溃前的一些现象:

    • 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
    • FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
    • 年老代的内存越来越大并且每次FullGC后年老代没有内存被释放

       之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。

     2.生成堆的dump文件

       通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

     3.分析dump文件

       下面要考虑的是如何打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux。当然我们可以借助X-Window把Linux上的图形导入到Window。我们考虑用下面几种工具打开该文件:

    1. Visual VM
    2. IBM HeapAnalyzer
    3. JDK 自带的Hprof工具

       使 用这些工具时为了确保加载速度,建议设置最大内存为6G。使用后发现,这些工具都无法直观地观察到内存泄漏,Visual VM虽能观察到对象大小,但看不到调用堆栈;HeapAnalyzer虽然能看到调用堆栈,却无法正确打开一个3G的文件。因此,我们又选用了 Eclipse专门的静态内存分析工具:Mat。

     4.分析内存泄漏

       通过Mat我们能清楚地看到,哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系。针对本案,在ThreadLocal中有很多的JbpmContext实例,经过调查是JBPM的Context没有关闭所致。

     另,通过Mat或JMX我们还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。

    5.回归问题

       Q:为什么崩溃前垃圾回收的时间越来越长?

       A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据

       Q:为什么Full GC的次数越来越多?

       A:因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收

       Q:为什么年老代占用的内存越来越大?

       A:因为年轻代的内存无法被回收,越来越多地被Copy到年老代


     三:性能调优

      从四个方便入手:  

      • 线程池:解决用户响应时间长的问题
      • 连接池
      • JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量
      • 程序算法:改进程序逻辑算法提高性能

      下面只介绍JVM的启动参数优化

       在JVM启动参数中,可以设置跟内存、垃圾回收相关的一些参数设置,默认情况不做任何设置JVM会工作的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能获得最佳性能。通过设置我们希望达到一些目标:

    • GC的时间足够的小
    • GC的次数足够的少
    • 发生Full GC的周期足够的长

      前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。

       (1)针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值
       (2)年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小

       (3)年轻代和年老代设置多大才算合理?这个我问题毫无疑问是没有答案的,否则也就不会有调优。我们观察一下二者大小变化有哪些影响

    • 更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
    • 更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
    • 如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性,在抉择时应该根 据以下两点:(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 (B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间

      (4)在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集

      (5)线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

      (4)可以通过下面的参数打Heap Dump信息

    • -XX:HeapDumpPath
    • -XX:+PrintGCDetails
    • -XX:+PrintGCTimeStamps
    • -Xloggc:/usr/aaa/dump/heap_trace.txt

        通过下面参数可以控制OutOfMemoryError时打印堆的信息

    • -XX:+HeapDumpOnOutOfMemoryError

      请看一下一个时间的Java参数配置:(服务器:Linux 64Bit,8Core×16G)

     JAVA_OPTS="$JAVA_OPTS -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/aaa/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/aaa/dump/heap_trace.txt -XX:NewSize=1G -XX:MaxNewSize=1G"

      经过观察该配置非常稳定,每次普通GC的时间在10ms左右,Full GC基本不发生,或隔很长很长的时间才发生一次

      通过分析dump文件可以发现,每个1小时都会发生一次Full GC,经过多方求证,只要在JVM中开启了JMX服务,JMX将会1小时执行一次Full GC以清除引用。


    调优方法:

      一切都是为了这一步,调优,在调优之前,我们需要记住下面的原则:

        1、多数的Java应用不需要在服务器上进行GC优化;

        2、多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;

        3、在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);

        4、减少创建对象的数量;

        5、减少使用全局变量和大对象;

        6、GC优化是到最后不得已才采用的手段;

        7、在实际使用中,分析GC情况优化代码比优化GC参数要多得多;

      GC优化的目的有两个:   

        1、将转移到老年代的对象数量降低到最小;

        2、减少full GC的执行时间;  

      为了达到上面的目的,一般地,你需要做的事情有:

        1、减少使用全局变量和大对象;

        2、调整新生代的大小到最合适;

        3、设置老年代的大小为最合适;

        4、选择合适的GC收集器;

    调优实例:

      实例1:

      发现部分开发测试机器出现异常:java.lang.OutOfMemoryError: GC overhead limit exceeded,这个异常代表:GC为了释放很小的空间却耗费了太多的时间,其原因一般有两个:1,堆太小,2,有死循环或大对象;

      首先排除了第2个原因,因为这个应用同时是在线上运行的,如果有问题,早就挂了。所以怀疑是这台机器中堆设置太小;

      使用ps -ef |grep "java"查看,发现:

       

      该应用的堆区设置只有768m,而机器内存有2g,机器上只跑这一个java应用,没有其他需要占用内存的地方。另外,这个应用比较大,需要占用的内存也比较多;

      通过上面的情况判断,只需要改变堆中各区域的大小设置即可,于是改成下面的情况:

      

      跟踪运行情况发现,相关异常没有再出现

      实例2:

      一个服务系统,经常出现卡顿,分析原因,发现Full GC时间太长

      jstat -gcutil:

        S0     S1    E     O       P        YGC YGCT FGC FGCT  GCT

        12.16 0.00 5.18 63.78 20.32  54   2.047 5     6.946  8.993   

      分析上面的数据,发现Young GC执行了54次,耗时2.047秒,每次Young GC耗时37ms,在正常范围,而Full GC执行了5次,耗时6.946秒,每次平均1.389s,数据显示出来的问题是:Full GC耗时较长,分析该系统的是指发现,NewRatio=9,也就是说,新生代和老生代大小之比为1:9,这就是问题的原因:

      1,新生代太小,导致对象提前进入老年代,触发老年代发生Full GC;

      2,老年代较大,进行Full GC时耗时较大;

      优化的方法是调整NewRatio的值,调整到4,发现Full GC没有再发生,只有Young GC在执行。这就是把对象控制在新生代就清理掉,没有进入老年代(这种做法对一些应用是很有用的,但并不是对所有应用都要这么做)

      实例3:

      一 应用在性能测试过程中,发现内存占用率很高,Full GC频繁,使用sudo -u admin -H  jmap -dump:format=b,file=文件名.hprof pid 来dump内存,生成dump文件,并使用Eclipse下的mat差距进行分析,发现:

      

      从图中可以看出,这个线程存在问题,队列LinkedBlockingQueue所引用的大量对象并未释放,导致整个线程占用内存高达378m,此时通知开发人员进行代码优化,将相关对象释放掉即可。

     出处资料:https://www.cnblogs.com/downey/p/5352593.html

  • 相关阅读:
    Atitit。D&D drag&drop拖拽功能c#.net java swing的对比与实现总结
    Atitit.js javascript异常处理机制与java异常的转换 多重catc hDWR 环境 .js exception process Vob7
    Atitit.web 视频播放器classid clsid 大总结quicktime,vlc 1. Classid的用处。用来指定播放器 1 2. 标签用于包含对象,比如图像、音
    ListView与Adapter的那些事儿
    (转)Android反面自动静音
    (转)socket 与 file_get_contents的区别和优势的简单介绍
    Android ArrayAdapter 详解
    ImageView相关
    Android dip,px,pt,sp 的区别
    (转)Android 程序获取、设置铃声、音量、静音、扬声器
  • 原文地址:https://www.cnblogs.com/myseries/p/10799880.html
Copyright © 2020-2023  润新知