JVM运维相关

1. jvm内存划分

Java中占用CPU的单元全部都是线程,整个jvm是一个进程。如果jvm挂掉的话,基本上都是内存爆了,OOM。StatckOverFlowError并不会导致jvm挂掉。

线程有自己独立的内存:本地方法栈、程序计数器。new出来的对象存储在堆,由所有线程共享,除了堆,还有Perm去或Metadata区,都是分享的内存区。它们的引用关系如图:

JVM内存分为年轻代(含两个Survivor存活区)和老年区,使用过程如下:

  1. 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快

  2. 当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);

  3. 此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;

  4. 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。

  5. 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代

这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中)

如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。

可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

Stop-and-copy的清理方法适用于年轻代到老年代的转换,当老年代满到一定程度触发Full GC时,会使用标记-整理算法进行清理。现在用的多的是多线程的CMS算法,CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间,使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。

2. jvm运维相关小工具(线上慎用)

Java提供很多工具用于查看jvm的实时运行状态。

使用jps命令显示java进程:

jps # 显示机器上java进程,类似于ps aux|grep java
jps -mlvV # 显示详情

查看jvm内存信息

使用jstat显示gc详情:

jstat -gc <pid>
# 显示的是jvm的内存使用情况和gc次数时间等数据。内存大小单位是KB
# S0C S1C S0U S1U : 两个survivor区的当前大小和分配大小
# EC EU : Eden区的当前大小和分配大小
# OC OU : Old区当前大小和分配大小
# MC MU CCSC CCSU : 方法区和压缩类
# YGC YGCT : young区的gc次数和时间 单位秒
# FGC FGCT : fullGC的次数和时间 单位秒
# GCT  : GC总时间 单位秒

使用jstat查看其它信息:

jstat -class <pid> # 查看加载的类
#Loaded  Bytes  Unloaded  Bytes     Time   
#  7545 13542.6       49    70.6      18.35
# Loaded 加载的class数,Bytes占用的空间大小

使用jmap查看当前java映射的本地文件,加载class等信息:

jmap <pid> # 映射的文件
jmap -histo <pid> # jvm对象实例个数和内存占用,可以grep一下项目自己的包名,观察有没有可能内存泄漏
jmap -heap <pid> # jvm内存的使用情况
jmap -dump:format=b,file=temp_heapdump.hprof <pid> # 手工dump出heap文件

显示jvm当前的线程

jstack -l <pid>

拿jvm当前所有线程的状态和堆栈,这个代码用code也能获取到线程的状态包括:1) Runnable 程序可运行或正在运行2) wait on condition 该状态出现在线程等待某个条件的发生。一般是在等网络或磁盘io,或者等待Thread 3) Waiting for monitor entry 和 in Object.wait()

显示jvm的详情信息

jinfo <pid>

开启jmx使用jconsole可视化工具查看jvm运行情况,用于本地开发,打开jmx的jvm选项:

-Dcom.sun.management.jmxremote=true
-Dcom.sun.management.jmxremote.port=1616
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

一些开源的小工具:jvmtop

3. 常用配置

简单的设置java内存和Permsize内存大小:

-Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M #直接跟在java命令后面

设置GC日志

-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xloggc:gc.log #直接跟在java命令后面

gclog默认打在标准输出中,加上-Xloggc:可以指定其打在指定文件中。这篇博客快速解读GC日志

GC日志揭示了一些重要信息:1. GC频率 2. GC时长 3. 每个分代内存使用数量和使用率,可以看出堆总大小

如果java程序启动稳定后,GC出现的频率很频繁(几秒一次)、GC的时间很久(正常情况下minor GCs只要几毫秒,fullGC大概一秒),那内存的使用就有问题。

4. 本地可视化工具

JProfiler本地分析(适合开发环境)

JProfiler是个很全面好用的分析工具,但是它只适合于本地使用,线上环境是不建议用的。
一般我认为,只要Java可以提供的分析工具,都可以在JProfiler中找到。
一般JVM分析问题,会从内存和CPU(线程)分析入手。JProfiler可以做到:

**查看jvm内存所有对象个数和占用内存大小** 优化功能:1.动态每秒刷新一次,支持filter选出想看的类2.记录某一时间段的创建的对象数
**jvm堆内存变化图**:Eden、s0s1、old、perm区 画成图,看得出各个区的内存变化
查看线程、当前状态、当前线程堆栈、线程数 优化功能:现场动态图显示不足:没有统计此刻cpu时间占比和历史cpu总时间和占比
**JEE组件**:jdbc链接socket链接文件操作等 

Java VisualVM(简易,产品感更好)

**jvm内存** dump堆内存,查看内存中所有数据也可以分析heap dump文件

可以从heap dump文件中拿到:

1) 基本概要信息:类数、实例数、字节数、系统属性、当前jstack线程栈,线程状态分析参考这篇博客

2)所有加载的类和实例数,每个实例的具体数据和依赖关系

Eclipse Memory Analyzer (MAT)(有eclipse插件)

使用方式:切换到Memory Analysis视图,然后菜单栏File下可以打开堆栈文件。

**jvm内存** 可以分析一个实例所关联的所有对象的保留内存大小根据我的finance那个项目的测试,数据要比visualvm准确同样可以筛选但是数据下钻的能力不如jvisualvm,但不关键,只是体验不太好;但是MAT有显示没有map的占内存百分比,这点很舒服

**检测可能的内存泄漏** 找到最大的几个对象,按大小排序最大对象的分析path to GC root 路径分析。可以按照线程栈方式,查看栈引用的对象的个数,然后再根据线程堆栈,找到执行对应的方法。

**thread线程列表** 这一点也做得比jvisualvm好,但是居然没有线程的状态每个线程有个threadStatus的字段:这个字段好像没有枚举,和不同的操作系统有关

**对象引用列表,双向**

4. 常用操作

分析哪个java线程最耗CPU

首先,linux下是没有真正的线程,线程是靠进程来实现的,所以,通过:

top -H    # -H 显示出线程进程来

找到java最耗CPU的线程,记下这个线程id,转成16进制。例如,28938的16进制是0x710A.
然后执行:

jstack <pid>  # grep 一下上面那个二进制值,可以找到nid=对应的那个线程,这个就是耗CPU的线程

分析哪个java线程的栈引用对象内存大小最大

通过上面提到的MAT工具,分析dump出来的heap文件,可以从线程的角度查内存泄漏问题。点击MAT工具栏中”黄色齿轮状”的图标,即可以线程为单位看引用的堆大小,从而知道代码执行到哪里出现的问题。

还有另外一种方式,可以看到线程对应的引用堆,每个对象的数量及内存大小的统计。首先,点击MAT工具栏的“Histogram”查看内存堆中,最耗内存的对象,然后在该对象上,右键选择“Merge Shortest Path to GC Roots - with all references”,就可以看到了。

文档更新时间: 2018-11-10 20:14   作者:nick