性能优化-JVM

Posted by Shane on October 9, 2019

运行时数据

  • 程序计数器:指向当前线程正在执行的字节码指令的地址,行号

  • 虚拟机栈:存储当前线程运行方法所需要的数据、指令、返回地址

  • 本地⽅法栈(Native Method Stack):与虚拟机栈所发挥的作⽤是⾮常相似的,它们之间的区别不过

    是虚拟机栈为虚拟机执⾏ Java ⽅法(也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。

  • 堆(Heap):是被所有线程共享的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例都在这⾥分配内存。这⼀点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致⼀些微妙的变化发⽣,所有的对象都分配在堆上也渐渐变得不是那么「绝对」了。

  • 方法区:类信息、常量(JDK1.7)、静态变量、JIT

    • 运⾏时常量池(RuntimeConstantPool):是⽅法区的⼀部分。Class⽂件中除了有类的版本、字段、⽅法、接⼜等描述信息外,还有⼀项信息是常量池(ConstantPoolTable),⽤于存放编译期⽣成的各种字⾯量和符号引⽤,这部分内容将在类加载后进⼊⽅法区的运⾏时常量池中存放。

JVM-1

JVM-2

分代

⼀般是把 Java 堆分为新⽣代和⽼年代,这样就可以根据各个年代的特点采⽤最适当的收集算法。在新⽣代中,每次垃圾收集时都发现有⼤批对象死去,只有少量存活,那就选⽤ 复制 算法,只需要付出少量存活对象的复制成本就可以完成收集。⽽⽼年代中因为对象存活率⾼、没有额外空间对它进⾏分配担保,就必须使⽤ 标记—清理 或者 标记—整理 算法来进⾏回收。

JVM-3

  • 新生代
    • 比例为什么是 8:1:1?98% 的对象在 Minor GC 的时候能被回收,保证内存利用率,减少老年代内存的占用,减少 Major GC
    • 98% 不能回收,分配担保
  • 老年代
  • 永久代(Perm Gen)/Meta Space 1.8
    • Java 6 stores all the constant pool and Class information in the Perm Gen
    • Java 7 only stores the class information in the Perm Gen. The String literal pool is on the heap.
    • Java 8 has no Perm Gen. The literal pools and class information are on the heap.

各个分代大小分配

新生代:老年代是 1:2

空间 倍数
总大小 3-4 倍活跃数据的大小
新生代 1-1.5 倍活跃数据的大小
老年代 2-3 倍活跃数据的大小
永久代 1.2-1.5 倍 Full GC 后的永久代空间占用

新生代晋升老年代

  • 对象优先分配在 Eden

    1
    2
    3
    4
    5
    
    -XX:SurvivorRatio=8 # 默认 8:1:1
    -Xms20M
    -Xmx
    -Xmn
    -XX:+PrintGCDetails
    
  • 对象分配 TLAB(Thread Local Allocation Buffer)
    • 栈上分配
    • 逃逸分析
    • 指针碰撞,CAS 同步
    • 空闲列表
  • 大对象直接进入老年代

    • -XX:PretenureSizeThreshold=3145728=3M
  • 新生代 Survivor 长期存活的对象将进入老年代
    • age
    • -XX:MaxTenuringThreshold=15
  • 动态对象年龄判断

    • 相同年龄所有对象的大小总合 > Survivor 空间的一半,直接晋级,不需要判断 MaxTenuringThreshold
  • 空间分配担保
    • Minor GC 之前检查,老年代最大可能连续空间是否 > 新生代所有对象总空间
      • 大于,Minor GC
      • 不大于,老年代最大可用连续空间是否 > 历次晋升到老年代对象的平均大小
        • 大于,Minor GC
        • 不大于,Full GC
    • -XX:HandlePromotionFailure(JDK 1.6 Update 24 之后不生效)
      • 允许担保失败
        • 大于,Minor GC
        • 不大于,Full GC
      • 不允许担保失败,Full GC

什么样的对象需要被 GC

判断算法

  • 引用计数法:相互引用,循环引用
  • 可达性分析算法
    • GC Roots
      • 虚拟机栈中本地变量表中引用的对象(方法中局部变量)
      • 方法区中
        • 类静态变量引用的对象
        • 常量引用的对象
      • 本地方法栈中 JNI 引用的对象
    • 不可达是不是就一定会被回收?
      • finalize():只运行一次,单独线程执行,异步的,不保证运行准确性

引用

理解 Java 的强引用、软引用、弱引用和虚引用

ThreadLocal 缺陷以及处理

  • 强引用 StrongRefrence
    • Object obj = new Object();
    • 只有这个引用被释放之后,对象才会被释放掉,只要引用存在,垃圾回收器永远不会回收,这是最常见的 new 出来的对象
  • 软引用 SoftRefrence
    • 内存不足时回收,缓存
    • 内存溢出之前通过代码回收的引用。软引用主要是用户实现类似缓存的功能,在内存足够的情况下直接通过软件引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据
  • 弱引用 WeakRefrence
    • 下一次垃圾收集会回收掉,不管内存是否够用,大对象
    • 第二次垃圾回收时回收的引用,短时间内通过弱引用对应的数据,可以取到,当执行第二次垃圾回收时,将返回 null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的 isEnQueued 方法返回对象是否被垃圾回收器标记
  • 虚引用 PhantomRefrence
    • 顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM 停止运行时
软引用 当内存不足时 对象缓存 内存不足时终止
软引用 正常垃圾回收时 对象缓存 垃圾回收后终止
虚引用 正常垃圾回收时 跟踪对象的垃圾回收 垃圾回收后终止

垃圾回收算法

标记-清除算法

Mark-Sweep 算法分为 标记清除 两个阶段:⾸先标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象

  • pros:最基础的收集算法
  • cons:效率不高,空间碎片

Mark-Sweep

复制算法

新生代都用此算法

  • pros:实现简单、高效,不用考虑碎片
  • cons:要分一块出来做复制,空间利用率低

Copying

标记-整理算法

标记-整理(Mark-Compact)算法,标记过程仍然与「标记-清除」算法⼀样,但后续步骤不是直接对可回收对象进⾏清理,⽽是让所有存活的对象都向⼀端移动,然后直接清理掉端边界以外的内存

Mark-Compact

分代收集算法

  • Generational Collection 算法⼀般是把Java堆分为新⽣代和⽼年代,这样就可以根据各个年代的特点采⽤最适当的收集算法。
  • 在新⽣代中,每次垃圾收集时都发现有⼤批对象死去,只有少量存活,那就选⽤复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  • ⽽⽼年代中因为对象存活率⾼、没有额外空间对它进⾏分配担保,就必须使⽤标记—清理或者标记—整理算法来进⾏回收。

垃圾收集器

如果说收集算法是内存回收的⽅法论,那么垃圾收集器就是内存回收的具体实现。

Serial

  • 复制算法
  • 历史悠久,单线程,client

ParNew

  • 复制算法
  • 多线程,多核下对资源的利用率会高
1
2
-XX:+UseConcMarkSweepGC # 默认新生代收集器
-XX:+ParallelGCThreads # GC 线程数

Parallel Scavenge

  • 复制算法
  • 吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)
1
2
3
-XX:+MaxGCPauseMillis # 垃圾回收时间尽量不超过
-XX:GCTimeRatis # 垃圾收集时间占总时间的比率
-XX:UseAdaptiveSizePolicy # 自动设置,动态调整,GC Ergonomics

Serial Old

  • 标记-整理算法
  • client
  • server
    • 与 Parallel Scavenge
    • CMS 的备用预案,Concurrent Mode Failure 时使用

Parallel Old

标记-整理算法

CMS(Concurrent Mark Sweep)

  • 标记-清除算法
  • 减少回收停顿时间
  • 步骤
    • 初始标记:快,标记 GC Roots 能直接关联对象
    • 并发标记:GC Roots Tracing
    • 重新标记
      • 修整并发标记期间,用户程序继续运行产生变动的那一部分对象的标记记录
      • 比初始标记时间长,比并发标记时间短
    • 并发清除:清除而不是整理,意味着有碎片
  • 缺点 cons
    • CPU 敏感:GC 线程占用 CPU 资源导致,吞吐量下降
    • 无法处理浮动垃圾
      • 垃圾收集阶段用户程序还要运行,所以要预留空间,不能被填满再回收
      • -XX:CMSInitiationOccupancyFraction,Concurrent Mode Failure 启动 Serial Old
    • 碎片
      • -XX:UseCMSCompactAtFullColletion,Full GC 时开启内存碎片的合并整理过程
      • -XX:CMSFullGCsBeforeCompaction,执行多少次不压缩 Full GC,执行一次带压缩的,0 表示每次都压缩
1
2
-XX:+UseConcMarkSweep # 显示使用 CMS 回收器
+XX:+ScavengeBeforeRemark # 触发 Minor GC

G1

  • G1 垃圾回收器适用于对内存很大的情况,它将堆内存分割成不同的区域,并且并发的对其进行垃圾回收
  • G1 也可以在回收内存之后对剩余的堆内存空间进行压缩
  • 并发扫描标记垃圾回收器,在 STW(Stop The World)情况下压缩内存
  • G1 垃圾回收会优先选择第一块垃圾最多的区域

-XX:+UseG1GC,使用 G1 垃圾回收器

垃圾回收器对比

Types of Java Garbage Collectors

Types-of-Java-Garbage-Collectors3_th

image-20191024211306302

查看当前垃圾回收器

1
2
-XX:+PrintFlagsFinal
-XX:+PrintCommandLineFlags
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * Copy
 * MarkSweepCompact
 */
@GetMapping("/jvm-info")
public String info() {
    List<GarbageCollectorMXBean> list = ManagementFactory.getGarbageCollectorMXBeans();
    StringBuffer sb = new StringBuffer();
    for (GarbageCollectorMXBean bean : list) {
        sb.append(bean.getName() + "\n");
    }
    return sb.toString();
}

什么时间节点回收

  • GC Roots 枚举效率
    • OopMap Ordinary Object Pointer
  • 安全点
    • 方法调用
    • 循环跳转
    • 异常跳转
    • 抢先式中断:先暂停,没在安全点的继续运行
    • 主动式中断:设置 GC 标志,看到标记就停
  • 安全域
    • 线程挂起怎么办
    • 一段代码中,引用关系不会发生变化
      • 标识线程自己进入 safe region
      • 离开时检查系统是否已经完成 GC 过程
  • -XX:PrintGCApplicationStoppedTime

GC 日志

输出日志

1
2
3
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-Xloggc:/var/log/gc.log

打印 Heap 信息

-XX:+PrintHeapAtGC:GC之前打印Heap的信息

日志文件控制

1
2
-XX:-UseGCLogFileRotation
-XX:GCLogFileSize=8K

如何查看 GC 日志

  • -XX:+UseConcMarkSweepGC
1
java -Xms8m -Xmx64m -verbose:gc -Xloggc:/usr/local/software/gc.log -XX:+PrintHeapAtGC -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCTimeStamps -XX:+PrintCommandLineFlags -XX:+PrintFlagsFinal -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=9004 -Djava.rmi.server.hostname=192.168.73.10 -jar jvm-demo1-0.0.1-SNAPSHOT.jar  > catalina.out  2>&1 &
  • -XX:+UseCMSCompactAtFullCollection
1
java -Xms128m -Xmx128m -verbose:gc -Xloggc:/usr/local/software/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/software/error.hprof -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCTimeStamps -XX:+PrintCommandLineFlags -XX:+PrintFlagsFinal -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC  -XX:+UseCMSCompactAtFullCollection -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=9004 -Djava.rmi.server.hostname=192.168.73.10 -jar jvm-demo1-0.0.1-SNAPSHOT.jar  > catalina.out  2>&1 &
  • -XX:+PrintHeapAtGC -XX:+UseCMSCompactAtFullCollection
1
java -Xms128m -Xmx128m -verbose:gc -Xloggc:/usr/local/software/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintHeapAtGC -XX:HeapDumpPath=/usr/local/software/error.hprof -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCTimeStamps -XX:+PrintCommandLineFlags -XX:+PrintFlagsFinal -XX:+PrintGCDetails -XX:+UseCMSCompactAtFullCollection -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=9004 -Djava.rmi.server.hostname=192.168.73.10 -jar jvm-demo1-0.0.1-SNAPSHOT.jar  > catalina.out  2>&1 &
1
-XX:+CMSScavengeBeforeRemark

Young GC 日志

Allocation Failure:新生代放不下了,触发 YoungGC

1
2
0.239: [GC (Allocation Failure) 0.239: [ParNew: 2176K->256K(2432K), 0.0059485 secs] 2176K->929K(7936K), 0.0060390 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
0.245: Total time for which application threads were stopped: 0.0061670 seconds, Stopping threads took: 0.0000187 seconds

img

图片转自:https://blog.csdn.net/alivetime/article/details/6895537

Full GC 日志

Full GC (Ergonomics):能效GC,old 区长期在某个比例(%)下,释放不掉时会触发

1
2
245.586: [Full GC (Allocation Failure) 245.586: [CMS: 43711K->43711K(43712K), 0.0599580 secs] 63359K->63359K(63360K), [Metaspace: 32567K->32567K(1079296K)], 0.0600689 secs] [Times: user=0.05 sys=0.00, real=0.06 secs] 
245.646: Total time for which application threads were stopped: 0.0602617 seconds, Stopping threads took: 0.0000310 seconds

img

图片转自:https://blog.csdn.net/alivetime/article/details/6895537

堆内存溢出案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static List<int[]> bigObj = new ArrayList();

public static int[] generate1M() {
    return new int[524288];
}

/**
 * 增加内存
*/
@GetMapping("/jvm-error")
public void error() throws InterruptedException {
    for (int i = 0; i < 1000; ++i) {
        if (i == 0) {
            Thread.sleep(500L);
            System.out.println("start = [" + new Date() + "]");
        } else {
            Thread.sleep(4000L);
        }
        bigObj.add(generate1M());
    }
}
  • 查看 catalina.out
1
2
3
4
5
6
7
8
9
10
11
12
tail -1000f catalina.out

java.lang.OutOfMemoryError: Java heap space
Dumping heap to /usr/local/software/error.hprof ...
Heap dump file created [72069993 bytes in 0.620 secs]
2019-07-27 05:56:47.532 ERROR 7159 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space] with root cause

java.lang.OutOfMemoryError: Java heap space
	at com.test.controllers.JvmController.generate1M(JvmController.java:58) ~[classes!/:0.0.1-SNAPSHOT]
	at com.test.controllers.JvmController.error(JvmController.java:74) ~[classes!/:0.0.1-SNAPSHOT]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_192]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_192]
  • top 命令查看
1
2
3
4
5
6
7
8
9
top - 05:56:41 up 1 day,  7:19,  2 users,  load average: 0.38, 0.22, 0.12
Tasks:  96 total,   1 running,  95 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.7 us,  0.7 sy,  0.0 ni, 97.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :   481876 total,    72088 free,   244760 used,   165028 buff/cache
KiB Swap:  1048572 total,  1038068 free,    10504 used.   180048 avail Mem 

   PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND        
  7159 root      20   0 2158888 165532  13956 S  1.7 34.4   0:08.84 java           
     1 root      20   0  127928   4200   2664 S  0.0  0.9   0:05.27 system
  • 分析
  1. 查看 GC 日志 CMS: 43711K->43711K(43712K), 0.0599580 secs] 63359K->63359K(63360K),Full GC 已无法回收

  2. 通过 MAT 查看 error.hprof

    1. 通过 dominator_tree 查看 占用 Retained Heap

      image-20191024231341793

    2. 查看 GC Roots 指向

      image-20191024231641777

      image-20191024231717654

JDK 监控工具

  • 官方文档
  • jmap -heap pid:堆使用情况
  • jstat -gcutil pid 1000:频率,多少毫秒刷新
    • jstat -gccause pid 1000
  • jstack:线程 dump
  • jconsole
  • jps
  • jinfo
  • VisualVM
  • MAT
1
2
3
# 文档 http://help.eclipse.org/oxygen/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/software/error.hprof

JVM 参数

官方文档

什么条件出发 STW 的 Full GC

  • Perm Gen(永久代)空间不足
  • CMS GC 时出现 promotion failedconcurrent mode failure
    • concurrent mode failure 发生的原因一般是 CMS 正在进行,但是由于老年代空间不足(真的空间/连续空间不足),需要尽快回收老年代里面的不再被使用的对象,这时停止所有的线程,同时终止 CMS,直接进行 Serial Old GC
    • promotion failed 产生的原因:Eden 空间不足的情况下,将 Eden 和 From Survivor 中的存活对象存入 To Survivor 区时,To Survivor 区的空间不足,再次晋升到老年代,而老年代内存也不够的情况下产生了 promotion failed,从而导致 Full GC
  • 统计得到的 Young GC 晋升到老年代的平均大小大于老年代的剩余空间
  • 主动触发 Full GC(执行 jmap -histo:live [pid])来避免碎片问题