线上问题排查思路与工具使用,JVM性能优化

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

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

「文末高能」

编辑 | 哈比

Java
应用性能优化是一个老生常谈的话题,典型的性能问题如页面响应慢、接口超时,服务器负载高、并发数低,数据库频繁死锁等。尤其是在“糙快猛”的互联网开发模式大行其道的今天,随着系统访问量的日益增加和代码的臃肿,各种性能问题开始纷至沓来。Java
应用性能的瓶颈点非常多,比如磁盘、内存、网络 I/O 等系统因素,Java
应用代码,JVM GC,数据库,缓存等。笔者根据个人经验,将 Java 性能优化分为
4 个层级:应用层、数据库层、框架层、JVM 层。

一、前言

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

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

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

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

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

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

银河国际手机版最新 1

二、Java 服务常见线上问题

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

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

Java 性能优化分层模型

1. 系统异常

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

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

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

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

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

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

每层优化难度逐级增加,涉及的知识和解决的问题也会不同。比如应用层需要理解代码逻辑,通过
Java 线程栈定位有问题代码行等;数据库层面需要分析
SQL、定位死锁等;框架层需要懂源代码,理解框架机制;JVM 层需要对 GC
的类型和工作机制有深入了解,对各种 JVM 参数作用了然于胸。

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

2. 业务服务异常

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

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

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

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

三、问题定位

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

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

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

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

调优准备

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

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

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

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

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

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

1. 定位流程

1.1 系统异常排查流程

1.2 业务应用排查流程

性能分析

性能诊断一种是针对已经确定有性能问题的系统和代码进行诊断,还有一种是对预上线系统提前性能测试,确定性能是否符合上线要求。针对前者,性能诊断工具主要分为两层:OS
层面和 Java 应用层面(包括应用代码诊断和 GC
诊断),后者可以用各种性能压测工具(例如 JMeter)进行测试。

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 的版本信息 .

OS 诊断

OS 的诊断主要关注的是 CPU、Memory、I/O 三个方面。

CPU 诊断

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

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

发生了频繁的gc

多线程的上下文切换

对于 CPU 主要关注平均负载(Load Average),CPU
使用率,上下文切换次数(Context Switch)。

通过 top 命令可以查看系统平均负载和 CPU 使用率,图为通过 top
命令查看某系统的状态。

top -H -p [pid]

银河国际手机版最新 2

top 命令示例

平均负载有三个数字:63.66,58.39,57.18,分别表示过去 1 分钟、5 分钟、15
分钟机器的负载。按照经验,若数值小于 0.7*CPU
个数,则系统工作正常;若超过这个值,甚至达到 CPU
核数的四五倍,则系统的负载就明显偏高。图中 15 分钟负载已经高达 57.18,1
分钟负载是 63.66(系统为 16
核),说明系统出现负载问题,且存在进一步升高趋势,需要定位具体原因了。

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

jstack [pid]

银河国际手机版最新 3

jstack命令示例

接下来需要注意的一点是,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]

银河国际手机版最新 4

jstat命令示例

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

vmstat 1 5

银河国际手机版最新 5

vmstat 命令示例

上下文切换次数发生的场景主要有如下几种:1)时间片用完,CPU
正常调度下一个任务;2)被其它优先级更高的任务抢占;3)执行任务碰到 I/O
阻塞,挂起当前任务,切换到下一个任务;4)用户代码主动挂起当前任务让出
CPU;5)多任务抢占资源,由于没有抢到被挂起;6)硬件中断。Java
线程上下文切换主要来自共享资源的竞争。一般单个对象加锁很少成为系统瓶颈,除非锁粒度过大。但在一个访问频度高,对多个对象连续加锁的代码块中就可能出现大量上下文切换,成为系统瓶颈。

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

3. JVM 定位问题工具

在 JDK 安装目录的 bin
目录下默认提供了很多有价值的命令行工具。每个小工具体积基本都比较小,因为这些工具只是
jdk\lib\tools.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 '%x\n' 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 '%x\n' 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`

内存诊断

从操作系统角度,内存关注应用进程是否足够,可以使用 free –m
命令查看内存的使用情况。通过 top 命令可以查看进程使用的虚拟内存 VIRT
和物理内存 RES,根据公式 VIRT = SWAP + RES
可以推算出具体应用使用的交换分区(Swap)情况,使用交换分区过大会影响
Java 应用性能,可以将 swappiness 值调到尽可能小。因为对于 Java
应用来说,占用太多交换分区可能会影响性能,毕竟磁盘性能比内存慢太多。

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

堆外内存

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

堆内内存

此部分内存为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端口供开发者使用浏览器浏览堆内对象的信息)。此外,VisualVM也能够打开hprof文件,使用它的heap
walker查看堆内存信息。

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
的内存泄露问题,当程序不在访问该对象时仍存在到该对象的引用路径。

I/O诊断

I/O 包括磁盘 I/O 和网络 I/O,一般情况下磁盘更容易出现 I/O 瓶颈。通过
iostat 可以查看磁盘的读写情况,通过 CPU 的 I/O wait 可以看出磁盘 I/O
是否正常。如果磁盘 I/O
一直处于很高的状态,说明磁盘太慢或故障,成为了性能瓶颈,需要进行应用优化或者磁盘更换。

文件IO

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

银河国际手机版最新 6

银河国际手机版最新,vmstat命令示例

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

大量的随机读写

设备慢

文件太大

网络IO

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

银河国际手机版最新 7

netstat -anp

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

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

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

银河国际手机版最新 8

cat /proc/interrupts

各个列依次是:

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

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

除了常用的 top、 ps、vmstat、iostat 等命令,还有其他 Linux
工具可以诊断系统问题,如 mpstat、tcpdump、netstat、pidstat、sar
等。Brendan 总结列出了 Linux
不同设备类型的性能诊断工具,如图所示,可供参考。

银河国际手机版最新 9

Linux 性能观测工具

四、日志分析

Java 应用诊断工具

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:

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

应用代码诊断

应用代码性能问题是相对好解决的一类性能问题。通过一些应用层面监控报警,如果确定有问题的功能和代码,直接通过代码就可以定位;或者通过
top+jstack,找出有问题的线程栈,定位到问题线程的代码上,也可以发现问题。对于更复杂,逻辑更多的代码段,通过
Stopwatch 打印性能日志往往也可以定位大多数应用代码性能问题。

常用的 Java 应用诊断包括线程、堆栈、GC 等方面的诊断。

jstack

jstack 命令通常配合 top 使用,通过 top -H -p pid 定位 Java
进程和线程,再利用 jstack -l pid
导出线程栈。由于线程栈是瞬态的,因此需要多次 dump,一般 3 次
dump,一般每次隔 5s 就行。将 top 定位的 Java 线程 pid 转成 16 进制,得到
Java 线程栈中的 nid,可以找到对应的问题线程栈。

银河国际手机版最新 10

通过 top –H -p 查看运行时间较长 Java 线程

如上图所示,其中的线程 24985 运行时间较长,可能存在问题,转成 16
进制后,通过 Java 线程栈找到对应线程 0x6199
的栈如下,从而定位问题点,如下图所示。

银河国际手机版最新 11

jstack 查看线程堆栈

JProfiler

JProfiler 可对
CPU、堆、内存进行分析,功能强大,如下图所示。同时结合压测工具,可以对代码耗时采样统计。

银河国际手机版最新 12

通过 JProfiler 进行内存分析

2. 业务日志

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

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

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

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

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

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

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

GC 诊断

Java GC 解决了程序员管理内存的风险,但 GC
引起的应用暂停成了另一个需要解决的问题。JDK 提供了一系列工具来定位 GC
问题,比较常用的有 jstat、jmap,还有第三方工具 MAT 等。

jstat

jstat 命令可打印 GC 详细信息,Young GC 和 Full GC
次数,堆信息等。其命令格式为

jstat –gcxxx -t pid ,如下图所示。

银河国际手机版最新 13

jstat 命令示例

jmap

jmap 打印 Java 进程堆信息 jmap –heap pid。通过 jmap –dump:file=xxx pid
可 dump 堆到文件,然后通过其它工具进一步分析其堆使用情况

MAT

MAT 是 Java 堆的分析利器,提供了直观的诊断报告,内置的 OQL
允许对堆进行类 SQL 查询,功能强大,outgoing reference 和 incoming
reference 可以对对象引用追根溯源。

银河国际手机版最新 14

MAT 示例

上图是 MAT 使用示例,MAT 有两列显示对象大小,分别是 Shallow size 和
Retained
size,前者表示对象本身占用内存的大小,不包含其引用的对象,后者是对象自己及其直接或间接引用的对象的
Shallow size 之和,即该对象被回收后 GC
释放的内存大小,一般说来关注后者大小即可。对于有些大堆 (几十 G) 的 Java
应用,需要较大内存才能打开
MAT。通常本地开发机内存过小,是无法打开的,建议在线下服务器端安装图形环境和
MAT,远程打开查看。或者执行 mat
命令生成堆索引,拷贝索引到本地,不过这种方式看到的堆信息有限。

为了诊断 GC 问题,建议在 JVM 参数中加上-XX:+PrintGCDateStamps。常用的 GC
参数如下图所示。

银河国际手机版最新 15

常用 GC 参数

对于 Java 应用,通过 top+jstack+jmap+MAT
可以定位大多数应用和内存问题,可谓必备工具。有些时候,Java
应用诊断需要参考 OS 相关信息,可使用一些更全面的诊断工具,比如
Zabbix(整合了 OS 和 JVM
监控)等。在分布式环境中,分布式跟踪系统等基础设施也对应用性能诊断提供了有力支持。

五、案列分析

其他分析工具

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

VisualVM

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

Java Mission Control(jmc)

此工具是jdk7
u40开始自带的,原来是JRockit上的工具,是一款采样型的集诊断、分析和监控与一体的非常强大的工具:https://docs.oracle.com/javacomponents/jmc-5-5/jmc-user-guide/toc.htm。但是此工具是基于JFR(jcmdJFR.start
name=test duration=60s settings=template.jfc
filename=output.jfr)的,而开启JFR需要商业证书:jcmdVM.unlock_commercial_features。

Btrace

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

Jwebap

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

CPU 使用率高问题定位

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

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

printf '%x\n' 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谈逻辑漏洞》


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

性能调优

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

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()的使用,尤其是后者其底层数据结构为StringTable,当字符串大量不重复时,会使得StringTable非常大(一个固定大小的hashmap,可以由参数-XX:StringTableSize=N设置大小),从而影响young
gc的速度。在jackson和fastjson中使用了此方法,某些场景下会引起gc问题:YGC越来越慢,为什么。

尽量不要使用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大的解释:https://www.zhihu.com/question/48059457/answer/113538171

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

JVM内存调优Tips

如何将新对象预留在年轻代

众所周知,由于 Full GC 的成本远远高于 Minor
GC,因此某些情况下需要尽可能将对象分配在年轻代,这在很多情况下是一个明智的选择。虽然在大部分情况下,JVM
会尝试在 Eden
区分配对象,但是由于空间紧张等问题,很可能不得不将部分年轻对象提前向年老代压缩。因此,在
JVM
参数调优时可以为应用程序分配一个合理的年轻代空间,以最大限度避免新对象直接进入年老代的情况发生。

分配足够大的年轻代空间,使用 JVM 参数-XX:+PrintGCDetails -Xmx20M
-Xms20M-Xmn6M

如何让大对象进入年老代

我们在大部分情况下都会选择将对象分配在年轻代。但是,对于占用内存较多的大对象而言,它的选择可能就不是这样的。因为大对象出现在年轻代很可能扰乱年轻代
GC,并破坏年轻代原有的对象结构。因为尝试在年轻代分配大对象,很可能导致空间不足,为了有足够的空间容纳大对象,JVM
不得不将年轻代中的年轻对象挪到年老代。因为大对象占用空间多,所以可能需要移动大量小的年轻对象进入年老代,这对
GC
相当不利。基于以上原因,可以将大对象直接分配到年老代,保持年轻代对象结构的完整性,这样可以提高
GC
的效率。如果一个大对象同时又是一个短命的对象,假设这种情况出现很频繁,那对于
GC
来说会是一场灾难。原本应该用于存放永久对象的年老代,被短命的对象塞满,这也意味着对堆空间进行了洗牌,扰乱了分代内存回收的基本思路。因此,在软件开发过程中,应该尽可能避免使用短命的大对象。

可以使用参数-XX:PetenureSizeThreshold
设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。参数-XX:PetenureSizeThreshold
只对串行收集器和年轻代并行收集器有效,并行回收收集器不识别这个参数。

如何设置对象进入年老代的年龄

堆中的每一个对象都有自己的年龄。一般情况下,年轻对象存放在年轻代,年老对象存放在年老代。为了做到这点,虚拟机为每个对象都维护一个年龄。如果对象在
Eden 区,经过一次 GC 后依然存活,则被移动到 Survivor 区中,对象年龄加
1。以后,如果对象每经过一次 GC 依然存活,则年龄再加
1。当对象年龄达到阈值时,就移入年老代,成为老年对象。这个阈值的最大值可以通过参数-XX:MaxTenuringThreshold
来设置,默认值是 15。虽然-XX:MaxTenuringThreshold 的值可能是 15
或者更大,但这不意味着新对象非要达到这个年龄才能进入年老代。事实上,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的,这个参数指定的是阈值年龄的最大值。即,实际晋升年老代年龄等于动态计算所得的年龄与-XX:MaxTenuringThreshold
中较小的那个。

参数为-XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=2
-XX:MaxTenuringThreshold=1

稳定的 Java 堆 VS 动荡的 Java 堆

一般来说,稳定的堆大小对垃圾回收是有利的。获得一个稳定的堆大小的方法是使-Xms
和-Xmx 的大小一致,即最大堆和最小堆 (初始堆)
一样。如果这样设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少
GC
的次数。因此,很多服务端应用都会将最大堆和最小堆设置为相同的数值。但是,一个不稳定的堆并非毫无用处。稳定的堆大小虽然可以减少
GC 次数,但同时也增加了每次 GC
的时间。让堆大小在一个区间中震荡,在系统不需要使用大内存时,压缩堆空间,使
GC 应对一个较小的堆,可以加快单次 GC 的速度。基于这样的考虑,JVM
还提供了两个参数用于压缩和扩展堆空间。

-XX:MinHeapFreeRatio 参数用来设置堆空间最小空闲比例,默认值是
40。当堆空间的空闲内存小于这个数值时,JVM 便会扩展堆空间。

-XX:MaxHeapFreeRatio 参数用来设置堆空间最大空闲比例,默认值是
70。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆。

当-Xmx 和-Xms 相等时,-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio
两个参数无效。

增大吞吐量提升系统性能

吞吐量优先的方案将会尽可能减少系统执行垃圾回收的总时间,故可以考虑关注系统吞吐量的并行回收收集器。在拥有高性能的计算机上,进行吞吐量优先优化,可以使用参数:

java –Xmx3800m –Xms3800m –Xmn2G –Xss128k –XX:+UseParallelGC

–XX:ParallelGC-Threads=20 –XX:+UseParallelOldGC

–Xmx380m –Xms3800m:设置 Java
堆的最大值和初始值。一般情况下,为了避免堆内存的频繁震荡,导致系统性能下降,我们的做法是设置最大堆等于最小堆。假设这里把最小堆减少为最大堆的一半,即
1900m,那么 JVM 会尽可能在 1900MB 堆空间中运行,如果这样,发生 GC
的可能性就会比较高;

-Xss128k:减少线程栈的大小,这样可以使剩余的系统内存支持更多的线程;

-Xmn2g:设置年轻代区域大小为 2GB;

–XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能地减少
GC 时间。

–XX:ParallelGC-Threads:设置用于垃圾回收的线程数,通常情况下,可以设置和
CPU 数量相等。但在 CPU
数量比较多的情况下,设置相对较小的数值也是合理的;

–XX:+UseParallelOldGC:设置年老代使用并行回收收集器。

尝试使用大的内存分页

CPU 是通过寻址来访问内存的。32 位 CPU 的寻址宽度是 0~0xFFFFFFFF
,计算后得到的大小是 4G,也就是说可支持的物理内存最大是
4G。但在实践过程中,碰到了这样的问题,程序需要使用 4G
内存,而可用物理内存小于
4G,导致程序不得不降低内存占用。为了解决此类问题,现代 CPU 引入了
MMU(Memory Management Unit 内存管理单元)。MMU
的核心思想是利用虚拟地址替代物理地址,即 CPU 寻址时使用虚址,由 MMU
负责将虚址映射为物理地址。MMU
的引入,解决了对物理内存的限制,对程序来说,就像自己在使用 4G
内存一样。内存分页 (Paging) 是在使用 MMU
的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页
(page) 和页帧 (page
frame),并保证页与页帧的大小相同。这种机制,从数据结构上,保证了访问内存的高效,并使
OS
能支持非连续性的内存分配。在程序内存不够用时,还可以将不常用的物理内存页转移到其他存储设备上,比如磁盘,这就是大家耳熟能详的虚拟内存。

在 Solaris 系统中,JVM 可以支持 Large Page Size
的使用。使用大的内存分页可以增强 CPU
的内存寻址能力,从而提升系统的性能。

java –Xmx2506m –Xms2506m –Xmn1536m –Xss128k –XX:++UseParallelGC

–XX:ParallelGCThreads=20 –XX:+UseParallelOldGC
–XX:+LargePageSizeInBytes=256m

–XX:+LargePageSizeInBytes:设置大页的大小。

过大的内存分页会导致 JVM 在计算 Heap 内部分区(perm, new,
old)内存占用比例时,会出现超出正常值的划分,最坏情况下某个区会多占用一个页的大小。

使用非占有的垃圾回收器

为降低应用软件的垃圾回收时的停顿,首先考虑的是使用关注系统停顿的 CMS
回收器,其次,为了减少 Full GC
次数,应尽可能将对象预留在年轻代,因为年轻代 Minor GC
的成本远远小于年老代的 Full GC。

java –Xmx3550m –Xms3550m –Xmn2g –Xss128k –XX:ParallelGCThreads=20

–XX:+UseConcMarkSweepGC –XX:+UseParNewGC –XX:+SurvivorRatio=8
–XX:TargetSurvivorRatio=90

–XX:MaxTenuringThreshold=31

–XX:ParallelGCThreads=20:设置 20 个线程进行垃圾回收;

–XX:+UseParNewGC:年轻代使用并行回收器;

–XX:+UseConcMarkSweepGC:年老代使用 CMS 收集器降低停顿;

–XX:+SurvivorRatio:设置 Eden 区和 Survivor 区的比例为 8:1。稍大的
Survivor 空间可以提高在年轻代回收生命周期较短的对象的可能性,如果
Survivor
不够大,一些短命的对象可能直接进入年老代,这对系统来说是不利的。

–XX:TargetSurvivorRatio=90:设置 Survivor 区的可使用率。这里设置为
90%,则允许 90%的 Survivor 空间被使用。默认值是 50%。故该设置提高了
Survivor
区的使用率。当存放的对象超过这个百分比,则对象会向年老代压缩。因此,这个选项更有助于将对象留在年轻代。

–XX:MaxTenuringThreshold:设置年轻对象晋升到年老代的年龄。默认值是 15
次,即对象经过 15 次 Minor GC 依然存活,则进入年老代。这里设置为
31,目的是让对象尽可能地保存在年轻代区域。

总结与建议

性能调优同样遵循 2-8 原则,80%的性能问题是由
20%的代码产生的,因此优化关键代码事半功倍。同时,对性能的优化要做到按需优化,过度优化可能引入更多问题。对于
Java 性能优化,不仅要理解系统架构、应用代码,同样需要关注 JVM
层甚至操作系统底层。总结起来主要可以从以下几点进行考虑:

1)基础性能的调优

这里的基础性能指的是硬件层级或者操作系统层级的升级优化,比如网络调优,操作系统版本升级,硬件设备优化等。比如
F5 的使用和 SDD 硬盘的引入,包括新版本 Linux 在 NIO
方面的升级,都可以极大的促进应用的性能提升;

2)数据库性能优化

包括常见的事务拆分,索引调优,SQL 优化,NoSQL
引入等,比如在事务拆分时引入异步化处理,最终达到一致性等做法的引入,包括在针对具体场景引入的各类
NoSQL 数据库,都可以大大缓解传统数据库在高并发下的不足;

3)应用架构优化

引入一些新的计算或者存储框架,利用新特性解决原有集群计算性能瓶颈等;或者引入分布式策略,在计算和存储进行水平化,包括提前计算预处理等,利用典型的空间换时间的做法等;都可以在一定程度上降低系统负载;

4)业务层面的优化

技术并不是提升系统性能的唯一手段,在很多出现性能问题的场景中,其实可以看到很大一部分都是因为特殊的业务场景引起的,如果能在业务上进行规避或者调整,其实往往是最有效的。

参考

Java
应用性能调优实践

JVM
优化经验总结

Java调优经验谈

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注