Dragon

业精于勤荒于嬉

0%

JVM详解及JVM调优

JVM类加载器

类的生命周期

加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 销毁

  • 验证阶段

    这一步就是根据 java 虚拟机规范,来校验加载进来的 .class 文件中的内容,是否符合指定的规范

  • 准备阶段

    1
    2
    3
    public class User {
    public static int age = 10;
    }

    准备工作,其实就是给这个 User 类分配一定的内存空间,然后给他里面的类变量(static 修饰的变量)分配内存空间,来一个默认的值。

    比如上面的代码里,就会给 age 这个类变量分配内存空间,并且给一个默认值 0。

  • 解析阶段

    这个阶段实际上是把 符号引用替换为直接引用

  • 初始化阶段

    这个阶段就会执行初始化代码

    1
    2
    3
    4
    5
    6
    7
    8
    public class User {
    public static int age = 10;
    public static Map<String, String> map;

    static {
    map = new HashMap<>();
    }
    }

    初始化的时候就会将之前给默认值的类变量的值改为真正我们设置的值,比如上面的 age ,准备阶段给了默认值 0,现在替换为我们设置的值 10。

    另外,static 静态代码块,也会在这个阶段执行。

    注意!!!

    如果在初始化一个类的时候,发现他的父类还没有初始化,那么必须先初始化他的父类。

    并且 JVM 加载类是懒加载的,只有在使用到的时候才会加载(也就是说你在 new User() 的时候,这个 User 类才会被加载)

类加载器

  • 启动类加载器 Bootstrap ClassLoader

    主要用来加载我们机器上安装的 Java 目录下的核心类,也就是 Java 安装目录下的 jre\lib 目录。JVM 一启动,就会首先加载该目录下的核心类。

  • 扩展类加载器 Extension ClassLoader

    主要用来加载我们机器上安装的 Java 目录下的核心类,也就是 Java 安装目录下的 jre\lib\ext 目录。JVM 一启动,就会首先加载该目录下的核心类。

  • 应用程序类加载器 Application ClassLoader

    主要用来加载 ClassPath 环境变量所指定的路径中的类,其实可以理解为就是加载我们写好的 Java 代码。

  • 自定义类加载器

    除了上面三种之外,还可以自定义类加载器,去根据自己的需求来加载类。

双亲委派机制

类加载器是有亲子层结构的(不是 Java 中的父类子类),就是说启动类加载器在最上层,扩展类加载器在第二层,应用程序类加载器在第三层,自定义类加载器在最后一层。如下图:

双亲委派的意思是:

假如你的应用程序类加载器要加载一个类,

它首先会向上委派给扩展类加载器去加载,

扩展类加载器也不会先加载,首先会向上委派给启动类加载器去加载。

如果启动类加载器没有找到这个类,则再往下委派给扩展类加载器去加载,如果扩展类加载器也没有找到这个类,则再继续往下委派给应用程序类加载器去加载。

也就是会从上往下加载,如果中途有一个类加载器加载到了该类,就不再往下走。

这就是双亲委派机制:先找父类加载,找不到的话再由儿子来加载。这么做的好处就是,可以避免多层级的类加载器重复加载某些类。

JVM参数

Xmn Xms Xmx Xss有什么区别

Xmn、Xms、Xmx、Xss都是JVM对内存的配置参数,我们可以根据不同需要区修改这些参数,以达到运行程序的最好效果。

  • -Xms 堆内存的初始大小,默认为物理内存的1/64。此值必须是大于1 MB的1024的倍数。 jdk8以后也可以使用 -XX:InitialHeapSize=1024M 来设置
  • -Xmx 堆内存的最大大小,默认为物理内存的1/4。此值必须是大于1 MB的1024的倍数。 jdk8以后也可以使用 -XX:MaxHeapSize=1024M 来设置
  • -Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn jdk8以后也可以使用 -XX:NewSize=512M -XX:MaxNewSize=512M 来设置
  • -Xss 设置每个线程可使用的内存大小,即栈的大小。在相同物理内存下,减小这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。

通用参数

  • -XX:NewRatio=2 设置新生代和老年代的比值。如:为3,表示年轻代与老年代比值为1:3
  • -XX:SurvivorRatio=8 新生代中Eden区与两个Survivor区的比值,默认为8。注意Survivor区有两个。如:为3,表示Eden:Survivor=3:2,一个Survivor区占整个新生代的1/5
  • -XX:MaxTenuringThreshold=15 对象晋升老年代的年龄,默认为15。如果是0,则直接跳过新生代进入老年代
  • -XX:PermSize=256M、**-XX:MaxPermSize=256M** 分别设置永久代最小大小与最大大小(Java8以前)
  • -XX:MetaspaceSize=256M、**-XX:MaxMetaspaceSize=256M** 分别设置元空间最小大小与最大大小(Java8以后)
  • -XX:PretenureSizeThreshold=1M 大对象大小,超过这个值的大对象直接放入老年代
  • -XX:+TraceClassLoading 开启打印类加载器加载了哪些类
  • -XX:+TraceClassUnloading 开启打印类加载器卸载了哪些类
  • -XX:+DisableExplicitGC 禁止显示执行 GC ,说白了就是不允许在代码中通过 System.gc() 来执行 GC。

    NIO 在创建堆外内存的时候,如果发现内存不足,会通过 System.gc() 来触发 JVM 的 Full GC,回收掉堆中已经没有被引用的 DirectByteBuffer 对象。如果开启了该参数,可能会导致 NIO 的 System.gc() 也不生效。最终导致堆外 OOM (当然避免这种情况的最好办法,还是合理的分配 JVM 内存,让变为垃圾的 DirectByteBuffer 对象尽量在新生代就被回收掉)

  • -XX:+PrintGC-verbose:gc 开启gc日志打印
  • -XX:+PrintGCDetails 打印 GC 日志
  • -XX:+PrintGCTimeStamps 打印 GC 执行时间日志
  • -Xloggc:/tmp/gc.log 将 GC 日志输出到指定文件
  • -XX:SoftRefLRUPolicyMSPerMB=1000 软引用对象的存活时长时间(单位ms)

    有一个公式

    clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB

    clock - timestamp 代表一个软引用对象多久没有被访问过了

    freespace 代表 JVM 中的空闲内存空间

    SoftRefLRUPolicyMSPerMB 代表每一MB空闲内存空间可以允许软引用对象存活多久

    当然在一般 GC 的时候, JVM 或多或少都会有一些内存空间的,所以基本上如果不是快要发生 OOM,一般软引用对象也不会被回收

    注意!!!

    因为反射的成本是很高的,JVM 在处理反射的时候做了一些优化,如果一个反射被调用了多次以后,JVM 就会自动创建一个软引用的对象,大概比如sun.reflect.GeneratedConstructorAccessor 或者 sun.reflect.GeneratedMethodAccessor 之类的。这样在后面继续调用反射的时候,就可以直接调用这些对象的方法来代替反射,从而提高效率。

    所以 -XX:SoftRefLRUPolicyMSPerMB 这个参数的值一定不能设置为 0 ,而且如果系统中有大量反射的时候,还需要把该参数设置大一些,比如 1000, 2000, 3000, 或者 5000 毫秒,都可以。

设置垃圾收集器

  • -XX:+UseParNewGC 设置新生代多线程收集器 一般搭配 CMS 使用 注意:ParNew 在单核环境下是不如 Serial 的,在多核的条件下才有优势
  • -XX:+UseConcMarkSweepGC 设置CMS收集器 (老年代)
  • -XX:+UseG1GC 设置G1收集器 jdk9的默认收集器 (新生代和老年代)
  • -XX:+UseParallelGC 设置多线程收集器 jdk8的默认新生代收集器 (新生代) 与 ParNew 的区别是,Parallel 注重的是吞吐量
  • -XX:+UseParallelOldGC 设置并行老年代收集器 在注重吞吐量的场景下,可以采用 Parallel Scavenge + Parallel Old 的组合
  • -XX:+UseSerialOldGC 设置串行收集器 CMS和G1回收失败以后会转为该收集器,性能超级低

CMS 参数

  • -XX:CMSInitiatingOccupancyFaction=92 设置老年代占用多少比例的时候触发CMS垃圾回收,jdk1.6 默认是 92%
  • -XX:+UseCMSInitiatingOccupancyOnly 一般配合上面的参数一起使用。如果只设置上面的参数而没有设置该参数,JVM仅在第一次使用设定值,后续则自动调整。然而,请记住大多数情况下,JVM比我们自己能作出更好的垃圾收集决策。因此,只有当我们充足的理由(比如测试)并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。
  • -XX:+UseCMSCompactAtFullCollection 开启内存碎片整理
  • -XX:CMSFullGCsBeforeCompaction=0 执行多少次Full gc 之后再执行一次内存碎片整理的工作,默认是0,意思是每次Full gc之后都整理
  • -XX:+CMSParallelInitialMarkEnabled 这个参数会在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行
  • -XX:+CMSScavengeBeforeRemark 这个参数会在CMS的重新标记阶段之前,先尽量执行一次Young GC。如果在重新标记之前,先执行一次Young GC,就会回收掉一些年轻代里没有人引用的对象。所以如果先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少他的耗时

G1参数

  • -XX:G1HeapRegionSize 手动指定 region 的大小,最小 1M,最大 32M。一般采用默认就行,当然如果机器内存很大,比如 32G,这时候需要适当调大该参数,防止 Region 过多,比如调整为 16M
  • -XX:G1NewSizePercent=5 设置新生代初始占比,默认是5%
  • -XX:G1MaxNewSizePercent=60 设置新生代最大占比,默认是60%
  • -XX:G1MixedGCCountTarget=8 设置一次混合回收分为几个阶段来执行,默认是8次
  • -XX:G1HeapWastePercent=5 设置混合回收空出来的Region数量达到了堆内存的多少,就立即停止混合回收。默认5%
  • -XX:G1MixedGCLiveThresholdPercent=85 确定要回收Region的时候,必须是存活对象低于多少的Region才可以进行回收,默认是85%
  • -XX:InitiatingHeapOccupancyPercent=45 设置老年代占据了堆内存多大的时候,尝试触发 新生代+老年代 的混合回收(Mixed GC),默认是45%

JVM调优工具

jstat

用来查看正在运行的 JVM 的各个指标数据

jstat -gc PID

查看 JVM 的内存情况和GC情况

每一列的解释:(以 KB 为单位)

S0C:这是 From Survivor 区的大小

S1C:这是 To Survivor 区的大小

S0U:这是 From Survivor 区当前使用的大小

S1U:这是 To Survivor 区当前使用的大小

EC:这是 Eden 区的大小

ECU:这是 Eden 区当前使用的大小

OC:这是 老年代 的大小

OU:这是 老年代 当前使用的大小

MC:这是 方法区(永久代、元数据区) 的大小

MU:这是 方法区(永久代、元数据区) 当前使用的大小

CCSC:这是 压缩类空间 的大小

CCSC:这是 压缩类空间 当前使用的大小

YGC:这是系统运行迄今为止的 Young GC 次数

YGCT:这是 Young GC 的耗时

FGC:这是系统运行迄今为止的 Full GC 次数

FGCT:这是 Full GC 的耗时

GCT:这是 所有GC 的总耗时

jstat -gccapacity PID

堆内存分析

jstat -gcnew PID

年轻代 GC 分析,这里的 TT 和 MTT 可以看到对象在年轻代存活的年龄和存活的最大年龄

jstat -gcnewcapacity PID

年轻代内存分析

jstat -gcold PID

老年代 GC 分析

jstat -gcoldcapacity PID

老年代内存分析

jstat -gcmetacapacity PID

元数据区内存分析

jmap

jmap -heap PID

打印堆内存相关的一些参数和各个区域的内存情况

jmap -histo PID

打印各个对象占用内存空间的大小,从大到小排列

jmap -dump:live,format=b,file=dump.hprof PID

生成堆转储快照,也可以在系统启动的时候指定参数 -XX:+HeapDumpOnOutOfMemoryError 和 -XX:HeapDumpPath=/home/heapdump.hprof 来开启在系统发生 OOM 的时候自动生成堆转储文件,方便分析

jhat

jhat dump.hprof -port 7000

启动 jhat 服务器,其中 dump.hprof 就是上一步使用 jmap -dump 来生成的堆转储文件名称

启动以后就可以在浏览器访问这台机器的 7000 端口号,通过图形化的方式去分析堆内存里的对象分布情况了

不过功能比较弱小,一般都是用 MAT 工具

MAT 工具

下载地址

https://www.eclipse.org/mat/downloads.php

使用

下载好以后,在他的安装目录里,可以看到一个文件名字叫做:MemoryAnalyzer.ini

注意:

如果 dump 出来的内存快照很大,比如有几个G,务必要记得在启动 MAT 之前先在这个配置文件里面给 MAT 本身设置一下堆大小,比如设置4个G、8个G。

他这里默认 -Xmx1024m 是1G

然后启动 MAT 即可,启动之后看到界面中有一个 Open a Heap Dump,就是打开一个内存快照,然后上传之前的 dump 文件即可。

打开一个快照后,上面工具栏里有一个按钮,Leak Suspects,是用来进行 内存泄漏 分析的

JVM如何调优

调优无非就是尽可能的减少 Full GC,想要减少 Full GC,就需要尽量减少对象进入老年代,让大部分对象在 Young GC 的时候就清理掉

对象在什么情况下进入老年代

  • 对象的年龄达到了 MaxTenuringThreshold 设置的年龄大小(默认15)以后,就会进入老年代
  • Young GC 后的存活对象 > Survivor 的大小,这批存活对象直接进入老年代
  • 大对象(PretenureSizeThreshold设置的大小)直接进入老年代
  • 动态年龄判断,每次 Young GC 后,Survivor里存活的对象从最低年龄往上加,比如 年龄1 + 年龄2 + 年龄3 + …,加到年龄3以后总大小超过了 Survivor 大小的 50%,那么 >= 年龄3 的对象直接进入老年代

我们在调优的时候最想知道哪些信息?

新生代对象的增长速率,Young GC 的触发频率,Young GC 的耗时,每次 Young GC 后有多少对象是存活下来的,每次 Young GC 后有多少对象进入了老年代

老年代对象的增长速率,Full GC 的触发频率,Full GC 的耗时

新生代对象的增长速率

要分析这个东西,需要使用这个命令

1
jstat -gc PID 1000 10

后边的 1000 10 啥意思呢?

就是说 每1000 毫秒执行输出一次,连着输出 10 次

稍微通用一点的模板

1
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M  -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/local/app/gc/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/app/oom/

JVM问题排查

查找cpu占用高的线程

1
2
3
4
5
6
7
8
# 首先查看进程,找到cpu使用率最高的进程pid
top
# 找到cpu使用率最高的线程pid
top -Hp 进程pid
# 将线程pid转换为16进制,为后面查找jstack日志做准备
printf '%x\n' 线程pid
# 查看线程栈信息,例如 jstack 1040 | vim +/0x431 -
jstack 进程pid | vim +/十六进制线程pid -