当前位置: 澳门新濠3559 > 服务器运维 > 正文

在业务应用层面,作为一名 Java

时间:2019-11-08 22:07来源:服务器运维
Java 线上问题排查思路与工具使用,java排查 本文来自作者 蓬蒿 在  GitChat  上分享 「Java线上问题排查思路与工具使用」,「阅读原文」查看交流实录。 「文末高能」 编辑 | 哈比 1、

Java 线上问题排查思路与工具使用,java排查

本文来自作者 蓬蒿 在 GitChat 上分享 「Java 线上问题排查思路与工具使用」,「阅读原文」查看交流实录。

「文末高能」

编辑 | 哈比

1、内存管理 - 栈 or 堆

    澳门新濠3559 1

Java调优经验谈

对于调优这个事情来说,一般就是三个过程:

 

  • 性能监控:问题没有发生,你并不知道你需要调优什么?此时需要一些系统、应用的监控工具来发现问题。

     

  • 性能分析:问题已经发生,但是你并不知道问题到底出在哪里。此时就需要使用工具、经验对系统、应用进行瓶颈分析,以求定位到问题原因。

     

  • 性能调优:经过上一步的分析定位到了问题所在,需要对问题进行解决,使用代码、配置等手段进行优化。

 

Java调优也不外乎这三步。

 

此外,本文所讲的性能分析、调优等是抛开以下因素的:

 

  • 系统底层环境:硬件、操作系统等

  • 数据结构和算法的使用

  • 外部系统如数据库、缓存的使用

 

调优准备

 

调优是需要做好准备工作的,毕竟每一个应用的业务目标都不尽相同,性能瓶颈也不会总在同一个点上。在业务应用层面,我们需要:

 

  • 需要了解系统的总体架构,明确压力方向。比如系统的哪一个接口、模块是使用率最高的,面临高并发的挑战。

     

  • 需要构建测试环境来测试应用的性能,使用ab、loadrunner、jmeter都可以。

     

  • 对关键业务数据量进行分析,这里主要指的是对一些数据的量化分析,如数据库一天的数据量有多少;缓存的数据量有多大等

     

  • 了解系统的响应速度、吞吐量、TPS、QPS等指标需求,比如秒杀系统对响应速度和QPS的要求是非常高的。

     

  • 了解系统相关软件的版本、模式和参数等,有时候限于应用依赖服务的版本、模式等,性能也会受到一定的影响。

 

此外,我们还需要了解Java相关的一些知识:

 

  1. Java内存相关:这一部分可以参见谈谈Java内存管理一文

     

  2. 对Java代码进行基准性能测试:可以使用JMH来进行,[译]使用JMH进行微基准测试:不要猜,要测试!

     

  3. HotSpot VM相关知识:

     

  4. jdk自带各种java工具:

 

性能分析

 

在系统层面能够影响应用性能的一般包括三个因素:CPU、内存和IO,可以从这三方面进行程序的性能瓶颈分析。

 

CPU分析

 

当程序响应变慢的时候,首先使用top、vmstat、ps等命令查看系统的cpu使用率是否有异常,从而可以判断出是否是cpu繁忙造成的性能问题。其中,主要通过us(用户进程所占的%)这个数据来看异常的进程信息。当us接近100%甚至更高时,可以确定是cpu繁忙造成的响应缓慢。一般说来,cpu繁忙的原因有以下几个:

 

  • 线程中有无限空循环、无阻塞、正则匹配或者单纯的计算

  • 发生了频繁的gc

  • 多线程的上下文切换

 

确定好cpu使用率最高的进程之后就可以使用jstack来打印出异常进程的堆栈信息:

 

jstack [pid]

 

澳门新濠3559 2

 

在业务应用层面,作为一名 Java。接下来需要注意的一点是,Linux下所有线程最终还是以轻量级进程的形式存在系统中的,而使用jstack只能打印出进程的信息,这些信息里面包含了此进程下面所有线程(轻量级进程-LWP)的堆栈信息。因此,进一步的需要确定是哪一个线程耗费了大量cpu,此时可以使用top -p [processId]来查看,也可以直接通过ps -Le来显示所有进程,包括LWP的资源耗费信息。最后,通过在jstack的输出文件中查找对应的lwp的id即可以定位到相应的堆栈信息。其中需要注意的是线程的状态:RUNNABLE、WAITING等。对于Runnable的进程需要注意是否有耗费cpu的计算。对于Waiting的线程一般是锁的等待操作。

 

也可以使用jstat来查看对应进程的gc信息,以判断是否是gc造成了cpu繁忙。

 

jstat -gcutil [pid]

 

澳门新濠3559 3

 

还可以通过vmstat,通过观察内核状态的上下文切换(cs)次数,来判断是否是上下文切换造成的cpu繁忙。

 

vmstat 1 5

 

澳门新濠3559 4

 

此外,有时候可能会由jit引起一些cpu飚高的情形,如大量方法编译等。这里可以使用-XX:+PrintCompilation这个参数输出jit编译情况,以排查jit编译引起的cpu问题。

 

内存分析

 

对Java应用来说,内存主要是由堆外内存和堆内内存组成。

 

1. 堆外内存堆外内存主要是JNI、Deflater/Inflater、DirectByteBuffer(nio中会用到)使用的。对于这种堆外内存的分析,还是需要先通过vmstat、sar、top、pidstat等查看swap和物理内存的消耗状况再做判断的。此外,对于JNI、Deflater这种调用可以通过Google-preftools来追踪资源使用状况。

 

2. 堆内内存此部分内存为Java应用主要的内存区域。通常与这部分内存性能相关的有:

 

  • 创建的对象:这个是存储在堆中的,需要控制好对象的数量和大小,尤其是大的对象很容易进入老年代

     

  • 全局集合:全局集合通常是生命周期比较长的,因此需要特别注意全局集合的使用

     

  • 缓存:缓存选用的数据结构不同,会很大程序影响内存的大小和gc

     

  • ClassLoader:主要是动态加载类容易造成永久代内存不足

     

  • 多线程:线程分配会占用本地内存,过多的线程也会造成内存不足

 

以上使用不当很容易造成:

 

  • 频繁GC -> Stop the world,使你的应用响应变慢

  • OOM,直接造成内存溢出错误使得程序退出。OOM又可以分为以下几种:

  • Heap space:堆内存不足

  • PermGen space:永久代内存不足

  • Native thread:本地线程没有足够内存可分配

 

排查堆内存问题的常用工具是jmap,是jdk自带的。一些常用用法如下:

 

  • 查看jvm内存使用状况:jmap -heap

  • 查看jvm内存存活的对象:jmap -histo:live

  • 把heap里所有对象都dump下来,无论对象是死是活:jmap -dump:format=b,file=xxx.hprof

  • 先做一次full GC,再dump,只包含仍然存活的对象信息:jmap -dump:format=b,live,file=xxx.hprof

 

此外,不管是使用jmap还是在OOM时产生的dump文件,可以使用Eclipse的MAT(MEMORY ANALYZER TOOL)来分析,可以看到具体的堆栈和内存中对象的信息。当然jdk自带的jhat也能够查看dump文件,会启动web端口供开发者使用浏览器浏览堆内对象的信息。

 

澳门新濠3559 5

 

IO分析

 

通常与应用性能相关的包括:文件IO和网络IO。

 

1. 文件IO可以使用系统工具pidstat、iostat、vmstat来查看io的状况。这里可以看一张使用vmstat的结果图。

 

澳门新濠3559 6

 

这里主要注意bi和bo这两个值,分别表示块设备每秒接收的块数量和块设备每秒发送的块数量,由此可以判定io繁忙状况。进一步的可以通过使用strace工具定位对文件io的系统调用。通常,造成文件io性能差的原因不外乎:

 

  • 大量的随机读写

  • 设备慢

  • 文件太大

 

1. 网络IO查看网络io状况,一般使用的是netstat工具。可以查看所有连接的状况、数目、端口信息等。例如:当time_wait或者close_wait连接过多时,会影响应用的相应速度。

 

netstat -anp

 

澳门新濠3559 7

 

此外,还可以使用tcpdump来具体分析网络io的数据。当然,tcpdump出的文件直接打开是一堆二进制的数据,可以使用wireshark阅读具体的连接以及其中数据的内容。

 

tcpdump -i eth0 -w tmp.cap -tnn dst port 8080 #监听8080端口的网络请求并打印日志到tmp.cap中

 

还可以通过查看/proc/interrupts来获取当前系统使用的中断的情况。

 

澳门新濠3559 8

 

各个列依次是:

 

irq的序号, 在各自cpu上发生中断的次数,可编程中断控制器,设备名称(request_irq的dev_name字段)

 

通过查看网卡设备的终端情况可以判断网络io的状况。

 

其他分析工具

 

上面分别针对CPU、内存以及IO讲了一些系统/JDK自带的分析工具。除此之外,还有一些综合分析工具或者框架可以更加方便我们对Java应用性能的排查、分析、定位等。

 

  • VisualVM这个工具应该是Java开发者们非常熟悉的一款java应用监测工具,原理是通过jmx接口来连接jvm进程,从而能够看到jvm上的线程、内存、类等信息。

 

澳门新濠3559 9

 

如果想进一步查看gc情况,可以安装visual gc插件。此外,visualvm也有btrace的插件,可以可视化直观的编写btrace代码并查看输出日志。 与VisualVm类似的,jconsole也是通过jmx查看远程jvm信息的一款工具,更进一步的,通过它还可以显示具体的线程堆栈信息以及内存中各个年代的占用情况,也支持直接远程执行MBEAN。当然,visualvm通过安装jconsole插件也可以拥有这些功能。

 

澳门新濠3559 10

 

但由于这俩工具都是需要ui界面的,因此一般都是通过本地远程连接服务器jvm进程。服务器环境下,一般并不用此种方式。

 

  • Java Mission Control(jmc)此工具是jdk7 u40开始自带的,原来是JRockit上的工具,是一款采样型的集诊断、分析和监控与一体的非常强大的工具。

 

澳门新濠3559 11

 

  • Btrace这里不得不提的是btrace这个神器,它使用java attach api+ java agent + instrument api能够实现jvm的动态追踪。在不重启应用的情况下可以加入拦截类的方法以打印日志等。具体的用法可以参考Btrace入门到熟练小工完全指南。

 

  • JwebapJwebap是一款JavaEE性能检测框架,基于asm增强字节码实现。支持:http请求、jdbc连接、method的调用轨迹跟踪以及次数、耗时的统计。由此可以获取最耗时的请求、方法,并可以查看jdbc连接的次数、是否关闭等。但此项目是2006年的一个项目,已经将近10年没有更新。根据笔者使用,已经不支持jdk7编译的应用。如果要使用,建议基于原项目二次开发,同时也可以加入对redis连接的轨迹跟踪。当然,基于字节码增强的原理,也可以实现自己的JavaEE性能监测框架。

 

澳门新濠3559 12

 

上图来自笔者公司二次开发过的jwebap,已经支持jdk8和redis连接追踪。

 

  • useful-scripts这里有一个本人参与的开源的项目:

 

性能调优

 

与性能分析相对应,性能调优同样分为三部分。

 

CPU调优

 

  • 不要存在一直运行的线程(无限while循环),可以使用sleep休眠一段时间。这种情况普遍存在于一些pull方式消费数据的场景下,当一次pull没有拿到数据的时候建议sleep一下,再做下一次pull。

     

  • 轮询的时候可以使用wait/notify机制

     

  • 避免循环、正则表达式匹配、计算过多,包括使用String的format、split、replace方法(可以使用apache的commons-lang里的StringUtils对应的方法),使用正则去判断邮箱格式(有时候会造成死循环)、序列/反序列化等。

     

  • 结合jvm和代码,避免产生频繁的gc,尤其是full GC。

 

此外,使用多线程的时候,还需要注意以下几点:

 

  • 使用线程池,减少线程数以及线程的切换

     

  • 多线程对于锁的竞争可以考虑减小锁的粒度(使用ReetrantLock)、拆分锁(类似ConcurrentHashMap分bucket上锁), 或者使用CAS、ThreadLocal、不可变对象等无锁技术。此外,多线程代码的编写最好使用jdk提供的并发包、Executors框架以及ForkJoin等,此外Discuptor和Actor在合适的场景也可以使用。

 

内存调优

 

内存的调优主要就是对jvm的调优。

 

  • 合理设置各个代的大小。避免新生代设置过小(不够用,经常minor gc并进入老年代)以及过大(会产生碎片),同样也要避免Survivor设置过大和过小。

     

  • 选择合适的GC策略。需要根据不同的场景选择合适的gc策略。这里需要说的是,cms并非全能的。除非特别需要再设置,毕竟cms的新生代回收策略parnew并非最快的,且cms会产生碎片。此外,G1直到jdk8的出现也并没有得到广泛应用,并不建议使用。

     

  • jvm启动参数配置-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:[log_path],以记录gc日志,便于排查问题。

 

其中,对于第一点,具体的还有一点建议:

 

  • 年轻代大小选择:响应时间优先的应用,尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生gc的频率是最小的。同时,也能够减少到达年老代的对象。吞吐量优先的应用,也尽可能的设置大,因为对响应时间没有要求,垃圾收集可以并行进行,建议适合8CPU以上的应用使用。

     

  • 年老代大小选择:响应时间优先的应用,年老代一般都是使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

 

  • 并发垃圾收集信息

  • 持久代并发收集次数

  • 传统GC信息

  • 花在年轻代和年老代回收上的时间比例

 

一般吞吐量优先的应用都应该有一个很大的年轻代和一个较小的年老代。这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代存放长期存活对象。

 

此外,较小堆引起的碎片问题:因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:-XX:+UseCMSCompactAtFullCollection,使用并发收集器时,开启对年老代的压缩。同时使用-XX:CMSFullGCsBeforeCompaction=xx设置多少次Full GC后,对年老代进行压缩。

 

其余对于jvm的优化问题可见后面JVM参数进阶一节。

 

代码上,也需要注意:

 

  • 避免保存重复的String对象,同时也需要小心String.subString()与String.intern()的使用

  • 尽量不要使用finalizer

  • 释放不必要的引用:ThreadLocal使用完记得释放以防止内存泄漏,各种stream使用完也记得close。

  • 使用对象池避免无节制创建对象,造成频繁gc。但不要随便使用对象池,除非像连接池、线程池这种初始化/创建资源消耗较大的场景,

  • 缓存失效算法,可以考虑使用SoftReference、WeakReference保存缓存对象

  • 谨慎热部署/加载的使用,尤其是动态加载类等

 

不要用Log4j输出文件名、行号,因为Log4j通过打印线程堆栈实现,生成大量String。此外,使用log4j时,建议此种经典用法,先判断对应级别的日志是否打开,再做操作,否则也会生成大量String。

 

if (logger.isInfoEnabled()) {

      logger.info(msg);

  }

 

IO调优

 

文件IO上需要注意:

 

  • 考虑使用异步写入代替同步写入,可以借鉴redis的aof机制。

  • 利用缓存,减少随机读

  • 尽量批量写入,减少io次数和寻址

  • 使用数据库代替文件存储

 

网络IO上需要注意:

 

  • 和文件IO类似,使用异步IO、多路复用IO/事件驱动IO代替同步阻塞IO

  • 批量进行网络IO,减少IO次数

  • 使用缓存,减少对网络数据的读取

  • 使用协程: Quasar

 

其他优化建议

 

  • 算法、逻辑上是程序性能的首要,遇到性能问题,应该首先优化程序的逻辑处理

  • 优先考虑使用返回值而不是异常表示错误

  • 查看自己的代码是否对内联是友好的: 你的Java代码对JIT编译友好么?

 

此外,jdk7、8在jvm的性能上做了一些增强:

 

  • 通过-XX:+TieredCompilation开启JDK7的多层编译(tiered compilation)支持。多层编译结合了客户端C1编译器和服务端C2编译器的优点(客户端编译能够快速启动和及时优化,服务器端编译可以提供更多的高级优化),是一个非常高效利用资源的切面方案。在开始时先进行低层次的编译,同时收集信息,在后期再进一步进行高层次的编译进行高级优化。需要注意的一点:这个参数会消耗比较多的内存资源,因为同一个方法被编译了多次,存在多份native内存拷贝,建议把code cache调大一点儿(-XX:+ReservedCodeCacheSize,InitialCodeCacheSize)。否则有可能由于code cache不足,jit编译的时候不停的尝试清理code cache,丢弃无用方法,消耗大量资源在jit线程上。

     

  • Compressed Oops:压缩指针在jdk7中的server模式下已经默认开启。

     

  • Zero-Based Compressed Ordinary Object Pointers:当使用了上述的压缩指针时,在64位jvm上,会要求操作系统保留从一个虚拟地址0开始的内存。如果操作系统支持这种请求,那么就开启了Zero-Based Compressed Oops。这样可以使得无须在java堆的基地址添加任何地址补充即可把一个32位对象的偏移解码成64位指针。

     

  • 逃逸分析(Escape Analysis): Server模式的编译器会根据代码的情况,来判断相关对象的逃逸类型,从而决定是否在堆中分配空间,是否进行标量替换(在栈上分配原子类型局部变量)。此外,也可以根据调用情况来决定是否自动消除同步控制,如StringBuffer。这个特性从Java SE 6u23开始就默认开启。

     

  • NUMA Collector Enhancements:这个重要针对的是The Parallel Scavenger垃圾回收器。使其能够利用NUMA (Non Uniform Memory Access,即每一个处理器核心都有本地内存,能够低延迟、高带宽访问) 架构的机器的优势来更快的进行gc。可以通过-XX:+UseNUMA开启支持。

 

此外,网上还有很多过时的建议,不要再盲目跟随:

 

  • 变量用完设置为null,加快内存回收,这种用法大部分情况下并没有意义。一种情况除外:如果有个Java方法没有被JIT编译但里面仍然有代码会执行比较长时间,那么在那段会执行长时间的代码前显式将不需要的引用类型局部变量置null是可取的。具体的可以见R大的解释:

     

  • 方法参数设置为final,这种用法也没有太大的意义,尤其在jdk8中引入了effective final,会自动识别final变量。

 

JVM参数进阶

 

jvm的参数设置一直是比较理不清的地方,很多时候都搞不清都有哪些参数可以配置,参数是什么意思,为什么要这么配置等。这里主要针对这些做一些常识性的说明以及对一些容易让人进入陷阱的参数做一些解释。

 

以下所有都是针对Oracle/Sun JDK 6来讲

 

1. 启动参数默认值Java有很多的启动参数,而且很多版本都并不一样。但是现在网上充斥着各种资料,如果不加辨别的全部使用,很多是没有效果或者本来就是默认值的。一般的,我们可以通过使用java -XX:+PrintFlagsInitial来查看所有可以设置的参数以及其默认值。也可以在程序启动的时候加入-XX:+PrintCommandLineFlags来查看与默认值不相同的启动参数。如果想查看所有启动参数(包括和默认值相同的),可以使用-XX:+PrintFlagsFinal。输出里“=”表示使用的是初始默认值,而“:=”表示使用的不是初始默认值,可能是命令行传进来的参数、配置文件里的参数或者是ergonomics自动选择了别的值。

 

此外,还可以使用jinfo命令显示启动的参数。

 

  • jinfo -flags [pid] #查看目前启动使用的有效参数

  • jinfo -flag [flagName] [pid] #查看对应参数的值

 

这里需要指出的是,当你配置jvm参数时,最好是先通过以上命令查看对应参数的默认值再确定是否需要设置。也最好不要配置你搞不清用途的参数,毕竟默认值的设置是有它的合理之处的。

 

澳门新濠3559 13

 

动态设置参数当Java应用启动后,定位到了是GC造成的性能问题,但是你启动的时候并没有加入打印gc的参数,很多时候的做法就是重新加参数然后重启应用。但这样会造成一定时间的服务不可用。最佳的做法是能够在不重启应用的情况下,动态设置参数。使用jinfo可以做到这一点(本质上还是基于jmx的)。

 

jinfo -flag [+/-][flagName] [pid] #启用/禁止某个参数 

jinfo -flag [flagName=value] [pid] #设置某个参数

 

对于上述的gc的情况,就可以使用以下命令打开heap dump并设置dump路径。

 

jinfo -flag +HeapDumpBeforeFullGC [pid] 

jinfo -flag +HeapDumpAfterFullGC [pid] 

jinfo -flag HeapDumpPath=/home/dump/dir [pid]

 

同样的也可以动态关闭。

 

jinfo -flag -HeapDumpBeforeFullGC [pid]

jinfo -flag -HeapDumpAfterFullGC [pid]

 

其他的参数设置类似。

 

  1. -verbose:gc 与 -XX:+PrintGCDetails很多gc推荐设置都同时设置了这两个参数,其实,只要打开了-XX:+PrintGCDetails,前面的选项也会同时打开,无须重复设置。

 

3. -XX:+DisableExplicitGC这个参数的作用就是使得system.gc变为空调用,很多推荐设置里面都是建议开启的。但是,如果你用到了NIO或者其他使用到堆外内存的情况,使用此选项会造成oom。可以用XX:+ExplicitGCInvokesConcurrent或XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses(配合CMS使用,使得system.gc触发一次并发gc)代替。此外,还有一个比较有意思的地方。如果你不设置此选项的话,当你使用了RMI的时候,会周期性地来一次full gc。这个现象是由于分布式gc造成的,为RMI服务。具体的可见此链接内容中与dgc相关的:

 

4. MaxDirectMemorySize此参数是设置的堆外内存的上限值。当不设置的时候为-1,此值为-Xmx减去一个survivor space的预留大小。

 

  1. 由于遗留原因,作用相同的参数

 

  • -Xss 与 -XX:ThreadStackSize

  • -Xmn 与 -XX:NewSize,此外这里需要注意的是设置了-Xmn的话,NewRatio就没作用了。

 

6. -XX:MaxTenuringThreshold使用工具查看此值默认值为15,但是选择了CMS的时候,此值会变成4。当此值设置为0时,所有eden里的活对象在经历第一次minor GC的时候就会直接晋升到old gen,survivor space直接就没用。

 

7. -XX:HeapDumpPath使用此参数可以指定-XX:+HeapDumpBeforeFullGC、-XX:+HeapDumpAfterFullGC、-XX:+HeapDumpOnOutOfMemoryError触发heap dump文件的存储位置。

 

参考资料

 

  • Java HotSpot™ Virtual Machine Performance Enhancements

  • Java HotSpot Virtual Machine Garbage Collection Tuning Guide

  • [HotSpot VM] JVM调优的”标准参数”的各种陷阱

一、前言

Java 语言是当前互联网应用最为广泛的语言,作为一名 Java 程序猿,当业务相对比较稳定之后平常工作除了 coding 之外,大部分时间(70%~80%)是会用来排查突发或者周期性的线上问题。

由于业务应用 bug(本身或引入第三方库)、环境原因、硬件问题等原因,Java 线上服务出现故障 / 问题几乎不可避免。例如,常见的现象包括部分请求超时、用户明显感受到系统发生卡顿等等。

尽快线上问题从系统表象来看非常明显,但排查深究其发生的原因还是比较困难的,因此对开发测试或者是运维的同学产生了许多的困扰。

排查定位线上问题是具有一定技巧或者说是经验规律的,排查者如果对业务系统了解得越深入,那么相对来说定位也会容易一些。

不管怎么说,掌握 Java 服务线上问题排查思路并能够熟练排查问题常用工具 / 命令 / 平台是每一个 Java 程序猿进阶必须掌握的实战技能。

笔者依据自己的 工作经验总结出一套基本的线上问题排查流程,同学们可以根据自己的实际工作情况进行归纳总结。

无论是java还是C,内存分配,本质上就是 栈和堆两个类型。简单来说,代码逻辑处理在栈上,数据在堆上。

    对于搞开发的我们其实也是一样,现在流行的框架越来越多,封装的也越来越完善,各种框架可以搞定一切,几乎不用关注底层的实现,初级程序员只要熟悉基本的使用方法,便可以快速的开发上线;但对于高级程序员来讲,内功的修炼却越发的重要,比如算法、设计模式、底层原理等,只有把这些基础熟练之后,才能在开发过程中知其然知其所以然,出现问题时能快速定位到问题的本质。
    对于Java程序员来讲,spring全家桶几乎可以搞定一切,spring全家桶便是精妙的招式,jvm就是内功心法很重要的一块,线上出现性能问题,jvm调优更是不可回避的问题。因此JVM基础知识对于高级程序员的重要性不必言语.
    一.jvm体系总体分四大块:
        1.类的加载机制
        2.jvm内存结构
        3.GC算法 垃圾回收
        4.GC分析 命令调优
    二.类的加载机制
        1.什么是类的加载
        2.类的生命周期
        3.类加载器
        4.双亲委派模型
    三.什么是类的加载
        类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
    四.类的生命周期
        1.加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
        2.连接,连接又包含三块内容:验证、准备、初始化。1)验证,文件格式、元数据、字节码、符号引用验证;2)准备,为类的静态变量分配内存,并将其初始化为默认值;3)解析,把类中的符号引用转换为直接引用
        3.初始化,为类的静态变量赋予正确的初始值
        4.使用,new出对象程序中使用
        5.卸载,执行垃圾回收
    五.类加载器
        1.启动类加载器:Bootstrap ClassLoader,负责加载存放在JDKjrelib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
        2.扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DKjrelibext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
        3.应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
    六.类加载机制
        1.全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
        2.父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
        3.缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
    七.jvm内存结构
        1.方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
        2.Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
        3.方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
        4.程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
        5.JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
        6.本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
    八.对象分配规则
        1.对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
        2.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
        3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
        4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
        5.空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
    九.GC算法
    GC最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。
        1.标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
        2.复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
        3.标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
        4.分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
    十.垃圾回收器
        1.Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
        2.ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
        3.Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
        4.Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
        5.CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
        6.G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
        7.GC算法和垃圾回收器算法图解以及更详细内容参考JVM(3):Java GC算法 垃圾收集器
    十一.GC日志分析
        摘录GC日志一部分(前部分为年轻代gc回收;后部分为full gc回收):
        2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs]
        2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]
        通过上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen属于Parallel收集器。其中PSYoungGen表示gc回收前后年轻代的内存变化;ParOldGen表示gc回收前后老年代的内存变化;PSPermGen表示gc回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁,耗时短;full gc 会对整个堆内存进行回城,耗时长,因此一般尽量减少full gc的次数
    十二.调优命令
    Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
        1.jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
        2.jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
        3.jmap,JVM Memory Map命令用于生成heap dump文件
        4.jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
        5.jstack,用于生成java虚拟机当前时刻的线程快照。
        6.jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
        详细的命令使用参考这里JVM(4):Jvm调优-命令篇
    十三.调优工具
    常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。
        1.jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控
        2.jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
        3.MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
        4.GChisto,一款专业分析gc日志的工具

二、Java 服务常见线上问题

所有 Java 服务的线上问题从系统表象来看归结起来总共有四方面:CPU、内存、磁盘、网络。例如 CPU 使用率峰值突然飚高、内存溢出 (泄露)、磁盘满了、网络流量异常、FullGC 等等问题。

基于这些现象我们可以将线上问题分成两大类: 系统异常、业务服务异常。

I、JVM内存模型

1. 系统异常

常见的系统异常现象包括:  CPU 占用率过高、CPU 上下文切换频率次数较高、磁盘满了、磁盘 I/O 过于频繁、网络流量异常 (连接数过多)、系统可用内存长期处于较低值 (导致 oom killer) 等等。

这些问题可以通过 top(cpu)、free(内存)、df(磁盘)、dstat(网络流量)、pstack、vmstat、strace(底层系统调用) 等工具获取系统异常现象数据。

此外,如果对系统以及应用进行排查后,均未发现异常现象的更笨原因,那么也有可能是外部基础设施如 IAAS 平台本身引发的问题。

例如运营商网络或者云服务提供商偶尔可能也会发生一些故障问题,你的引用只有某个区域如广东用户访问系统时发生服务不可用现象,那么极有可能是这些原因导致的。

今天我司部署在阿里云华东地域的业务系统中午时分突然不能为广东地区用户提供正常服务,对系统进行各种排查均为发现任何问题。

最后,通过查询阿里云公告得知原因是 “ 广东地区电信线路访问华东地区互联网资源(包含阿里云华东 1 地域)出现网络丢包或者延迟增大的异常情况 “。

堆:新生代(Eden,survivor),年老代(Gen) -- 分配对象、数组等

非堆(栈):虚拟机栈,本地方法栈 -- 栈帧 分配局部变量、操作需要的空间比如方法链接

2. 业务服务异常

常见的业务服务异常现象包括: PV 量过高、服务调用耗时异常、线程死锁、多线程并发问题、频繁进行 Full GC、异常安全攻击扫描等。

方法区-(永久代) -- 分配代码、全局变量、静态变量

三、问题定位

我们一般会采用排除法,从外部排查到内部排查的方式来定位线上服务问题。

  • 首先我们要排除其他进程 (除主进程之外) 可能引起的故障问题;

  • 然后排除业务应用可能引起的故障问题;

  • 可以考虑是否为运营商或者云服务提供商所引起的故障。

Object o = new Object()

1. 定位流程

1.1 系统异常排查流程

1.2 业务应用排查流程

首先代码在 方法区中。

2. Linux 常用的性能分析工具

Linux 常用的性能分析工具使用包括 : top(cpu)、free(内存)、df(磁盘)、dstat(网络流量)、pstack、vmstat、strace(底层系统调用) 等。

2.1 CPU

CPU 是系统重要的监控指标,能够分析系统的整体运行状况。监控指标一般包括运行队列、CPU 使用率和上下文切换等。

top 命令是 Linux 下常用的 CPU 性能分析工具 , 能够实时显示系统中各个进程的资源占用状况 , 常用于服务端性能分析。

top 命令显示了各个进程 CPU 使用情况 , 一般 CPU 使用率从高到低排序展示输出。其中 Load Average 显示最近 1 分钟、5 分钟和 15 分钟的系统平均负载,上图各值为 2.46,1.96,1.99。

我们一般会关注 CPU 使用率最高的进程,正常情况下就是我们的应用主进程。第七行以下:各进程的状态监控。

PID : 进程 id
USER : 进程所有者
PR : 进程优先级
NI : nice 值。负值表示高优先级,正值表示低优先级
VIRT : 进程使用的虚拟内存总量,单位 kb。VIRT=SWAP+RES
RES : 进程使用的、未被换出的物理内存大小,单位 kb。RES=CODE+DATA
SHR : 共享内存大小,单位 kb
S : 进程状态。D= 不可中断的睡眠状态 R= 运行 S= 睡眠 T= 跟踪 / 停止 Z= 僵尸进程
%CPU : 上次更新到现在的 CPU 时间占用百分比
%MEM : 进程使用的物理内存百分比
TIME+ : 进程使用的 CPU 时间总计,单位 1/100 秒
COMMAND : 进程名称

2.2 内存

内存是排查线上问题的重要参考依据,内存问题很多时候是引起 CPU 使用率较高的见解因素。

系统内存:free 是显示的当前内存的使用 ,-m 的意思是 M 字节来显示内容。

free -m

部分参数说明:

  total 内存总数: 3790M
  used 已经使用的内存数: 1880M
  free 空闲的内存数: 118M
  shared 当前已经废弃不用 , 总是 0
  buffers Buffer 缓存内存数: 1792M

2.3 磁盘

df -h



du -m /path

2.4 网络

dstat 命令可以集成了 vmstat、iostat、netstat 等等工具能完成的任务。

   dstat -c  cpu 情况
    -d 磁盘读写
        -n 网络状况
        -l 显示系统负载
        -m 显示形同内存状况
        -p 显示系统进程信息
        -r 显示系统 IO 情况

2.5 其它

vmstat:

vmstat 2 10 -t

vmstat 是 Virtual Meomory Statistics(虚拟内存统计)的缩写 , 是实时系统监控工具。该命令通过使用 knlist 子程序和 /dev/kmen 伪设备驱动器访问这些数据,输出信息直接打印在屏幕。

使用 vmstat 2 10  -t 命令,查看 io 的情况 (第一个参数是采样的时间间隔数单位是秒,第二个参数是采样的次数)。

r 表示运行队列 (就是说多少个进程真的分配到 CPU),b 表示阻塞的进程。    
swpd 虚拟内存已使用的大小,如果大于 0,表示你的机器物理内存不足了,如果不是程序内存泄露的原因,那么你该升级内存了或者把耗内存的任务迁移到其他机器。
free   空闲的物理内存的大小,我的机器内存总共 8G,剩余 3415M。
buff   Linux/Unix 系统是用来存储,目录里面有什么内容,权限等的缓存,我本机大概占用 300 多 M
cache 文件缓存
si 列表示由磁盘调入内存,也就是内存进入内存交换区的数量;
so 列表示由内存调入磁盘,也就是内存交换区进入内存的数量
一般情况下,si、so 的值都为 0,如果 si、so 的值长期不为 0,则表示系统内存不足,需要考虑是否增加系统内存。    
bi 从块设备读入数据的总量(读磁盘)(每秒 kb)
bo 块设备写入数据的总量(写磁盘)(每秒 kb)
随机磁盘读写的时候,这两个值越大 ((超出 1024k),能看到 cpu 在 IO 等待的值也会越大
这里设置的 bi+bo 参考值为 1000,如果超过 1000,而且 wa 值比较大,则表示系统磁盘 IO 性能瓶颈。
in 每秒 CPU 的中断次数,包括时间中断
cs(上下文切换 Context Switch)

strace:strace 常用来跟踪进程执行时的系统调用和所接收的信号。

strace -cp tid
strace -T -p tid
    -T 显示每一调用所耗的时间 .
    -p pid  跟踪指定的进程 pid.
    -v 输出所有的系统调用 . 一些调用关于环境变量 , 状态 , 输入输出等调用由于使用频繁 , 默认不输出 .
    -V 输出 strace 的版本信息 .

执行时,Object o 会存放在 java栈 的本地变量表中。

new Object 会在 java堆中。

3. JVM 定位问题工具

在 JDK 安装目录的 bin 目录下默认提供了很多有价值的命令行工具。每个小工具体积基本都比较小,因为这些工具只是 jdklibtools.jar 的简单封装。

其中,定位排查问题时最为常用命令包括:jps(进程)、jmap(内存)、jstack(线程)、jinfo(参数) 等。

  • jps: 查询当前机器所有 JAVA 进程信息;

  • jmap: 输出某个 java 进程内存情况 (如:产生那些对象及数量等);

  • jstack: 打印某个 Java 线程的线程栈信息;

  • jinfo: 用于查看 jvm 的配置参数。

3.1 jps 命令

jps 用于输出当前用户启动的所有进程 ID,当线上发现故障或者问题时,能够利用 jps 快速定位对应的 Java 进程 ID。

jps -l -m
-m -l -l 参数用于输出主启动类的完整路径

当然,我们也可以使用 Linux 提供的查询进程状态命令,例如:

ps -ef | grep tomcat

我们也能快速获取 tomcat 服务的进程 id。

3.2 jmap 命令

jmap -heap pid   输出当前进程 JVM 堆新生代、老年代、持久代等请情况,GC 使用的算法等信息
jmap -histo:live {pid} | head -n 10  输出当前进程内存中所有对象包含的大小
jmap -dump:format=b,file=/usr/local/logs/gc/dump.hprof {pid} 以二进制输出档当前内存的堆情况,然后可以导入 MAT 等工具进行

jmap(Java Memory Map) 可以输出所有内存中对象的工具 , 甚至可以将 VM 中的 heap, 以二进制输出成文本。

jmap -heap pid:

jmap -heap pid   输出当前进程 JVM 堆新生代、老年代、持久代等请情况,GC 使用的算法等信息

jmap 可以查看 JVM 进程的内存分配与使用情况,使用 的 GC 算法等信息。

jmap -histo:live {pid} | head -n 10:

jmap -histo:live {pid} | head -n 10  输出当前进程内存中所有对象包含的大小

输出当前进程内存中所有对象实例数 (instances) 和大小 (bytes), 如果某个业务对象实例数和大小存在异常情况,可能存在内存泄露或者业务设计方面存在不合理之处。

jmap -dump:

jmap -dump:format=b,file=/usr/local/logs/gc/dump.hprof {pid}

-dump:formate=b,file= 以二进制输出当前内存的堆情况至相应的文件,然后可以结合 MAT 等内存分析工具深入分析当前内存情况。

一般我们要求给 JVM 添加参数 -XX:+Heap Dump On Out Of Memory Error OOM 确保应用发生 OOM 时 JVM 能够保存并 dump 出当前的内存镜像。

当然,如果你决定手动 dump 内存时,dump 操作占据一定 CPU 时间片、内存资源、磁盘资源等,因此会带来一定的负面影响。

此外,dump 的文件可能比较大 , 一般我们可以考虑使用 zip 命令对文件进行压缩处理,这样在下载文件时能减少带宽的开销。

下载 dump 文件完成之后,由于 dump 文件较大可将 dump 文件备份至制定位置或者直接删除,以释放磁盘在这块的空间占用。

3.3 jstack 命令

printf '%xn' tid   -->  10 进制至 16 进制线程 ID(navtive 线程) %d 10 进制
jstack pid | grep tid -C 30 --color
ps -mp 8278 -o THREAD,tid,time | head -n 40

某 Java 进程 CPU 占用率高,我们想要定位到其中 CPU 占用率最高的线程。

(1) 利用 top 命令可以查出占 CPU 最高的线程 pid

top -Hp {pid}

(2) 占用率最高的线程 ID 为 6900,将其转换为 16 进制形式 (因为 java native 线程以 16 进制形式输出)

printf '%xn' 6900

(3) 利用 jstack 打印出 java 线程调用栈信息

jstack 6418 | grep '0x1af4' -A 50 --color

3.4 jinfo 命令

查看某个 JVM 参数值
jinfo -flag ReservedCodeCacheSize 28461
jinfo -flag MaxPermSize 28461

3.5 jstat 命令

jstat -gc pid
jstat -gcutil `pgrep -u admin java`

II、JVM 内存分配过程

4. 内存分析工具 MAT

4.1 什么是 MAT?

MAT(Memory Analyzer Tool),一个基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 JAVA heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。

使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。

右侧的饼图显示当前快照中最大的对象。单击工具栏上的柱状图,可以查看当前堆的类信息,包括类的对象数量、浅堆 (Shallow heap)、深堆 (Retained Heap).

浅堆表示一个对象结构所占用内存的大小。深堆表示一个对象被回收后,可以真实释放的内存大小。

1)支配树 (The Dominator Tree)

列出了堆中最大的对象,第二层级的节点表示当被第一层级的节点所引用到的对象,当第一层级对象被回收时,这些对象也将被回收。

这个工具可以帮助我们定位对象间的引用情况,垃圾回收时候的引用依赖关系

2)Path to GC Roots

被 JVM 持有的对象,如当前运行的线程对象,被 systemclass loader 加载的对象被称为 GC Roots, 从一个对象到 GC Roots 的引用链被称为 Path to GC Roots。

通过分析 Path to GC Roots 可以找出 JAVA 的内存泄露问题,当程序不在访问该对象时仍存在到该对象的引用路径。

a、创建的对象都在堆的新生代(Eden)上分配空间;垃圾收集器回收时,把Eden上存活的对象和一个Survivor 的对象拷贝到另一个Survivor

四、日志分析

一般是MinorGC

1. GC 日志分析

1.1 GC 日志详细分析

Java 虚拟机 GC 日志是用于定位问题重要的日志信息,频繁的 GC 将导致应用吞吐量下降、响应时间增加,甚至导致服务不可用。

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/usr/local/gc/gc.log -XX:+UseConcMarkSweepGC

我们可以在 java 应用的启动参数中增加 -XX:+PrintGCDetails 可以输出 GC 的详细日志,例外还可以增加其他的辅助参数,如-Xloggc 制定 GC 日志文件地址。如果你的应用还没有开启该参数 , 下次重启时请加入该参数。

上图为线上某应用在平稳运行状态下的 GC 日志截图。

2017-12-29T18:25:22.753+0800: 73143.256: [GC2017-12-29T18:25:22.753+0800: 73143.257: [ParNew: 559782K->1000K(629120K), 0.0135760 secs] 825452K->266673K(2027264K), 0.0140300 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
 [2017-12-29T18:25:22.753+0800: 73143.256] : 自JVM启动73143.256秒时发生本次GC.
[ParNew: 559782K->1000K(629120K), 0.0135760 secs] : 对新生代进行的GC,使用ParNew收集器,559782K是新生代回收前的大小,1000K是新生代回收后大小,629120K是当前新生代分配的内存总大小, 0.0135760 secs表示本次新生代回收耗时 0.0135760秒
[825452K->266673K(2027264K), 0.0140300 secs]:825452K是回收堆内存大小,266673K是回收堆之后内存大小,2027264K是当前堆内存总大小,0.0140300 secs表示本次回收共耗时0.0140300秒
[Times: user=0.02 sys=0.00, real=0.02 secs] : 用户态耗时0.02秒,系统态耗时0.00,实际耗时0.02秒

无论是 minor GC 或者是 Full GC, 我们主要关注 GC 回收实时耗时 , 如 real=0.02secs, 即 stop the world 时间,如果该时间过长,则严重影响应用性能。

1.2 CMS GC 日志分析

Concurrent Mark Sweep(CMS) 是老年代垃圾收集器 , 从名字 (Mark Sweep) 可以看出,CMS 收集器就是 “标记-清除” 算法实现的,分为六个步骤:

  • 初始标记 (STW initial mark);

  • 并发标记 (Concurrent marking);

  • 并发预清理 (Concurrent precleaning);

  • 重新标记 (STW remark);

  • 并发清理 (Concurrent sweeping);

  • 并发重置 (Concurrent reset)。

其中初始标记 (STW initial mark) 和 重新标记 (STW remark) 需要”Stop the World”。

初始标记 :在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法 STW(Stop The Word)。这个过程从垃圾回收的 “ 根对象 “ 开始,只扫描到能够和 “ 根对象 “ 直接关联的对象,并作标记。

所以这个过程虽然暂停了整个 JVM,但是很快就完成了。

并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。

并发预清理 :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象 (可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。

通过重新扫描,减少下一个阶段 “ 重新标记 “ 的工作,因为下一个阶段会 Stop The World。

重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在 CMS 堆中剩余的对象。扫描从 “ 跟对象 “ 开始向下追溯,并处理对象关联。

并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

并发重置 :这个阶段,重置 CMS 收集器的数据结构,等待下一次垃圾回收。

cms 使得在整个收集的过程中只是很短的暂停应用的执行 , 可通过在 JVM 参数中设置 -XX:UseConcMarkSweepGC 来使用此收集器 , 不过此收集器仅用于 old 和 Perm(永生) 的对象收集。

CMS 减少了 stop the world 的次数,不可避免地让整体 GC 的时间拉长了。

Full GC 的次数说的是 stop the world 的次数,所以一次 CMS 至少会让 Full GC 的次数 +2,因为 CMS Initial mark 和 remark 都会 stop the world,记做 2 次。而 CMS 可能失败再引发一次 Full GC。

上图为线上某应用在进行 CMS GC 状态下的 GC 日志截图。

如果你已掌握 CMS 的垃圾收集过程,那么上面的 GC 日志你应该很容易就能看的懂,这里我就不详细展开解释说明了。

此外 CMS 进行垃圾回收时也有可能会发生失败的情况。

异常情况有:

1)伴随 prommotion failed, 然后 Full GC:

[prommotion failed:存活区内存不足,对象进入老年代,而此时老年代也仍然没有内存容纳对象,将导致一次 Full GC]

2)伴随 concurrent mode failed,然后 Full GC:

[concurrent mode failed:CMS 回收速度慢,CMS 完成前,老年代已被占满,将导致一次 Full GC]

3)频繁 CMS GC:

[内存吃紧,老年代长时间处于较满的状态]

b、大对象直接进入老年代

2. 业务日志

业务日志除了关注系统异常与业务异常之外,还要关注服务执行耗时情况,耗时过长的服务调用如果没有熔断等机制,很容易导致应用性能下降或服务不可用,服务不可用很容易导致雪崩。

上面是某一接口的调用情况,虽然大部分调用没有发生异常,但是执行耗时相对比较长。

grep ‘[0-9]{3,}ms’ *.log

找出调用耗时大于 3 位数的 dao 方法,把 3 改成 4 就是大于 4 位数

互联网应用目前几乎采用分布式架构,但不限于服务框架、消息中间件、分布式缓存、分布式存储等等。

那么这些应用日志如何聚合起来进行分析呢 ?

首先,你需要一套分布式链路调用跟踪系统,通过在系统线程上线文间透传 traceId 和 rpcId,将所有日志进行聚合,例如淘宝的鹰眼,spring cloud zipkin 等等。

-- 所以对于大对象比较多的,年老代分配的内存要多一点,年轻代分配的少一点

五、案列分析

--可以配置对象大小的门限

CPU 使用率高问题定位

按照定位流程首先排除了系统层面的问题。

利用 top -Hp 6814 输出进程 ID 为 6814 的所有线程 CPU 使用率情况,发现某个线程使用率比较高,有些异常。

printf '%xn' 2304     #输出线程 ID 的 16 进制
jstack pid | grep '0x900' -C 30 --color

输出的日志表明该线程一直处于与 mysql I/O 状态:

利用 jmap -dump:format=b,file=/usr/local/logs/gc/dump.hprof {pid} 以二进制输出档当前内存的堆情况,然后可以导入 MAT 等工具进行分析。

如下图所示,点击 MAT 的支配树可以发现存在某个超大对象数组,实例对象数目多大 30 多万个。

经过分析发现数组中每一个对象都是核心业务对象,我们的业务系统有一个定时任务线程会访问数据库某张业务表的所有记录。

然后加载至内存然后进行处理因此内存吃紧,导致 CPU 突然飙升。发现该问题后,已对该方案进行重新设计。

近期热文

《谈谈源码泄露 · WEB 安全》

《用 LINQ 编写 C# 都有哪些一招必杀的技巧?》

《机器学习面试干货精讲》

《深入浅出 JS 异步处理技术方案》

《敏捷教练 V 形六步法实战:从布朗运动到深度协作》

《从零开始,搭建 AI 音箱 Alexa 语音服务》

《修改订单金额!?0.01 元购买 iPhoneX?| Web谈逻辑漏洞》


「阅读原文」看交流实录,你想知道的都在这里

c、长期存活的对象进入老年代

--在多次MinorGC时仍然存活的,进入老年代

--可以配置门限MinorGC次数,默认15次

原因很简单,因为 年轻代一般是复制算法,多次复制代价很大

老年代是 Full GC

d、年龄判断,如果 Survivor 的同龄对象占所有对象的一半,大于这个年龄的就直接进入老年代

MinorGC时,检查晋升老年代的对象是否大于 老年代剩余空间,如果大于则进行 Full GC

e、空间分配担保

在发生Minor GC 时,虚拟机检测之前 晋升到老年代的空间平均大小 是否大于老年代剩余空间,如果大于则直接进行 Full GC;如果小于,则查看HandlePromotionFailure 设置是否允许担保失败。如果允许,就进行Minor GC,并把存活的对象移到老年代,如果不允许,则进行Full GC

III、内存溢出

a、OutOfMemoryError

首先,堆内存不够分配、肯定会出现 内存溢出的问题

永久代,加载的类太多,也会有

栈内存申请不到也有

本机native直接内存溢出

内存溢出会出现在各个内存区域

b、StackOverflowError

递归调用(没有关闭条件)

线程太多

c、内存溢出定位过程

使用内存映像分析工具(Eclipse Memory Analyzer),对dump出的文件进行分析

确认内存中的对象是否必要的。 即分清楚出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

如果是内存泄漏通过工具查看 泄漏对象到 GC root的引用链

如果不是泄漏,则 检查 虚拟机的堆参数(-Xmx -Xms),从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况。

2、内存(垃圾)回收

在描述 java 垃圾回收之前,想象一下 C ++ 内存如何内存管理 和 垃圾回收。

通常new 一片内存区域,存储一些数据,假设就是 new int[]

频繁的操作删除后,留下了很多内存碎片

然后一般都是 memcpy 把数据转移到内存的一端,一般是都移动到开始端。

事实上,所有的内存回收后的管理,基本都是 拷贝移动已有数据。比如 Redis的 ziplist 就是这么设计的。

垃圾回收两个问题:

I、如何判断 对象不再被使用?

a、首先想到的是记录每一个使用者 - 引用计数器,事实上早期的java垃圾回收就是如此。

引用计数有个很大的困扰,几个对象间的互相循环引用,怎么办?引用计数一直存在。

b、标记引用链 + 从根开始 - 根搜索法

通过引用链可以识别对象引用关系;从根开始,就能识别脱离主链的 循环引用的问题。这样利用有向图,从根开始寻找整个引用链,把不再链上的对象都进行标记。

什么样的对象适合做根对象 GCroot

静态变量 - 程序加载首先进内存的对象,全局根

栈帧的变量 - 程序当前执行到的对象,临时根 (因为执行完毕,栈帧的数据就会回收,执行过程中,作为当前流程开始的对象同样也是根)

II、如何操作回收不用的对象?

前面已经描述过C++ 内存回收方法,java也非常类似

标记清除法 - 前面发现的对象,标记完后,进行删除, 类似 delete,这样会产生很多碎片

复制算法 - 把存活的对象,统一拷贝到 另一块完整内存

标记整理法 - 把存活对象移动到一端,剩下的内存统一清理,类似 memcpy,后delete

适用场景

复制算法,适用存活对象较少的场景,比如 新生代;标记整理算法和清除算法,适用于存活对象较多的场景。

III、垃圾回收器

除了标记清除法外,其他两种需要移动对象,都会造成程序的卡顿(移动过程中,对象不能被改变),这个问题数据库备份过程中也有同样的问题。

a、复制算法收集器 -- 基本都用在新生代

Serial收集器 - 单线程条件下运行 (一般client和默认的)

ParNew收集器 - 多线程条件下运行 (一般server模式适用)

Parallel Scanvenge收集器

ParNew VS Parallel Scanvenge

ParNew 关注卡顿时延; Parallel Scanvenge 关注系统吞吐量

b、标记整理算法收集器 -- 基本用在老年代

Serial Old收集器 - 单线程

Parallel Old收集器 - 多线程 关注吞吐量

c、标记清除算法收集器 -- 用在老年代

CMS(Concurrent Mark Sweep)收集器 关注时延(因为耗时最多的标记和清除不需要影响用户业务)

G1收集器(Garbage First)收集器

时延 or 吞吐量

可以这么理解,时延的目标是单次回收要尽快,减少单次时延,而整体卡顿累计时长可能更多,导致吞吐量下降;吞吐量则关注整体卡顿情况,累计时长要端,吞吐量要高,单次卡顿时延可能会较长。

在这两种策略下,关注时延的 可能是多次频繁小范围的GC、关注吞吐量的可能是 一次就彻底的大范围的GC

组合:

单线程版本 - Serial + Serial Old 用在 Client模式下 (一般很少使用)

吞吐量优先组合 - Parallel Scanvenge + Parallel Old(Serial Old 老版本) 用在 Server模式下

时延优先组合 - ParNew + CMS(Serial Old 备用)用在 Server 模式下

3、JVM 优化

I、JVM crash

JVM 宕机的问题分析,首先 JVM 是一个C++进程,同样可以采用 C++ coredump 的分析思路来分析 JVM (网上描述的,好像用jmap生成的dump不能用GDB调试)

a、定位的文件素材

crash 日志

生成 -XX:ErrorFile=/path/xxx.log;

执行命令-XX:OnError="string"

-XX:+ShowMessageBoxOnError -- 打开实时GDB调试

程序自带日志

coredump文件

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/xx.log

linx: kill -3 | windows : Ctrl + Break

用JDK 自带命令jmap ,或者工具 JConsole和VisualVM

如果不能生成 则检查linux的 ulimit 配置

如果都找不到 到 /var/log/message中 找 cat messages|grep java java线程相关信息

b、分析文件

crash 日志

日志头(概要信息): -- 得到在哪个大的部分出现的问题。粗略信息

SIGSEGV - 执行 JNI 时出现的问题,一般是 编译加载类、执行JVM 外部代码出现的问题

EXCEPTION_ACCESS_VIOLATION - 执行 JVM 自身的代码

EXCEPTION_STACK_OVERFLOW - 堆栈出错

执行代码类型 C J VM 等

线程信息: -- 得到crash时线程的工作情况

线程类型 - Java Thread | VMThread | CompilerThread | GCTaskThread | WatcherThread | ConcurrentMarkSweepThread

线程状态 - _thread_in_native | _thread_uninitialized | _thread_new | _thread_in_vm | _thread_in_Java | _thread_blocked

安全点 safepoint 和锁 Mutex

安全点是标记线程运行到一个区域,JVM将其挂起,以便执行GC 等JVM操作。没有运行到安全点的线程,GC是不能回收其内存的;如果线程一直不到安全点可能会出现假死状态

澳门新濠3559,内存heap情况

各个内存区域使用情况

其他信息

JVM参数,系统环境

分析重点:概要信息里,判断Crash时正在执行什么信息;当前线程状态;还有内存使用情况。

经典问题:内存溢出,一般永久代因为分配较少,出现问题的情况比较多;堆栈溢出,主要是 jni 本地栈溢出的可能较多

threaddump / heapdump 文件

当前线程运行状态、线程堆栈信息

类、对象使用情况

分析重点:基本信息里面,生成堆栈时的 异常线程和异常原因; wait/lock 等信息

经典问题:内存溢出

分析顺序 crash日志 > thread dump > heap dump

II、JVM OOM 问题

分析的文件和 crash 一样的。分析过程也是类似;

另外,可以通过JDK工具和命令实时监控分析。

不过,OOM 不一定会出现crash的情况。一般都是分析 heap dump文件。

a、分析内存 堆、非堆的使用情况; 看看是否是内存分配参数设置不合理。

b、分析 出现OOM的线程,正在操作的情况。找到导致泄露的对象的 GC Root 链

c、分析 类实例数最多、最大的 类的使用情况。(大对象、多对象)

-- 找到上面这两种情况下的 类和对象 是否需要/需要这么多,分清是 泄露还是对象生命周期不合理

d、分析 GC 的情况

-- 看看Full GC的情况

III、性能优化

程序的性能优化,无非就是 CPU、内存、IO 三种资源的占用情况分析。

性能优化的关键在于,分段排查,逐步逼近的方式,确定问题代码所在

先测量

再逐步逼近

找到问题代码

分析原因,并给解决方法

第一步:Linux 命令查看

top -H -p 找到 java进程和线程 中最耗资源的线程

第二步:在各种日志中找到对应的线程进行分析

a、卡顿时间较长 or 处理很慢 - 一般是CPU在高负荷运转,说明线程在高负荷执行;GC 时间较长等

查看GC 时长,GC 频次,各种GC占比 使用的是什么垃圾处理器

(比如 GCViewer 工具),GC的具体情况 -XX:+PrintGCTimeStamps -Xloggc:/tmp/gc.log -XX:+PrintGCDetails

查看各个线程处理情况,lock wait/notify 等情况;长时间运行的线程

查看JNI线程使用情况

连续生成两次的 线程堆栈(core) 文件,对比,查看 对象变化,线程执行变化;如果执行方法没有变化的线程,一般就是有问题的线程

IV、JDK自带工具

a、命令行工具

内存信息 jmap工具

生成dump jmap dump:format=b,file=xxx pid

内存统计 jmap -heap

内存跟踪 jstat

线程堆栈跟踪 jstack

配置信息 jinfo

分析工具 jhat

利用命令行就是 jmap+jstack,然后详细信息通过jhat

b、可视化工具

JConsole

JVisualVM - 其中 BTTrace 可以嵌入到每个方法追踪每个方法的执行(通过类似 asm/CGLib 字节码加载替换)

对比两个 dump 的差异,找出对象的

V、性能分析工具

MAT

IBM Heap - 可以分析出OOM中的 内存占用最大的地方,可以溯源GC root,找到对象树

VI、配置建议

a、内存分配,各个代的分配,32位JVM下 堆分配 1G,年轻代 一半,年老代一半。持久代64M

-Xmx512m

-Xms512m

-- 最大最小保持一致,避免频繁扩容和收缩

-Xmn256m

-- 年轻代一般在 一半左右,官方推荐 3/8

-XX:PermSize=64m

-- 持久代一般固定在 64M左右

-XX:MaxPermSize=128m

b、垃圾收集器选择

-XX:+UseParNewGC

--设置年轻代使用 ParNew收集器 并行

-XX:+UseConcMarkSweepGC

--设置年老代为CMS收集器 并发

c、其他设置项:内存压缩,对象晋级等等。

-XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction=5

-- Full GC 5次后,进行内存碎片压缩

-XX:+HeapDumpOnOutOfMemoryError

-- 内存溢出时生成dump文件

d、启用运行期编译

Jit即时编译器

C1 – Client (简单优化

C2 – Server(激进优化 运行在Server模式下会更高效)

根据监控,针对热点代码进行优化(比如方法内联)

Jit的缺点是 编译有耗时,另外,对于一些类装载卸载比较多的场景也不适合。

Server模式启动时要慢一点,运行时效率很高

也可以指定,运行模式 解释模式-Xint,编译模式-XComp

比较理想的情况是,编译成Class,运行时可以动态Server模式;兼顾了效率和可移植性。

编辑:服务器运维 本文来源:在业务应用层面,作为一名 Java

关键词: