精华内容
下载资源
问答
  • jvm 怎么调优

    2016-09-05 17:26:00
    JVM性能调优 博客分类: JVM JVM垃圾回收与性能调优总结 JVM调优的几种策略 一、JVM内存模型及垃圾收集算法 1.根据Java虚拟机规范,JVM将内存划分为: New(年轻代) Tenured(年老代) 永久代(Perm) 其中...
    JVM性能调优
    
    博客分类: JVM
    
    JVM垃圾回收与性能调优总结
    JVM调优的几种策略
    
    
    
    一、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。
    
    二、内存泄漏及解决方法
     1.系统崩溃前的一些现象:
    每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到45s
    FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
    年老代的内存越来越大并且每次FullGC后年老代没有内存被释放
     之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。
    
     2.生成堆的dump文件
     通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。
    
     3.分析dump文件
     下面要考虑的是如何打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux。当然我们可以借助X-Window把Linux上的图形导入到Window。我们考虑用下面几种工具打开该文件:
    Visual VM
    IBM HeapAnalyzer
    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到年老代
    
    三、性能调优
     除了上述内存泄漏外,我们还发现CPU长期不足3%,系统吞吐量不够,针对8core×16G、64bit的Linux服务器来说,是严重的资源浪费。
     在CPU负载不足的同时,偶尔会有用户反映请求的时间过长,我们意识到必须对程序及JVM进行调优。从以下几个方面进行:
    线程池:解决用户响应时间长的问题
    连接池
    JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量
    程序算法:改进程序逻辑算法提高性能
      1.Java线程池(java.util.concurrent.ThreadPoolExecutor)
        大多数JVM6上的应用采用的线程池都是JDK自带的线程池,之所以把成熟的Java线程池进行罗嗦说明,是因为该线程池的行为与我们想象的有点出入。Java线程池有几个重要的配置参数:
    corePoolSize:核心线程数(最新线程数)
    maximumPoolSize:最大线程数,超过这个数量的任务会被拒绝,用户可以通过RejectedExecutionHandler接口自定义处理方式
    keepAliveTime:线程保持活动的时间
    workQueue:工作队列,存放执行的任务
        Java线程池需要传入一个Queue参数(workQueue)用来存放执行的任务,而对Queue的不同选择,线程池有完全不同的行为:
    SynchronousQueue: 一个无容量的等待队列,一个线程的insert操作必须等待另一线程的remove操作,采用这个Queue线程池将会为每个任务分配一个新线程
    LinkedBlockingQueue : 无界队列,采用该Queue,线程池将忽略 maximumPoolSize参数,仅用corePoolSize的线程处理所有的任务,未处理的任务便在LinkedBlockingQueue中排队
    ArrayBlockingQueue: 有界队列,在有界队列和 maximumPoolSize的作用下,程序将很难被调优:更大的Queue和小的maximumPoolSize将导致CPU的低负载;小的Queue和大的池,Queue就没起动应有的作用。
        其实我们的要求很简单,希望线程池能跟连接池一样,能设置最小线程数、最大线程数,当最小数<任务<最大数时,应该分配新的线程处理;当任务>最大数时,应该等待有空闲线程再处理该任务。
        但线程池的设计思路是,任务应该放到Queue中,当Queue放不下时再考虑用新线程处理,如果Queue满且无法派生新线程,就拒绝该任务。设计导致“先放等执行”、“放不下再执行”、“拒绝不等待”。所以,根据不同的Queue参数,要提高吞吐量不能一味地增大maximumPoolSize。
        当然,要达到我们的目标,必须对线程池进行一定的封装,幸运的是ThreadPoolExecutor中留了足够的自定义接口以帮助我们达到目标。我们封装的方式是:
    以SynchronousQueue作为参数,使maximumPoolSize发挥作用,以防止线程被无限制的分配,同时可以通过提高maximumPoolSize来提高系统吞吐量
    自定义一个RejectedExecutionHandler,当线程数超过maximumPoolSize时进行处理,处理方式为隔一段时间检查线程池是否可以执行新Task,如果可以把拒绝的Task重新放入到线程池,检查的时间依赖keepAliveTime的大小。
      2.连接池(org.apache.commons.dbcp.BasicDataSource)
        在使用org.apache.commons.dbcp.BasicDataSource的时候,因为之前采用了默认配置,所以当访问量大时,通过JMX观察到很多Tomcat线程都阻塞在BasicDataSource使用的Apache ObjectPool的锁上,直接原因当时是因为BasicDataSource连接池的最大连接数设置的太小,默认的BasicDataSource配置,仅使用8个最大连接。
        我还观察到一个问题,当较长的时间不访问系统,比如2天,DB上的Mysql会断掉所以的连接,导致连接池中缓存的连接不能用。为了解决这些问题,我们充分研究了BasicDataSource,发现了一些优化的点:
    Mysql默认支持100个链接,所以每个连接池的配置要根据集群中的机器数进行,如有2台服务器,可每个设置为60
    initialSize:参数是一直打开的连接数
    minEvictableIdleTimeMillis:该参数设置每个连接的空闲时间,超过这个时间连接将被关闭
    timeBetweenEvictionRunsMillis:后台线程的运行周期,用来检测过期连接
    maxActive:最大能分配的连接数
    maxIdle:最大空闲数,当连接使用完毕后发现连接数大于maxIdle,连接将被直接关闭。只有initialSize < x < maxIdle的连接将被定期检测是否超期。这个参数主要用来在峰值访问时提高吞吐量。
    initialSize是如何保持的?经过研究代码发现,BasicDataSource会关闭所有超期的连接,然后再打开initialSize数量的连接,这个特性与minEvictableIdleTimeMillis、timeBetweenEvictionRunsMillis一起保证了所有超期的initialSize连接都会被重新连接,从而避免了Mysql长时间无动作会断掉连接的问题。
      3.JVM参数
        在JVM启动参数中,可以设置跟内存、垃圾回收相关的一些参数设置,默认情况不做任何设置JVM会工作的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能获得最佳性能。通过设置我们希望达到一些目标:
    GC的时间足够的小
    GC的次数足够的少
    发生Full GC的周期足够的长
      前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。
       (1)针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值
       (2)年轻代和年老代将根据默认的比例(12)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小
       (3)年轻代和年老代设置多大才算合理?这个我问题毫无疑问是没有答案的,否则也就不会有调优。我们观察一下二者大小变化有哪些影响
    更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
    更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
    如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例12也是这个道理 (B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在11。但应该给年老代至少预留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以清除引用,关于这点请参考附件文档。
     4.程序算法调优:本次不作为重点
    
    参考资料:
    http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html
    来源:http://blog.csdn.net/chen77716/article/details/5695893
    
    =======================================================================================
    调优方法
    一切都是为了这一步,调优,在调优之前,我们需要记住下面的原则:
    
    1、多数的Java应用不需要在服务器上进行GC优化;
    2、多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;
    3、在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);
    4、减少创建对象的数量;
    5、减少使用全局变量和大对象;
    6、GC优化是到最后不得已才采用的手段;
    7、在实际使用中,分析GC情况优化代码比优化GC参数要多得多;
    
    GC优化的目的有两个(http://www.360doc.com/content/13/0305/10/15643_269388816.shtml):
    1、将转移到老年代的对象数量降低到最小;
    2、减少full GC的执行时间;
    
    为了达到上面的目的,一般地,你需要做的事情有:
    1、减少使用全局变量和大对象;
    2、调整新生代的大小到最合适;
    3、设置老年代的大小为最合适;
    4、选择合适的GC收集器;
    
    在上面的4条方法中,用了几个“合适”,那究竟什么才算合适,一般的,请参考上面“收集器搭配”和“启动内存分配”两节中的建议。但这些建议不是万能的,需要根据您的机器和应用情况进行发展和变化,实际操作中,可以将两台机器分别设置成不同的GC参数,并且进行对比,选用那些确实提高了性能或减少了GC时间的参数。
    
    真正熟练的使用GC调优,是建立在多次进行GC监控和调优的实战经验上的,进行监控和调优的一般步骤为:
    1,监控GC的状态
    使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化;
    
    2,分析结果,判断是否需要优化
    如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;如果GC时间超过1-3秒,或者频繁GC,则必须优化;
    注:如果满足下面的指标,则一般不需要进行GC:
       Minor GC执行时间不到50ms;
       Minor GC执行不频繁,约10秒一次;
       Full GC执行时间不到1s;
       Full GC执行频率不算频繁,不低于10分钟1次;
    
    3,调整GC类型和内存分配
    如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择;
    4,不断的分析和调整
    通过不断的试验和试错,分析并找到最合适的参数
    5,全面应用参数
    如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。
    
    
    调优实例
    上面的内容都是纸上谈兵,下面我们以一些真实例子来进行说明:
    实例1:
    笔者昨日发现部分开发测试机器出现异常:java.lang.OutOfMemoryError: GC overhead limit exceeded,这个异常代表:
    GC为了释放很小的空间却耗费了太多的时间,其原因一般有两个:1,堆太小,2,有死循环或大对象;
    笔者首先排除了第2个原因,因为这个应用同时是在线上运行的,如果有问题,早就挂了。所以怀疑是这台机器中堆设置太小;
    使用ps -ef |grep "java"查看,发现:
    
    
    该应用的堆区设置只有768m,而机器内存有2g,机器上只跑这一个java应用,没有其他需要占用内存的地方。另外,这个应用比较大,需要占用的内存也比较多;
    笔者通过上面的情况判断,只需要改变堆中各区域的大小设置即可,于是改成下面的情况:
    
    
    跟踪运行情况发现,相关异常没有再出现;
    
    实例2:(http://www.360doc.com/content/13/0305/10/15643_269388816.shtml)
    一个服务系统,经常出现卡顿,分析原因,发现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,此时通知开发人员进行代码优化,将相关对象释放掉即可。
    来源:Java系列笔记(4) - JVM监控与调优

    原文

    展开全文
  • 070:jvm内存溢出+调优实战1 怎样的对象会进入到老年代2 怎么去定位死锁代码3 CPU100%怎么去定位4 堆内存溢出怎么定位5 jvm调优案例分析 1 怎样的对象会进入到老年代 课程内容: 1.什么时候会发生full gc 2.死锁问题...

    1 怎样的对象会进入到老年代

    课程内容:
    1.什么时候会发生full gc
    2.死锁问题怎么去定位
    3.服务器cpu100%如何定位
    4.内存溢出如何去寻找问题
    5.jvm调优实际案例分析
    在这里插入图片描述
    什么样的对象进入老年代?

    1. 大对象;
    2. 长期存活的对象(默认年龄超过15次、可配置);
    3. 空间担保 对象太多minor gc s0区、s1区放不下
    4. 动态年龄 当前Survivor区对应对象年龄有50%>Survivor平均年龄,将大于的部分对象转入老年区

    指定创建的对象超过多少会直接创建在老年代
    此参数只能在serial收集器和parnew收集器才有效
    -XX:PretenureSizeThreshold
    (比如 -XX:PretenureSizeThreshold=1M)
    指定多大年龄的对象进入老年代
    -XX:MaxTenuringThreshold
    (比如 -XX:MaxTenuringThreshold=15 默认也是15次)

    2 怎么去定位死锁代码

    @RestController
    public class GCController {
    
        @Value("${mayikt.name}")
        private String name;
    
        private List<byte[]> bytes = new ArrayList<>();
    
    
        @GetMapping("/getName")
        public String getName() {
            byte[] bytes1 = new byte[1024 * 1024 * 4];
            return name;
        }
    
        @GetMapping("/deadlock")
        public String dieLock() {
            new Thread(() -> {
                synchronized (Integer.class) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        synchronized (String.class) {
                            System.out.println("执行成功");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            synchronized (String.class) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                    synchronized (Integer.class) {
                        System.out.println("执行成功");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return "死锁";
        }
    
        @GetMapping("/oom")
        public String cms() {
            List<byte[]> bytes = new ArrayList<>();
    
            while (true) {
                byte[] bytes1 = new byte[1024 * 1024 * 4];
                bytes.add(bytes1);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        @GetMapping("/loop")
        public String loop() {
            boolean b = true;
            while (b) {
            }
            return "123";
        }
    
        @GetMapping("/memoryLeak")
        public String oom() {
    
            while (true) {
                byte[] bytes1 = new byte[1024 * 1024 * 4];
                bytes.add(bytes1);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    @SpringBootApplication
    public class DemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    
    }
    

    本地使用VisualVm查看死锁
    在这里插入图片描述
    定位死锁
    控制台输入jsp命令,查看所有java项目pid
    jstack pid 打出当前运行的所有线程的快照
    在这里插入图片描述
    死锁只是该线程阻塞,不影响其他线程使用,但是如果死锁的代码一直有请求,最终还是会导致整个服务无法使用(线程池数量有限制)。
    在这里插入图片描述

    3 CPU100%怎么去定位

    cpu 100% 定位思路
    1.定位到哪一个程序cpu占用率高 top
    2.定位到当前程序 是哪一个线程占用率高 ps -mp pid -o THREAD,tid,time,
    找到对应tid
    3.jstack定位到对应线程快照,打印出的日志搜索tid(转换成16进制)
    在这里插入图片描述

    4 堆内存溢出怎么定位

    oom 异常分两种情况
    1.内存溢出
    2.内存泄漏

    内存溢出时候打印堆内存快照
    -XX:+HeapDumpOnOutOfMemoryError该配置会把快照保存在用户目录或者tomcat目录下,也可以通过 -XX:HeapDumpPath=/tmp/heapdump.dump来显示指定路径。
    Configuration中VM options输入:
    -Xloggc:D:\logs\gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\heapdump.dump

    内存溢出怎么定位:
    1.程序里面有大对象;
    2.大对象被谁引用,使用visualVM装入dump快照,根据线程快照找到在哪一行出现溢出。
    在这里插入图片描述
    内存泄漏定位:
    在这里插入图片描述

    5 jvm调优案例分析

    参考文章:《从实际案例聊聊Java应用的GC优化》
    作者:录录 美团技术团队 2017-12-28
    链接:从实际案例聊聊Java应用的GC优化.

    展开全文
  • JVM 性能调优实战之:一次系统性能瓶颈的寻找过程 玩过性能优化的朋友都清楚,性能优化的关键并不在于怎么进行优化,而在于怎么找到当前系统的性能瓶颈。 性能优化分为好几个层次,比如系统层次、算法层次、代码...

    JVM 性能调优实战之:一次系统性能瓶颈的寻找过程

     

    玩过性能优化的朋友都清楚,性能优化的关键并不在于怎么进行优化,而在于怎么找到当前系统的性能瓶颈。
    性能优化分为好几个层次,比如系统层次、算法层次、代码层次...JVM 的性能优化被认为是底层优化,门槛较高,精通这种技能的人比较少。笔者呆过几家技术力量不算弱的公司,每个公司内部真正能够进行 JVM 性能调优的人寥寥无几、甚至没有。如是乎,能够有效通过 JVM 调优提升系统性能的人往往被人们冠以"大牛"、"大师"之类的称呼。
    其实 JVM 本身给我们提供了很多强大而有效的监控进程、分析定位瓶颈的工具,比如 JConsole、JMap、JStack、JStat 等等。使用这些命令,再结合 Linux 自身提供的一些强大的进程、线程命令,能够快速定位系统瓶颈。本文以一次使用这些工具准确定位到某系统瓶颈的实战经验为例,希望能为大家掌握 JVM 调优这项技能起到一些借鉴作用。
    本文背景:

    • Linux:RedHat 6.1
    • Weblogic:11g
    • JRokit:R28.2.4
    • JDK:1.6.0_45

    Weblogic 跑在 JRokit 上面,JDK 是我们对 JRokit 进行监控分析的工具。

    1. LoadRunner 压测结果

    该系统其实早已跑在生产上好多年,虽然没有做过压力测试,但稳定性还是可以的。公司进行架构调整,有一些新系统将对接该系统。公司领导担心对接后该系统的并发性能问题,于是开始对该系统进行并发压力测试。
    50 个用户并发压十几个小时,TRT 在 20 左右,TPS 在 2.5 左右。领导对这个结果不满意,要求进行优化。但这是个老系统,开发在之前明显已经对其代码做过很多优化,如今面对这种状况也束手无策。

    2. Oracle 的 awr 报告分析

    有句老话,优化来优化去,系统的性能瓶颈还是会在数据库上面。在这里我们首先想到的也是数据库的问题。
    并发压测的时候 Spotlight 查看数据库服务器各项性能指标,很清闲。

    分析 awr 报告,结果显示也是很清闲。

    并发压测的时候去相关数据表执行一些 sql 的速度很快也证明着问题不在 Oracle 这边。

    3. Weblogic 服务器的性能指标

    使用 Spotlight 查看并发压测时的 Weblogic 所在的 Linux 服务器,除了 CPU 之外其它各项指标显示,Linux 也很清闲。
    虽然 CPU 利用率始终在 200% 左右徘徊,但这对于 16 核的系统来讲也算是正常的吧?

    4. JStack 报告分析

    事情到了这里,大家已经想到了线程死锁、等待的问题了。
    没错,JStack 隆重登场。JStack 能够看到当前 Java 进程中每个线程的当前状态、调用栈、锁住或等待去锁定的资源,而且很强悍的是它还能直接报告是否有线程死锁,可谓解决线程问题的不二之选。
    /opt/jdk1.6.0_45/bin/jstack -l 10495 > ~/10495jstack.txt
    JRokit 堆栈的拉取,可以直接用 JDK 的 JStack,10495 是 Weblogic 服务的进程 ID。注意一定要用该进程的启动用户去拉,否则会报 Unable to open socket file: target process not responding or HotSpot VM not loaded 错误。
    JStack 拉取的文件信息基本分为以下几个部分:

    • 该拉取快照的服务器时间
    • JVM 版本
    • 以线程 ID(即 tid)升序依次列出当前进程中每个线程的调用栈
    • 死锁(如果有的话)
    • 阻塞锁链
    • 打开的锁链
    • 监视器解锁情况跟踪

    每个线程在等待什么资源,这个资源目前在被哪个线程 hold,尽在眼前。JStack 最好在压测时多次获取,找到的普遍存在的现象即为线程瓶颈所在。

    4.1. TLA 空间的调整

    多次拉取 JStack,发现很多线程处于这个状态:
        at jrockit/vm/Allocator.getNewTla(JJ)V(Native Method)
        at jrockit/vm/Allocator.allocObjectOrArray(Allocator.java:354)[optimized]
        at java/util/HashMap.resize(HashMap.java:564)[inlined]
        at java/util/LinkedHashMap.addEntry(LinkedHashMap.java:414)[optimized]
        at java/util/HashMap.put(HashMap.java:477)[optimized]
    由此怀疑出现上述堆栈的原因可能是 TLA 空间不足引起。TLA 是 thread local area 的缩写,是每个线程私有的空间,所以在多线程环境下 TLA 带来的性能提升是显而易见的。如果大部分线程的需要分配的对象都较大,可以考虑提高 TLA 空间,因为这样更大的对象可以在 TLA 中进行分配,这样就不用担心和其它线程的同步问题了。但这个也不可以调的太大,否则也会带来一些问题,比如会带来更多内存碎片、更加频繁的垃圾搜集。
    TLA 默认最小大小 2 KB,默认首选大小 16 KB - 256 KB (取决于新生代分区大小)。这里我们调整 TLA 空间大小为最小 32 KB,首选 1024 KB,JVM 启动参数中加入:
    -XXtlaSize:min=32k,preferred=1024k

    5. JStat 结合 GC 日志报告分析

    第 4 步参数生效之后继续压测,TLA 频繁申请是降下来了,但 TRT 仍旧是 20,TPS 依旧 2.5。别灰心,改一个地方就立竿见影,胜利似乎来得太快了点。
    现在怀疑是服务堆内存太小,查看一下果然。服务器物理内存 32 GB,Weblogic 进程只分到了 6 GB。怎么查看?至少有四种办法:

    5.1. ps 命令

    ps -ef | grep java
    defonds     29874 29819  2 Sep03 ?        09:03:17 /opt/jrockit-jdk1.6.0_33/bin/java -jrockit -Xms6000m -Xmx6000m -Dweblogic.Name=AdminServer -Djava.security.policy=

    5.2. Weblogic 控制台

    登录 Weblogic 管理控制台 -> 环境 -> 服务器,选择该服务器实例 -> 监视 -> 性能 -> 当前堆大小。
    这个页面还能看到进程已运行时间,启动以来发生的 GC 次数,可以折算出 GC 的频率,为本次性能瓶颈 - GC 过于频繁提供有力的佐证。

    5.3. GC 日志报告

    开启 JRokit GC 日志报告只需在 Java 进程启动参数里加入
    -Xverbose:memory -Xverboselog:verboseText.txt
    GC 日志将会被输出到 verboseText.txt 文件,这个文件一般会生成在启动的 Weblogic 域目录下。如果找不着也可以用 find 命令去搜:
    find /appserver/ -name verboseText.txt
    /appserver/Oracle/Middleware/user_projects/domains/defonds_domain/verboseText.txt
    GC log 拿到后,第 3 行中的 maximal heap size 即为该进程分配到的最大堆大小:
    [INFO ][memory ] Heap size: 10485760KB, maximal heap size: 10485760KB, nursery size: 5242880KB.
    下面还有进程启动以后较为详细的每次 GC 的信息:
    [INFO ][memory ] [YC#2547] 340.828-340.845: YC 10444109KB->10417908KB (10485760KB), 0.018 s, sum of pauses 17.294 ms, longest pause 17.294 ms.
    [INFO ][memory ] [YC#2548] 340.852-340.871: YC 10450332KB->10434521KB (10485760KB), 0.019 s, sum of pauses 18.779 ms, longest pause 18.779 ms.
    [INFO ][memory ] [YC#2549] 340.878-340.895: YC 10476739KB->10485760KB (10485760KB), 0.017 s, sum of pauses 16.520 ms, longest pause 16.520 ms.
    [INFO ][memory ] [OC#614] 340.895-341.126: OC 10485760KB->10413562KB (10485760KB), 0.231 s, sum of pauses 206.458 ms, longest pause 206.458 ms.
    第一行表示该进程启动后的第 340.828 秒 - 340.845 秒期间进行了一次 young gc,该次 GC 持续了 17.294 ms,将整个已用掉的堆内存由 10444109 KB 降低到 10417908 KB。
    第三行同样是一次 young gc,但该次 GC 后已用堆内存反而上升到了 10485760 KB,也就是达到最大堆内存,于是该次 young gc 结束的同时触发 full gc。
    第四行是一次 old gc (即 full gc),将已用堆内存由 10485760 KB 降到了 10413562 KB,耗时 206.458 ms。
    这些日志同样能够指出当前压力下的 GC 的频率,为本次性能瓶颈 - GC 过于频繁提供有力的佐证。

    5.4. JStat 报告

    跟 JStack 的拉取一样,可以直接用 JDK 的 JStat 去拉取 JRokit 的 GC 信息:
    /opt/jdk1.6.0_45/bin/jstat -J-Djstat.showUnsupported=true -snap 10495 > ~/10495jstat.txt
    注意这个信息是一个快照,这是跟 GC 日志报告不同的地方。
    jrockit.gc.latest.heapSize=10737418240
    jrockit.gc.latest.nurserySize=23100384
    上述是当前已用碓大小和新生代分区大小。多拉几次即可估算出各自分配的大小。

    5.5. 内存分配

    根据 5.1 - 5.4 我们得出当前服务器分配堆内存太小的结论,根据 5.3 GC 日志报告和 5.4. JStat 报告可以得出新生代分区太小的结论。
    于是我们调整它们的大小,结合 4.1 TLA 调整的结论,JVM 启动参数增加以下:
    -Xms10240m -Xmx10240m -Xns:1024m -XXtlaSize:min=32k,preferred=1024k
    再次压测,TRT 降到了 2.5,TPS 上升到 20。

    6. 性能瓶颈的定位

    很明显,上述 JVM 调整没有从根本上解决性能问题,我们还没有真正定位到系统性能瓶颈。

    6.1. 性能线程的定位

    6.1.1. 性能进程的获取

    使用 TOP 命令拿到最耗 CPU 的那个进程:
    性能进程号的获取.png
    进程 ID 为 10495 的那个进程一直在占用很高的 CPU。

    6.1.2. 性能线程的获取

    现在我们来找到这个进程中占用 CPU 较高的那些线程:
    ps p 10495 -L -o pcpu,pid,tid,time,tname,cmd > ~/10495ps.txt
    多次拉这个快照,我们找到了 tid 为 10499、10500、10501、10502 等线程一直在占用着很高的 CPU:
    tid为10499、10500、10501、10502等线程占用CPU很高.png
    拉 JStack 快照看看都是一些什么线程:
    /opt/jdk1.6.0_45/bin/jstack -l 10495 > ~/10495jstack.txt
    相关部分结果如下:
    "(GC Worker Thread 1)" id=? idx=0x10 tid=10499 prio=5 alive, daemon
        at pthread_cond_wait@@GLIBC_2.3.2+202(:0)@0x3708c0b44a
        at eventTimedWaitNoTransitionImpl+71(event.c:90)@0x7fac47be8528
        at eventTimedWaitNoTransition+66(event.c:72)@0x7fac47be8593
        at mmGCWorkerThread+137(gcthreads.c:809)@0x7fac47c0774a
        at thread_stub+170(lifecycle.c:808)@0x7fac47cc15bb
        at start_thread+208(:0)@0x3708c077e1
        Locked ownable synchronizers:
            - None

    "(GC Worker Thread 2)" id=? idx=0x14 tid=10500 prio=5 alive, daemon
        at pthread_cond_wait@@GLIBC_2.3.2+202(:0)@0x3708c0b44a
        at eventTimedWaitNoTransitionImpl+71(event.c:90)@0x7fac47be8528
        at eventTimedWaitNoTransition+66(event.c:72)@0x7fac47be8593
        at mmGCWorkerThread+137(gcthreads.c:809)@0x7fac47c0774a
        at thread_stub+170(lifecycle.c:808)@0x7fac47cc15bb
        at start_thread+208(:0)@0x3708c077e1
        Locked ownable synchronizers:
            - None

    "(GC Worker Thread 3)" id=? idx=0x18 tid=10501 prio=5 alive, daemon
        at pthread_cond_wait@@GLIBC_2.3.2+202(:0)@0x3708c0b44a
        at eventTimedWaitNoTransitionImpl+71(event.c:90)@0x7fac47be8528
        at eventTimedWaitNoTransition+66(event.c:72)@0x7fac47be8593
        at mmGCWorkerThread+137(gcthreads.c:809)@0x7fac47c0774a
        at thread_stub+170(lifecycle.c:808)@0x7fac47cc15bb
        at start_thread+208(:0)@0x3708c077e1
        Locked ownable synchronizers:
            - None

    "(GC Worker Thread 4)" id=? idx=0x1c tid=10502 prio=5 alive, daemon
        at pthread_cond_wait@@GLIBC_2.3.2+202(:0)@0x3708c0b44a
        at eventTimedWaitNoTransitionImpl+71(event.c:90)@0x7fac47be8528
        at eventTimedWaitNoTransition+66(event.c:72)@0x7fac47be8593
        at mmGCWorkerThread+137(gcthreads.c:809)@0x7fac47c0774a
        at thread_stub+170(lifecycle.c:808)@0x7fac47cc15bb
        at start_thread+208(:0)@0x3708c077e1
        Locked ownable synchronizers:
            - None

    6.2. 找到性能瓶颈

    事情到了这里,已经不难得出当前系统瓶颈就是频繁 GC。
    为何会如此频繁 GC 呢?继续看 JStack,发现这两个互相矛盾的现象:
    一方面 GC Worker 线程在拼命 GC,但是 GC 前后效果不明显,已用堆内存始终降不下来;
    另一方面大量 ExecuteThread 业务处理线程处于 alloc_enqueue_allocation_and_wait_for_gc 的 native_blocked 阻塞状态。
    此外,停止压测以后,查看已用堆内存大小,也就几百兆,不到分配堆内存的 1/10。
    这说明了什么呢?这说明了我们应用里没有内存泄漏、静态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象。
    很明显还是代码里有问题。那么这些对象来自哪里?如何在海量业务代码里边准确定位这些性能代码?也就是说如何利用 JVM 调优驱动代码层面的调优?请参考博客《JVM 性能调优实战之:使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码》,使用 TProfiler 我们成功找到了代码里边导致 JVM 频繁 GC 的元凶,并最终解决掉了这个性能瓶颈,将 TRT 降到了 0.5,TPS 提升至 100 +。

    参考资料

    JVM 性能调优实战之:使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码

    置顶2016年09月21日 11:25:21

    阅读数:40567

    本文是《JVM 性能调优实战之:一次系统性能瓶颈的寻找过程》 的后续篇,该篇介绍了如何使用 JDK 自身提供的工具进行 JVM 调优将 TPS 由 2.5 提升到 20 (提升了 7 倍),并准确定位系统瓶颈:我们应用里静态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。那么问题来了,如何在海量业务代码里边准确定位这些性能代码?本文将介绍如何使用阿里开源工具 TProfiler 来定位这些性能代码,成功解决掉了 GC 过于频繁的性能瓶颈,并最终在上次优化的基础上将 TPS 再提升了4 倍,即提升到 100。

    1. TProfiler 的下载安装

    1.1. 下载

    访问 TProfiler 的 GitHub 主页,点击 Clone or download 按钮的打开下载选项,点击该选项下的 Download ZIP 按钮将 TProfiler-master.zip 下载到本地。笔者上传了一份截至 20160920 最新 TProfiler-master.zip 到 CSDN 资源,读者朋友也可以去这里下载:http://download.csdn.net/detail/defonds/9635731

    1.2. 安装

    SSH 登录需要监控的远程服务器主机,为 TProfiler 新建安装路径:
    mkdir /opt/tprofiler
    本地将下载后的 TProfiler-master.zip 解压缩,将 dist 目录下的 profile.properties 以及 dist/lib 目录下的 tprofiler-1.0.1.jar ftp 上传到远程服务器 /opt/tprofiler 目录下。
    最后将远程服务器 /opt/tprofiler 目录及其下所有文件的所有者改为启动 Weblogic 进程的用户及其所在用户组。

    2. TProfiler 的配置部署

    2.1. TProfiler 配置

    编辑服务器 /opt/tprofiler/profile.properties 文件内容如下:
    #log file name
    logFileName = tprofiler.log
    methodFileName = tmethod.log
    samplerFileName = tsampler.log

    #basic configuration items
    startProfTime = 9:00:00
    endProfTime = 23:00:00
    eachProfUseTime = 5
    eachProfIntervalTime = 50
    samplerIntervalTime = 20
    port = 30000
    debugMode = false
    needNanoTime = false
    ignoreGetSetMethod = true

    #file paths
    logFilePath = ${user.home}/logs/${logFileName}
    methodFilePath = ${user.home}/logs/${methodFileName}
    samplerFilePath = ${user.home}/logs/${samplerFileName}

    #include & excludes items
    excludeClassLoader = org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader
    includePackageStartsWith = com.caucho;com.defonds;com.fasterxml;com.sun.jersey;com.sun.jmx;org.apache;org.codehaus;org.jdbcdslog;org.mybatis;org.quartz;org.springframework
    excludePackageStartsWith = com.taobao.sketch;org.apache.velocity;com.alibaba;com.taobao.forest.domain.dataobject
    红色部分是我们修改后的内容,其它部分使用默认值。

    2.2. Weblogic 启动参数配置

    在 Weblogic JVM 启动参数里加入:
    -javaagent:/opt/tprofiler/tprofiler-1.0.1.jar -Dprofile.properties=/opt/tprofiler/profile.properties
    之后重启 Weblogic。

    3. TProfiler 的远程操作

    使用启动 Weblogic 进程的用户 SSH 远程登录正在进行压测的机器。

    3.1. 查看 TProfiler 当前状态

    java -cp /opt/tprofiler/tprofiler-1.0.1.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 30000 status
    running
    得到这个结果证明 TProfiler 正在进行采集工作。

    3.2. 将 TProfiler 停止,以释放其占用的系统资源

    随时关闭 TProfiler:
    java -cp /opt/tprofiler/tprofiler-1.0.1.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 30000 stop
    java -cp /opt/tprofiler/tprofiler-1.0.1.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 30000 status
    stop
    随时启动以继续采集:
    java -cp /opt/tprofiler/tprofiler-1.0.1.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 30000 start
    java -cp /opt/tprofiler/tprofiler-1.0.1.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 30000 status
    running

    3.3. 刷出数据

    java -cp /opt/tprofiler/tprofiler-1.0.1.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 30000 flushmethod
    会将数据刷出到 ~/logs/ 目录下:
    TProfiler的日志.png

    4. TProfiler 对性能方法的采集

    4.1. 普通方法、线程统计

    java -cp /opt/tprofiler/tprofiler-1.0.1.jar com.taobao.profile.analysis.SamplerLogAnalysis ~/logs/tsampler.log ~/logs/method.log ~/logs/thread.log

    4.2. top 统计

    java -cp /opt/tprofiler/tprofiler-1.0.1.jar com.taobao.profile.analysis.ProfilerLogAnalysis ~/logs/tprofiler.log ~/logs/tmethod.log ~/logs/topmethod.log ~/logs/topobject.log
    方法执行时间统计:这个非常非常重要,这个是 TProfiler 最最重要的 feature,是其能够傲视所有其他性能测试类(包括 jvm 性能测试类)软件的关键所在,我们将会不止一次地在关键的时候受益于 TProfiler 的这一非常有用的特性。
    上述命令刷出的 topmethod.log 部分结果如下:
    com/defonds/core/ppts/common/support/JsonUtils:object2jsonString:123 13519 154 2083584
    com/caucho/hessian/client/HessianURLConnection:sendRequest:156 15894 130 2072565
    com/defonds/rest/core/client/proxy/ResourceJsonInvocationHandler:invoke:39 8123 113 921340
    com/defonds/core/ppts/cache/service/impl/MerBankCfgServiceImpl:selectMerBankCfgByParams:72 54213 15 799322
    com/defonds/core/ppts/incomes/biz/sinopay/service/impl/SinoPayBankReturnServiceImpl4Json:updateOrderSuccess:792 2495 176 438542
    com/defonds/core/ppts/common/support/framework/bean/Message:<init>:76 6219 26 163741
    com/fasterxml/jackson/databind/ser/impl/IndexedListSerializer:serializeContents:107 51883 3 145556
    com/defonds/core/ppts/cache/biz/cims/impl/AccountPrdAndBankCacheImpl:selectBasicProductCfg:144 16131 8 137029
    格式说明:方法信息 执行次数 平均执行时间(单位:ms) 全部执行时间(单位:ms)

    5. 性能方法的优化

    根据 topmethod.log 统计结果,我们拿到了热点方法 top10:

    热点方法 top10
    方法名 被调用次数 平均执行时间(ms) 采样内总执行时间(ms)
    com/defonds/core/ppts/common/support/JsonUtils:object2jsonString:123 13519 154 2083584
    com/caucho/hessian/client/HessianURLConnection:sendRequest:156 15894 130 2072565
    com/defonds/rest/core/client/proxy/ResourceJsonInvocationHandler:invoke:39 8123 113 921340
    com/defonds/core/ppts/cache/service/impl/MerBankCfgServiceImpl:selectMerBankCfgByParams:72 54213 15 799322
    com/defonds/core/ppts/incomes/biz/sinopay/service/impl/SinoPayBankReturnServiceImpl4Json:updateOrderSuccess:792 2495 176 438542
    com/defonds/core/ppts/common/support/framework/bean/Message:<init>:76 6219 26 163741
    com/fasterxml/jackson/databind/ser/impl/IndexedListSerializer:serializeContents:107 51883 3 145556
    com/defonds/core/ppts/cache/biz/cims/impl/AccountPrdAndBankCacheImpl:selectBasicProductCfg:144 16131 8 137029
    com/defonds/core/ppts/common/jms/retrieve/listener/DefaultMessageListener:handleMessage:64 2981 46 136180
    com/fasterxml/jackson/databind/ser/BeanPropertyWriter:serializeAsField:573 53892 2 112553


    这是压测时根据多次采样结果,拣选出的一次比较有代表性的一次。红色部分值得我们去重点关注并优化一下,因为极有可能就是应用瓶颈所在。这些代码要么是导致平均响应时间低下的一些点,要么是导致大量临时对象产生的一些点。
    对于上篇博客中的结论,这些代码的调优原则是:临时对象能改成静态对象进行复用就改成公用对象否则要想方设法缩短其生命周期;高频访问代码提高响应速度。根据 jvm gc 日志发现很多 young gc 之后堆内存已用空间不仅下降反而上升至最大使用量导致 full gc,临时对象如果可以和其它线程复用的话改成静态对象以减少大量线程 local 对象的产生。
    以排名第一的热点方法 com/defonds/core/ppts/common/support/JsonUtils:object2jsonString:123 为例,看看如何来进行调优。

    
     
    1. import org.codehaus.jackson.map.ObjectMapper;

    2. public static <T> String object2jsonString(T t) {

    3. try {

    4. ObjectMapper objectMapper = instanceObjectMapper();

    5. return objectMapper.writeValueAsString(t);

    6. } catch (JsonParseException e) {

    7. log.error(e.getMessage(), e);

    8. throw new SysException(e);

    9. } catch (JsonMappingException e) {

    10. log.error(e.getMessage(), e);

    11. throw new SysException(e);

    12. } catch (IOException e) {

    13. log.error(e.getMessage(), e);

    14. throw new SysException(e);

    15. }

    16. }

    17. public static ObjectMapper instanceObjectMapper() {

    18. JsonFactory jf = new JsonFactory();

    19. jf.configure(Feature.WRITE_NUMBERS_AS_STRINGS, true);

    20. return new ObjectMapper(jf);

    21. }


    该热点方法的优化建议:
    这个方法平均调用时间在 154ms,如果在低并发时可能比这要小得多。但是高并发时可能要等待 GC 的堆内存释放、GC 作业时对业务线程造成的暂停时间等因素影响,这个时间会被无限放大。

    5.1. 临时对象改成静态对象

    object2jsonString 方法的 objectMapper 对象,instanceObjectMapper 方法的 jf 对象;

    5.2. json 处理由 jackson 改为 fastjson

    jackson 和 spring 整合的很好,提供的功能点很多很强大。但是其性能未必靠得住。
    比如我们原来用过谷歌的 Gson 进行 json 处理,某个大对象的 json 解析使用 gson 是 100 多秒,而换成 fastjson 解析后是 900 多毫秒。上百倍的性能差距呀,这还是在单用户操作、不能存在 CPU 和内存等资源限制及竞争的情况下拿到的数据。在此向贡献出 fastjson 的阿里人致敬~

    5.3. 频繁 GC 的瓶颈已不复存在

    针对 TProfiler 帮我们在海量业务代码中定位到的 top5 性能代码进行优化后,部署重新压测,50 个用户并发两个小时左右,我们拉了几次快照,上篇博客中定位的频繁 GC 的性能瓶颈已不复存在,TRT 也由上篇博客优化到的 2.5 下降到 0.5,TPS 基本能稳定在 100 个。问题圆满解决。

    6. 需要注意的一些问题

    6.1. TProfiler 端口号是否已被占用

    为 TProfiler 选取端口号之前要先检测一下该端口号是否已被占用:
    netstat -an | grep 30000

    6.2. TProfiler 配置里 includePackageStartsWith 

    一定要根据你自己的系统进行实际更改,不然就会遇到《TProfiler.log的内容为空 #33》的问题,截图如下:
    TProfiler.log的内容为空.png

    6.3. 几个命令配合使用

    在压测的时候,结合使用 start、stop、flushmethod、ProfilerLogAnalysis topmethod 等几个命令,以拿到关键性的结果。如果能再结合 Weblogic、LoadRunner 的启动、停止,效果最佳。不然的话,如果 JVM 已经跑了很多天,拿到的数据可能不是你想要的,反而会误导你南辕北辙。

    7. 后记

    总体来讲,TProfiler 配置部署、远程操作、日志阅读都不太复杂,操作还是很简单的。但是其却是能够起到一针见血、立竿见影的效果,帮我们解决了 GC 过于频繁的性能瓶颈。
    TProfiler 最重要的特性就是能够统计出你指定时间段内 JVM 的 topmethod,这些 topmethod 极有可能就是造成你 JVM 性能瓶颈的元凶。这是其他大多数 JVM 调优工具所不具备的,包括 JRockit Mission Control。JRokit 首席开发者 Marcus Hirt 在其私人博客《Low Overhead Method Profiling with Java Mission Control》下的评论中曾明确指出 JRMC 并不支持 TOP 方法的统计:
    JRMC并不支持TOP方法的统计.png
    最后再次向具备开源精神的阿里技术团队致敬~

    参考资料

    • TProfiler是一个可以在生产环境长期使用的性能分析工具
    • Low Overhead Method Profiling with Java Mission Control
    • 微信公众号

      个人公众号:程序员黄小斜

      微信公众号【程序员黄小斜】新生代青年聚集地,程序员成长充电站。作者黄小斜,职业是阿里程序员,身份是斜杠青年,希望和更多的程序员交朋友,一起进步和成长!专注于分享技术、面试、职场等成长干货,这一次,我们一起出发。

      关注公众号后回复“2019”领取我这两年整理的学习资料,涵盖自学编程、求职面试、算法刷题、Java技术学习、计算机基础和考研等8000G资料合集。

      技术公众号:Java技术江湖

      微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,专注于 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!

      关注公众号后回复“PDF”即可领取200+页的《Java工程师面试指南》强烈推荐,几乎涵盖所有Java工程师必知必会的知识点。

    展开全文
  • 前言 本次邀请的大佬十余年Java行业经验。曾就职于招商银行互联网...JVM是 Java Virtual Machine(Java虚拟机) 的缩写,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一

    前言

    本次邀请的大佬十余年Java行业经验。曾就职于招商银行互联网事业部58同城互联网金融等行业,有丰富的大型项目设计与建设经验。 主要对分布式架构、微服务、数据安全等领域有深入的研究及改造经验。

    犹豫大佬不愿意透露姓名,这里我就不给大佬透底了!为了让大佬分享一些他的实战经验,我可是大出血了一回!!!

    大家应该都知道什么是 JVM 吧!

    JVMJava Virtual Machine(Java虚拟机) 的缩写,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行,这也是Java能够 “一次编译,到处运行的” 原因。

    详细的介绍可以去看我之前的一篇文章!JVM知识点详解!

    正文

    项目介绍

    代码介绍


    在 Linux 服务跑起来

    java -cp ref-jvm3.jar -XX:+PrintGC -Xms200M -Xmx200M ex13.FullGCProblem
    

    CPU 占用过高排查实战

    1. 先通过 top 命令找到消耗 cpu 很高的进程 id 假设是 2732

    top 命令是我们在 Linux 下最常用的命令之一,它可以实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息。其中上半部分显示的是系统的统计信息,下半部分显示的是进程的使用率统计信息。

    1. 执行 top -p 2732 单独监控该进程
    2. 在第 2 步的监控界面输入 H,获取当前进程下的所有线程信息

    1. 找到消耗 cpu 特别高的线程编号,假设是 2734(要等待一阵)
    2. 执行 jstack 2732 对当前的进程做 dump,输出所有的线程信息

    1. 将第 4 步得到的线程编号 2734 转成 16 进制是 AAE


    也可以通过计算器来换算。

    1. 根据第 6 步得到的 0x7b 在第 5 步的线程信息里面去找对应线程内容
    2. 解读线程信息,定位具体代码位置

    发现找是 VM 的线程占用过高,我们发现我开启的参数中,有垃圾回收的日志显示,所以我们要换一个思路,可能是我们的业务线程没问题,而是垃圾回收的导致的。
    代码中有打印 GC 参数,生产上可以使用这个 jstat –gc 来统计,达到类似的效果

    是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载内存垃圾收集JIT 编译等运行数据,在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。

    假设需要每 250 毫秒查询一次进程 13616 垃圾收集状况,一共查询 10 次,那命令应当是:jstat-gc 13616 250010

    使用这个大量的 FullGC
    还抛出了 OUT Of Memory

    • S0C:第一个幸存区的大小
    • S1C:第二个幸存区的大小
    • S0U:第一个幸存区的使用大小
    • S1U:第二个幸存区的使用大小
    • EC:伊甸园区的大小
    • EU:伊甸园区的使用大小
    • OC:老年代大小
    • OU:老年代使用大小
    • MC:方法区大小
    • MU:方法区使用大小
    • CCSC:压缩类空间大小
    • CCSU:压缩类空间使用大小
    • YGC:年轻代垃圾回收次数
    • YGCT:年轻代垃圾回收消耗时间
    • FGC:老年代垃圾回收次数
    • FGCT:老年代垃圾回收消耗时间
    • GCT:垃圾回收消耗总时间

    怎么办?OOM 了。

    我们可以看到,这个里面 CPU 占用过高是什么导致的?

    是业务线程吗?不是的,这个是 GC 线程占用过高导致的JVM 在疯狂的进行垃圾回收,再回顾下之前的知识,JVM 中默认的垃圾回收器是多线程的(回顾下之前的知识),所以多线程在疯狂回收,导致 CPU 占用过高

    内存占用过高内存占用过高思路

    用于生成堆转储快照(一般称为 heapdumpdump 文件)。jmap 的作用并不仅仅是为了获取 dump 文件,它还可以查询 finalize 执行队列Java 堆永久代的详细信息,如空间使用率当前用的是哪种收集器等。和 jinfo 命令一 样,jmap 有不少功能在 Windows 平台下都是受限的,除了生成 dump 文件的 -dump 选项和用于查看每个类的实例、 空间占用统计的 -histo 选项在所有操作系统都提供之外

    JVM 中的对象全部打印出来, 但是这样太多了,那么我们选择前 20 的对象展示出来, jmap –histo 1196 | head -20


    定位问题的关键,就是这条命令。

    很多个 88 万个对象。

    问题总结(找到问题)

    一般来说,前面这几行,就可以看出,到底是哪些对象占用了内存。

    这些对象回收不掉吗?是的,这些对象回收不掉,这些对象回收不掉,导致了FullGC,里面还有 OutOfMemory



    任务数多于线程数,那么任务会进入阻塞队列,就是一个队列,你进去,排队,有机会了,你就上来跑。

    但是同学们,因为代码中任务数一直多于线程数,所以每 0.1S,就会有 50 个任务进入阻塞对象,50 个任务底下有对象,至少对象送进去了,但是没执行。

    所以导致对象一直都在,同时还回收不了。

    为什么回收不了。Executor 是一个 GCroots

    所以堆中,就会有对象 80 万个,阻塞队列中 80 万个任务,futureTask。并且这些对象还回收不了。

    总结

    在 JVM 出现性能问题的时候。(表现上是 CPU100%,内存一直占用)

    1. 如果 CPU 的 100%,要从两个角度出发,一个有可能是业务线程疯狂运行,比如说想很多死循环。还有一种可能性,就是 GC 线程在疯狂的回收,因为 JVM 中垃圾回收器主流也是多线程的,所以很容易导致 CPU 的 100%
    2. 在遇到内存溢出的问题的时候,一般情况下我们要查看系统中哪些对象占用得比较多,我的是一个很简单的代码,在实际的业务代码中,找到对应的对象,分析对应的类,找到为什么这些对象不能回收的原因,就是我们前面讲过的可达性分析算法JVM 的内存区域,还有垃圾回收器的基础,当然,如果遇到更加复杂的情况,你 要掌握的理论基础远远不止这些(JVM 很多理论都是排查问题的关键)

    看在我大出血给大家带来好东西的份上,最后来一个一键三连不过分吧!!!

    展开全文
  • 2.JVM GC怎么判断对象可以被回收了? 3.JVM GC什么时候执行? 4.按代的垃圾回收机制 5.新生代空间的构成与逻辑 6.JVM GC 算法讲解 7.垃圾回收器简介 8.拓展收集器(新生代、老年代、G1) 9.调优jvm参数介绍 ...
  • 我所在的公司基本上是没有机会进行JVM参数调优的,但是如果有些东西自己不亲身经历一下,看再多的理论知识也只能算是纸上谈兵,真正碰到问题的时候还是不知道该怎么分析。所以就自己制造一些问题然后看其现象,利用...
  • JVM(转)jvm性能调优

    2012-04-08 11:22:32
    jvm性能调优 --------------------------------、 你们的堆栈是怎么记住的啊,stack是栈,栈比堆笔画少,那么就存基本数据和引用,堆大就存数组和对象。 栈用计算,堆存储。 -------------------------------- ...
  • JVM 性能调优实战之:一次系统性能瓶颈的寻找过程

    万次阅读 多人点赞 2016-09-20 17:39:18
    玩过性能优化的朋友都清楚,性能优化的关键...笔者呆过几家技术力量不算弱的公司,每个公司内部真正能够进行 JVM 性能调优的人寥寥无几、甚至没有。如是乎,能够有效通过 JVM 调优提升系统性能的人往往被人们冠以"大牛
  • JVM垃圾收集器(Java Garbage Collection)。本教程均在JDK1.8+HotSpot为例来讲解的.先来看看Java7的:编辑​再来看看Jva8的编辑​从上图中我们可以看出,java8之后换成了元空间。那么怎么证明,堆区是有新生代、永久...
  • Jvm调优需要我们对系统有所了解,其中比较关键的是对核心业务的理解,特别是会造成频繁GC的部分,比如高并发造成的不及时回收。 要知道为什么会造成频繁GC,首先我们要懂怎么估算java类的大小 下面列举各个...
  • JVM 性能调优实战之:一次系统性能瓶颈的寻找过程 &nbsp; 玩过性能优化的朋友都清楚,性能优化的关键并不在于怎么进行优化,而在于怎么找到当前系统的性能瓶颈。 性能优化分为好几个层次,比如系统层次、算法...
  • 前几期我们讲启动类、扩展类、应用程序类加载器详解、如何手写定义类加载器https://zhuanlan.zhihu.com/p/146612533​zhuanlan.zhihu.com一日之计始于晨:【性能调优专题】【Jvm性能调优】【JVM【类加载机制详解】...
  • 简介:堆内存分配⽅式,分配规则讲解 对象分配的规则有哪些 对象主要分配在新⽣代的 Eden 区上 如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配 少数情况下也可能会直接分配在⽼年代中 ...
  • JVM性能调优(一)

    2017-11-25 15:06:04
    这篇文章(这个系列的第一部分)讲述了经典Java虚拟机是怎么样工作的,例如:Java一次编写的利弊,跨平台引擎,垃圾回收基础知识,经典的GC算法和编译优化。之后的文章会讲JVM性能优化,包括最新的JVM设计——支持...
  • 玩过性能优化的朋友都清楚,性能优化...笔者呆过几家技术力量不算弱的公司,每个公司内部真正能够进行 JVM 性能调优的人寥寥无几、甚至没有。如是乎,能够有效通过 JVM 调优提升系统性能的人往往被人们冠以”大牛”、”
  • 对于开发同学来说,Java 是大家耳熟能详的编程语言,而能运行 Java 的程序正是 JVM 虚拟机。纸上得来终觉浅,绝知此事要躬行。如果有些东西自己不亲身经历一下,看再多的理论知识也只能算是纸上谈兵,真正碰到问题的...
  • top 结果怎么看 2、查看GC统计信息 jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数] jstat -gcutil 11802 2000 -options vmid 采集时间间隔ms jstat -help 采集结果 S0:幸存1区当前使用比例 ...
  • 069:jvm垃圾收集算法与垃圾收集器1 回收对象判断之引用计数法2 回收对象判断之根...jvm怎么判断一个对象是否要被回收?(对象是否为垃圾) 引用计数法(当前对象有没有被引用) 引用计数法:如果一个对象没有被任何引
  • JVM调优概述

    2021-02-05 14:04:03
    三面:JVM如何调优、参数怎么调? 拼多多: 从SQL、JVM、架构、数据库四个方面讲讲优化思路 京东: JVM诊断调优工具用过哪些? 每秒几十万并发的秒杀系统为什么会频繁发生GC? 日均百万级交易系统如何优化JVM? 线上...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 718
精华内容 287
关键字:

jvm怎么调优