logo头像
Snippet 博客主题

GC调优基础之必备知识

1. GC基础知识

1.1 内存管理

内存管理,百度百科的定义是:

它指软件运行时对计算机内存资源的分配和使用的技术。

其最主要的目的是如何高效、快速的分配,并且在适当的时候释放和回收内存资源。

首先,我们先来了解一下常见语言的内存管理。

C是通过两个标准库的函数malloc和free来完成内存空间的动态分配与释放。

C++通过两个运算符new和delete来完成内存空间的动态分配与释放。其实它的内部机制还是C的内存管理(malloc和free)。new可以认为malloc加构造函数(初始化)的执行,new侧重于内存分配和建立对象,而malloc只是分配一块内存。

由此可见,C和C++的内存管理都是我们开发人员手动控制的,免不了会出现问题:忘记回收和多次回收。

忘记回收就会导致内存泄露,这个很好理解。

而多次回收就会报错,导致系统很不稳定,一次free一个指针的时候,只是清空该指针所指的堆中的对应空间,但该指针变量在栈中的值并没有没清空,它还是指向原来分配的内存空间(但是该内存空间已经不属于该指针了,CPU随时可把该指针原来所指的空间分配给任何一个指针变量)。

这时,再free一次时,由于该指针已经没有堆空间与之对应了,所以编译器将会提示出错。

当一个指针被delete后,该指针就成了野指针(不指向任何内存空间的指针我们称之为野指针,野指针所指向的地方是随机的)。当再次delete该指针时,编译器就会提示你操作非法。

Java的内存管理全部是自动的,即自动分配内存、释放内存,开发者无需过多参与,编程简单,也不容易出错,大大提高了开发人员的效率。

正是因为Java屏蔽了内存管理细节,倘若我们的Java应用出现了内存泄露(该内存空间使用完毕后未回收,在不涉及复杂数据结构的一般情况下,表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有是也将其称为“对象游离”。)

这个时候我们需要更深入的分析、排查和解决问题, GC实现细节对于我们开发人员将显得非常重要,因此我们Java工程师必须从更底层了解Java内存管理的细节。

1.2 JVM常见术语

  • 并行(parallelism),指两个或者多个事件在同一时刻发生,在现代计算机中通常指多台处理器上同时处理多个任务。
  • 并发(concurrency),指两个或多个事件在同一时间间隔内发生,在现代计算机中一台处理器“同时”处理多个任务,那么这些任务只能交替运行,从处理器的角度上看任务只能串行执行,从用户的角度看这些任务“并行”执行,实际上是处理器根据一定的策略不断地切换执行这些“并行”的任务。
  • JVM中的并行,指多个垃圾回收相关线程在操作系统之上并发运行,这里的并行强调的是只有垃圾回收线程工作,Java应用程序都暂停执行,因此ParNew工作的时候一定发生了STW。
  • JVM中的并发,指垃圾回收相关的线程并发运行(如果启动多个线程),同时这些线程会和Java应用程序并发运行。
  • STW(Stop-The-World), 直译就是停止一切,在JVM中指停止一切Java应用线程。
  • 安全点(Safepoint),指JVM在执行一些操作的时需要STW,但并不是任何线程在任何地方都能进入STW,例如我们正在执行一段代码时,线程如何能够停止?设计安全点的目的是,当线程进入到安全点时,线程就会主动停止。
  • Mutator,它指的是我们的Java应用线程。Mutator的含义是可变的,在这里的含义是因为线程运行,导致了内存的变化。GC中通常需要STW才能使Mutator暂停。
  • GC Root,垃圾回收的根。在JVM的垃圾回收过程中,需要从GC Root出发标记活跃对象,确保正在使用的对象在垃圾回收后都是存活的。
  • 根集合(Root Set)。在JVM的垃圾回收过程中,需要从不同的GC Root出发,这些GC Root有线程栈、monitor列表、JNI对象等,而这些GC Root就构成了Root Set。
  • Full GC,简称为FGC,整个堆的垃圾回收动作。通常Full GC是串行的,G1的Full GC不仅有串行实现,在JDK10中还有并行实现。
  • Minor GC/Young GC,指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
  • 再标记(Remark)。指的是并发标记算法中,处理完并发标记后,需要更新并发标记中Mutator变更的引用,这一步需要STW。

1.3 垃圾定位算法

1.3.1 引用计数算法

引用计数算法,即Reference Count, 它是垃圾收集器中的早期策略。

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就+1;

当引用失效时,计数器值就-1;

任何时刻计数器为零的对象就是不可能再被使用的,会被当作垃圾收集。

优点:

原理简单,判断效率也高,可以很快的执行,交织在程序运行中。

对程序需要不被长时间打断的实时环境比较有利,Python的默认内存垃圾定位算法。

缺点:无法检测出循环引用,这种算法将会变得力不从心。因此Java并没有选用引用计数算法来管理内存

我们先看一段对象循环引用的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @projectName: ltmf
* @className: com.ltmf.gc.ReferenceCountTest
* @description: 证明Java定位垃圾不是引用计数算法
* @author: tong.li
* @createTime: 2020/5/24 10:43:22
* @version: v1.0
*/
public class ReferenceCountTest {

/** 成员引用对象 */
public ReferenceCountTest instance = null;

public static void main(String[] args) {
// 创建对象object1
ReferenceCountTest object1 = new ReferenceCountTest();
// 创建对象object2
ReferenceCountTest object2 = new ReferenceCountTest();
// 将object2对象的引用set到object1.instance引用变量中,使object1和object2互相引用,理论上引用计数器不可能为0,垃圾收集器就永远不会回收它们。
object1.instance = object2;
// 将object1对象的引用set到object2.instance引用变量中,使object1和object2互相引用,理论上引用计数器不可能为0,垃圾收集器就永远不会回收它们。
object2.instance = object1;
// 设置为object1为null,为了更快的GC,但是和object2互相引用
object1 = null;
// 设置为object2为null,为了更快的GC
object2 = null;
// 手动回收,调用System.gc(),先触发Minor GC,然后再触发Full GC
System.gc();
}

}

然后我们在运行时候添加JVM参数设置堆大小和GC日志打印:

1
2
# 设置堆的初始化大小和最大大小为50M, 并打印GC日志
-Xms50M -Xmx50M -XX:+PrintGCDetails

最后我们运行,运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
[GC (System.gc()) [PSYoungGen: 7027K->2021K(14848K)] 7027K->2053K(49152K), 0.0033907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 2021K->0K(14848K)] [ParOldGen: 32K->1932K(34304K)] 2053K->1932K(49152K), [Metaspace: 3316K->3316K(1056768K)], 0.0097631 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 14848K, used 128K [0x00000000fef80000, 0x0000000100000000, 0x0000000100000000)
eden space 12800K, 1% used [0x00000000fef80000,0x00000000fefa0178,0x00000000ffc00000)
from space 2048K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffe00000)
to space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)
ParOldGen total 34304K, used 1932K [0x00000000fce00000, 0x00000000fef80000, 0x00000000fef80000)
object space 34304K, 5% used [0x00000000fce00000,0x00000000fcfe3328,0x00000000fef80000)
Metaspace used 3339K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 355K, capacity 388K, committed 512K, reserved 1048576K

从上述GC日志分析出来,调用System.gc();后先触发了一次Minor GC, 随后触发了一次Full GC,

[PSYoungGen: 7027K->2021K(14848K)] 7027K->2053K(49152K),先回收掉了新生代的垃圾。
[ParOldGen: 32K->1932K(34304K)] 2053K->1932K(49152K)。

由此可见,循环引用的对象也回收了,也证实了Java并不是通过引用计数算法来判断对象是否存活的。

接下来,我们证明一下Python循环引用对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin python3.6
# -*- encoding: utf-8 -*-
"""
@File : __init__.py.py
@Description : Python循环依赖问题
@Author : tong.li
@Email : lt_alex@163.com
@Blog : http://ltmf.top
@Time : 2020/05/24 下午8:31
"""
# 可以用来检测内存泄漏的类库
import objgraph

if __name__ == '__main__':
a = list()
b = list()
# 查看list类型对象的数量
print(objgraph.count('list'))
# 循环引用
a.append(b)
# 循环引用
b.append(a)

print(a)
print(b)
# 删除对象
del a
del b
# 再次查看list类型对象的数量
print(objgraph.count('list'))

执行上述结果为:

1
2
3
4
5
6
# 打印list类型内部的对象数
158
[[[...]]]
[[[...]]]
# 删除对象后,list类型内部的对象数仍然为158,说明并没有回收
158

若我们在代码上注释掉循环引用的两行代码,循环引用打印结果为:

1
2
3
4
5
6
# 打印list类型内部的对象数
158
[]
[]
# del操作后,两个对象会回收,所以会变为156,此时我们可以判断确实存在循环对象引用无法回收
156

当然我们打开注释,并在最后一行代码添加一些代码:

1
2
# 调用GC收集器
gc.collect()

添加并执行,我们发现 gc.collect()解决了循环引用的回收问题。由此可见,Python默认的垃圾定位算法是引用计数算法。

1.3.2 根可达性分析算法

根可达性分析算法,即Root Reachability Analysis,当前主流语言(Java、C#)都是基于根可达性分析算法来判定对象的存活。

根搜索算法是从离散数学中的图论引入的,它的基本思路是程序把所有的引用关系看作一张图,通过一系列称为“GC Roots”的根对象作为起始节点集,

从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),

如果某个对象到GCRoots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

image

上图表示了对象引用图,蓝色对象引用表示对象存活,

而橙色的ObjF、ObjE、ObjeD三个对象是GC Root不可达,我们可以断定出这三个对象是可回收的对象。

即使ObjD和ObjE两个对象之间有引用关系,也算垃圾对象。

在Java中,可作为GC Root的对象有:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 方法区中静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 本地方法栈中引用的JNI对象(Native对象)。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

除了以上GC Root,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。

比如分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集),必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不可见的),更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。

目前最新的几款垃圾收集器(G1、Shenandoah、ZGC)无一例外都具备了局部回收的特征,为了避免GC Roots包含过多对象而过度膨胀,它们在实现上也做出了各种优化处理。

需要注意的是,当通过根可达性分析出那些引用不可达的对象时候,只能初步断定为可回收的对象,并不是立即回收该对象,一个对象的死亡需要经历两次标记:

  1. 判断标记为可回收对象。
  2. 标记筛选是否有必要执行finalize()方法
    1. 若对象重写finalize()方法或finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
    2. 若对象没有2.1的上述情况,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。

2.1所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。

这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记。

如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。以下代码,就是对象唯一一次的自我拯救:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* @projectName: ltmf
* @className: com.ltmf.gc.FinalizeEscapeGc
* @description: 对象在GC时可以自救一次,因为finalize()方法只调用一次
* @author: tong.li
* @createTime: 2020/5/23 14:52
* @version: v1.0
*/
public class FinalizeEscapeGC {

/**
* 持有的成员变量
*/
public static FinalizeEscapeGC SAVE_HOOK = null;

/**
* 判断是否存活的方法
*/
public void isAlive() {
System.out.println("yes, i am still alive :)");
}

/**
* 重写finalize(),生成环境禁止重写finalize()方法
* @throws Throwable
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
// 自己引用自己,自救
FinalizeEscapeGC.SAVE_HOOK = this;
}

public static void main(String[] args) throws Throwable {
// 实例化一个对象
SAVE_HOOK = new FinalizeEscapeGC();

//对象第一次成功拯救自己
SAVE_HOOK = null;
// 触发gc,但是不一定触发垃圾回收
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,等待它...
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}

// 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为finalize()方法优先级很低,暂停0.5秒,等待它...
Thread.sleep(500);...
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
1
2
3
4
5
6
# finalize()执行
finalize method executed!
# 第一次对象自救成功
yes, i am still alive :)
# 第二次对象自救失败
no, i am dead :(

1.4 垃圾回收算法实现

1.4.1 标记清除算法

标记清除(Mark-Sweep),最早的最基础的垃圾回收算法。

它分为两个阶段:标记阶段和清除阶段。从根集合出发,标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。也可以反着来。标记存活的对象,统一回收所有未被标记的对象。

优点:实现简单,不需要进行对象移动,在存活对象比较多的情况下极为高效。

缺点:一是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率(两遍扫描)都随对象数量增长而降低;二是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记清除

1.4.2 复制算法

标记-复制算法,我们常叫做复制(copying)算法。

它的实现也有很多种,可以使用两个分区,也可以使用多个分区。

使用两个分区时内存的利用率只有50%;使用多个分区(如3个分区),则可以提高内存的使用率。

工作原理:当一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

因此,我们可以总结出其的优缺点:

优点:实现简单,运行高效,没有内存碎片。

缺点:内存缩小原来的一半,空间浪费严重。在对象存活率较高时就要进行较多的复制操作,效率将会降低。

复制算法图示

1.4.3 标记压缩算法

标记压缩(mark compact),和标记清除算法类似,在此基础上将存活的对象都向内存空间一端移动,将垃圾对象移动到内存空间的另一端,然后直接清理掉边界以外的内存,直接对可回收对象进行清理。

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

优点:没有碎片,不需要类似复制算法的分配担保空间。

缺点:移动对象,指针需要调整,两次扫描,效率偏低。

标记整理算法图示

1.5 JVM内存分代收集模型

分代收集(Generational Collection)就是把内存划分成不同的区域进行管理,其思想来源是:有些对象存活的时间短,有些对象存活的时间长,把存活时间短的对象放在一个区域管理,把存活时间长的对象放在另一个区域管理。

那么可以为两个不同的区域选择不同的算法,加快垃圾回收的效率。我们假定内存被划分成2个代:新生代和老生代。这样的组合算法,使得堆的分配率和使用率大大提高。

把容易死亡的对象放在新生代,通常采用复制算法回收;

把预期存活时间较长的对象放在老生代,通常采用标记整理算法。

Generational Collection

HotSpot虚拟机默认把新生代分为三部分:Eden区、Surivor1区、Surivor2区。三区之间的比例是8:1:1。

新生代发生的GC也叫做Minor GC或YGC,MinorGC/YGC发生频率比较高(不一定等Eden区满了才触发)

其工作机制是:每次分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。如此往复,当新生代无法分配内存时,会存在内存分配担保机制,会把新生代存活的对象转移到老年代中。

老年代是指在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。老年代内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

除此之外,永久代(Permanent Generation) 在堆区之外还有一个代就是永久代,用于存放静态文件,如Java类、常量、方法描述等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。对永久代的回收主要回收两部分内容:废弃常量和无用的类。JDK8移除了永久代,改为元空间。

2. 垃圾回收器

常见的GC垃圾回收器

如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。

上图就是十种常见的垃圾回收器。虚线连接的两个垃圾回收器可搭配使用。

2.1 Serial收集器

最基础、历史最悠久的单线程串行收集器,JDK1.3之前的唯一收集器,针对的是新生代垃圾回收。

主要针对的是几十兆甚至一两百兆的的垃圾回收,最多一百多毫秒以内的回收,运行在客户端模式下是个很好的选择,但是收集过程中一定会发生STW。

2.2 Serial Old收集器

Serial Old是Serial收集器的老年代版本,在服务器端模式下

2.3 ParNew收集器

是Serial收集器多线程并行版本,多运行在服务器端,JDK7之前首选的新生代并行收集器。除Serial收集器外,目前只有它能与CMS收集器配合使用。

2.4 CMS收集器

CMS(Concurrent Mark Sweep)收集器 。划时代意义的老年代垃圾回收器,可以支持几十个G 的垃圾回收。

这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。

美中不足的是无法与Parallel Scavenge新生代垃圾回收器配合使用。只能与ParNew或者Serial收集器中的一个配合使用。

CMS收集器是基于标记-清除算法实现的。主要收集回收过程为四步:初始标记、并发标记、重新标记、并发清除。

初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;

并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;

而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;

并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

综上所述,我们总结出初始标记、重新标记仍然需要STW。

CMS存在的问题是无法处理“浮动垃圾”,有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。

在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。

还有一个缺点就是采用标记-清楚算法,收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

2.5 Parallel Scavenge收集器

CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。

2.6 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。JDK1.6开始提供。

JDK1.8默认的垃圾回收是Parallel Scavenge+Parallel Old。

2.7 G1收集器

继承和改进了CMS收集器,G1面向全堆的收集器闪亮登场,不再需要其他新生代收集器的配合工作。

JDK9开始,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。

它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。支持上百G的垃圾回收,停顿时间更小。目标是替换JDK1.5的CMS收集器。它可以指定最大停顿时间。可将停顿时间缩小在200ms - 10ms之间。

G1之前的所有收集器都是全堆(要么整个新生代、要么整个老年代)垃圾回收,而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。

G1收集器的运作过程大致可划分为以下四个步骤:初始标记、并发标记、最终标记、筛选回收。

G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。

相比CMS,G1的优点有很多,可以指定最大停顿时间、分Region的内存布局、按收益动态确定回收集这些创新性设计带来的红利。

从整体看,G1是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何都不会产生内存碎片。

2.8 ZGC收集器

是一款在JDK 11中新加入的具有实验性质的低延迟垃圾收集器,是由Oracle公司研发的,支持4T-16T的垃圾回收。

ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。可将停顿时间缩小在10ms - 1ms之间。

回收算法使用:ColoredPointers + LoadBarrier

3. JVM命令行参数

要想JVM调优,我们首先必须熟悉JVM常用命令行参数。根据官方文档描述,HotSpot参数分为以下三类:

  • 标准: - 开头,所有的HotSpot都支持。
  • 非标准:-X 开头,特定版本HotSpot支持特定命令。
  • 不稳定:-XX 开头,下个版本可能取消。

3.1 标准参数

当我们在命令行输入java -help显示以-开始都是标准的参数选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
litong@LT:~$ java -help
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=gasp
用法: java [-options] class [args...]
(执行类)
或 java [-options] -jar jarfile [args...]
(执行 jar 文件)
其中选项包括:
-d32 使用 32 位数据模型 (如果可用)
-d64 使用 64 位数据模型 (如果可用)
-server 选择 "server" VM
默认 VM 是 server,
因为您是在服务器类计算机上运行。
-cp <目录和 zip/jar 文件的类搜索路径>
-classpath <目录和 zip/jar 文件的类搜索路径>
用 : 分隔的目录, JAR 档案
和 ZIP 档案列表, 用于搜索类文件。
-D<名称>=<值>
设置系统属性
-verbose:[class|gc|jni]
启用详细输出
-version 输出产品版本并退出
-version:<值>
警告: 此功能已过时, 将在
未来发行版中删除。
需要指定的版本才能运行
-showversion 输出产品版本并继续
-jre-restrict-search | -no-jre-restrict-search
警告: 此功能已过时, 将在
未来发行版中删除。
在版本搜索中包括/排除用户专用 JRE
-? -help 输出此帮助消息
-X 输出非标准选项的帮助
-ea[:<packagename>...|:<classname>]
-enableassertions[:<packagename>...|:<classname>]
按指定的粒度启用断言
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>]
禁用具有指定粒度的断言
-esa | -enablesystemassertions
启用系统断言
-dsa | -disablesystemassertions
禁用系统断言
-agentlib:<libname>[=<选项>]
加载本机代理库 <libname>, 例如 -agentlib:hprof
另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>]
按完整路径名加载本机代理库
-javaagent:<jarpath>[=<选项>]
加载 Java 编程语言代理, 请参阅 java.lang.instrument
-splash:<imagepath>
使用指定的图像显示启动屏幕
有关详细信息, 请参阅 http://www.oracle.com/technetwork/java/javase/documentation/index.html。

3.2 非标准参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
litong@LT:~$ java -X  # 输出java非标准参数
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=gasp
-Xmixed 混合模式执行 (默认)
-Xint 仅解释模式执行
-Xbootclasspath:<用 : 分隔的目录和 zip/jar 文件>
设置搜索路径以引导类和资源
-Xbootclasspath/a:<用 : 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
-Xbootclasspath/p:<用 : 分隔的目录和 zip/jar 文件>
置于引导类路径之前
-Xdiag 显示附加诊断消息
-Xnoclassgc 禁用类垃圾收集
-Xincgc 启用增量垃圾收集
-Xloggc:<file> 将 GC 状态记录在文件中 (带时间戳)
-Xbatch 禁用后台编译
-Xms<size> 设置初始 Java 堆大小
-Xmx<size> 设置最大 Java 堆大小
-Xss<size> 设置 Java 线程堆栈大小
-Xprof 输出 cpu 配置文件数据
-Xfuture 启用最严格的检查, 预期将来的默认值
-Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文档)
-Xcheck:jni 对 JNI 函数执行其他检查
-Xshare:off 不尝试使用共享类数据
-Xshare:auto 在可能的情况下使用共享类数据 (默认)
-Xshare:on 要求使用共享类数据, 否则将失败。
-XshowSettings 显示所有设置并继续
-XshowSettings:all
显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
显示所有属性设置并继续
-XshowSettings:locale
显示所有与区域设置相关的设置并继续

-X 选项是非标准选项, 如有更改, 恕不另行通知。

3.3 不稳定参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
litong@LT:~$ java -XX:+PrintFlagsFinal | head -n 50 # 这里只列举前50个不稳定参数
[Global flags]
intx ActiveProcessorCount = -1 {product}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AdaptiveSizePausePolicy = 0 {product}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product}
uintx AdaptiveSizePolicyInitializingSteps = 20 {product}
uintx AdaptiveSizePolicyOutputInterval = 0 {product}
uintx AdaptiveSizePolicyWeight = 10 {product}
uintx AdaptiveSizeThroughPutPolicy = 0 {product}
uintx AdaptiveTimeWeight = 25 {product}
bool AdjustConcurrency = false {product}
bool AggressiveHeap = false {product}
bool AggressiveOpts = false {product}
intx AliasLevel = 3 {C2 product}
bool AlignVector = false {C2 product}
intx AllocateInstancePrefetchLines = 1 {product}
intx AllocatePrefetchDistance = 192 {product}
intx AllocatePrefetchInstr = 0 {product}
intx AllocatePrefetchLines = 4 {product}
intx AllocatePrefetchStepSize = 64 {product}
intx AllocatePrefetchStyle = 1 {product}
bool AllowJNIEnvProxy = false {product}
bool AllowNonVirtualCalls = false {product}
bool AllowParallelDefineClass = false {product}
bool AllowUserSignalHandlers = false {product}
bool AlwaysActAsServerClassMachine = false {product}
bool AlwaysCompileLoopMethods = false {product}
bool AlwaysLockClassLoader = false {product}
bool AlwaysPreTouch = false {product}
bool AlwaysRestoreFPU = false {product}
bool AlwaysTenure = false {product}
bool AssertOnSuspendWaitFailure = false {product}
bool AssumeMP = false {product}
intx AutoBoxCacheMax = 128 {C2 product}
uintx AutoGCSelectPauseMillis = 5000 {product}
intx BCEATraceLevel = 0 {product}
intx BackEdgeThreshold = 100000 {pd product}
bool BackgroundCompilation = true {pd product}
uintx BaseFootPrintEstimate = 268435456 {product}
intx BiasedLockingBulkRebiasThreshold = 20 {product}
intx BiasedLockingBulkRevokeThreshold = 40 {product}
intx BiasedLockingDecayTime = 25000 {product}
intx BiasedLockingStartupDelay = 4000 {product}
bool BindGCTaskThreadsToCPUs = false {product}
bool BlockLayoutByFrequency = true {C2 product}
intx BlockLayoutMinDiamondPercentage = 20 {C2 product}
bool BlockLayoutRotateLoops = true {C2 product}
bool BranchOnRegister = false {C2 product}
bool BytecodeVerificationLocal = false {product}

3.4 常用参数

参数 描述
-Xmx 1024m 指定堆的最大大小(以字节为单位)。此值必须是1024的倍数且大于2 MB。该-Xmx选项等效于-XX:MaxHeapSize
-Xms 1024m 设置堆的初始大小(以字节为单位)。此值必须是1024的倍数且大于1 MB。如果未设置此选项,则初始大小将设置为为老年代和年轻代分配的大小之和。可以使用-Xmn选项或-XX:NewSize选项设置年轻代的堆的初始大小。
-Xmn 256m 设置年轻代的堆的初始大小和最大大小(以字节为单位)。堆的年轻代区域用于新对象分配使用。与在其他区域相比,在该区域执行GC的频率更高。如果年轻代的大小太小,则会执行许多次Minor GC。如果大小太大,那么将仅执行Full GC,这可能需要很长时间才能完成。Oracle建议您将年轻代的大小保持在整个堆大小的一半到四分之一之间。您可以使用-XX:NewSize设置年轻代的初始大小和-XX:MaxNewSize设置年轻代的最大尺寸。
-XX:NewRatio = 2 设置年轻代与老年代大小(不含永久代/元空间)之间的比率。默认情况下,此选项设置为2。
-XX:MetaspaceSize = 11 设置分配的类元空间的大小,该类元空间将在首次超过垃圾收集时触发垃圾收集。垃圾收集的阈值取决于使用的元数据量而增加或减少。默认大小取决于平台。
-Xloggc:filename 设置应将详细的GC事件信息重定向到该文件以进行日志记录的文件。从-verbose:gc每个记录的事件之前的第一个GC事件开始,写入此文件的信息与经过时间后的输出类似。如果两者都使用同一命令给出,则该-Xloggc选项将覆盖。-verbose:gcjava
-XX:+UseTLAB 允许在年轻代空间中使用线程本地分配块(TLAB)。默认情况下启用此选项。要禁用TLAB,请指定-XX:-UseTLAB
-XX:TLABSize=512k 设置线程本地分配缓冲区(TLAB)的初始大小(以字节为单位)。如果此选项设置为0,那么JVM将自动选择初始大小。
-XX:MaxTenuringThreshold=15 设置用于自适应GC大小调整的最大使用期限阈值(垃圾的最大年龄)。最大值为15。并行(吞吐量)收集器的默认值为15,而CMS收集器的默认值为6。
-XX:+UseParallelGC 启用并行垃圾收集器(也称为吞吐量收集器)来利用多个处理器来提高应用程序的性能。默认情况下,此选项是禁用的。若-XX:+UseParallelOldGC启用,默认-XX:+UseParallelGC选项也会启用,除非你手动禁用该选项。选择的是Parallel Scavenge + Parallel Old/SerialOld垃圾回收器
-XX:+UseParallelOldGC 允许将并行垃圾收集器用于Full GC,即老年代。默认情况下,此选项是禁用的。启用它会自动启用该-XX:+UseParallelGC选项。选择的是Parallel Scavenge + Parallel Old垃圾回收器
-XX:+UseParNewGC 允许在年轻代中使用并行线程进行收集。默认情况下,此选项是禁用的。设置该-XX:+UseConcMarkSweepGC选项时,它将自动启用。JDK\ 8启用了-XX:+UseParNewGC+-XX:+UseConcMarkSweepGC的选择。
-XX:+ UseSerialGC 启用串行垃圾收集器的使用。对于不需要垃圾回收具有任何特殊功能的小型和简单应用程序,这通常是最佳选择。默认情况下,此选项是禁用的,并且将根据计算机的配置和JVM的类型自动选择收集器。
-XX:+UseConcMarkSweepGC 启用CMS用于老年代的垃圾收集器。当吞吐量(-XX:+UseParallelGC)垃圾收集器无法满足应用程序延迟要求时,Oracle建议您使用CMS垃圾收集器。G1垃圾收集器(-XX:+UseG1GC)是另一种选择。默认情况下,此选项是禁用的,并且将根据计算机的配置和JVM的类型自动选择收集器。启用此选项后,将-XX:+UseParNewGC自动设置该选项,并且您不应禁用它,因为JDK 8中已弃用以下选项组合:-XX:+UseConcMarkSweepGC -XX:-UseParNewGC
-XX:+UseG1GC 启用垃圾优先(G1)垃圾收集器的使用。它是服务器类型的垃圾收集器,适用于具有大量RAM的多处理器计算机。它极有可能满足GC暂停时间目标,同时保持良好的吞吐量。建议将G1收集器用于需要大堆(大小约为6 GB或更大)且GC延迟要求有限(稳定且可预测的暂停时间低于0.5秒)的应用程序。
-XX:MaxGCPauseMillis=500 设置最大GC暂停时间的目标(以毫秒为单位)。这是一个软目标,并且JVM将尽最大的努力来实现它。默认情况下,没有最大暂停时间值。
-XX:InitialSurvivorRatio=4 设置吞吐量优先的垃圾收集器使用的初始survivor空间比率(由-XX:+UseParallelGC和/或- XX:+UseParallelOldGC选项启用)。缺省情况下,吞吐量垃圾收集器通过使用-XX:+UseParallelGC-XX:+UseParallelOldGC选项来启用自适应大小调整,并根据应用程序的行为从初始值开始调整幸存空间的大小。如果禁用了自适应大小调整(使用该-XX:-UseAdaptiveSizePolicy选项),-XX:SurvivorRatio则应使用该选项来设置整个应用程序执行过程中surivor空间的大小。以下公式可用于根据年轻代(Y)的大小和初始survivor空间比率(R)计算幸存者空间的初始大小(S):S = Y /(R + 2),等式中的2表示两个survivor空间。指定为初始survivor空间比率的值越大,初始幸存者空间大小越小。默认情况下,初始survivor空间比率设置为8。如果使用了年轻代空间大小的默认值(2 MB),则survivor空间的初始大小将为0.2 MB。
-XX:+ UseAdaptiveSizePolicy 用自适应生成大小调整。默认情况下启用此选项。要禁用自适应生成大小调整,-XX:-UseAdaptiveSizePolicy请显式指定并设置内存分配池的大小(请参阅该-XX:SurvivorRatio选项)。
-XX:SurvivorRatio=8 设置eden空间大小与surivor空间大小之间的比率。默认情况下,此选项设置为8。
-XX:TargetSurvivorRatio=30 设置垃圾回收后所需的surivor空间百分比(0到100)。默认情况下,此选项设置为50%。
-XX:ConcGCThreads=2 设置用于并发GC的线程数。缺省值取决于JVM可用的CPU数量。
-XX:+DisableExplicitGC 禁用对System.gc()的调用,但jvm在必要时仍会执行GC。
-XX:+ScavengeBeforeFullGC 在每个Full GC之前启用一次YGC。默认情况下启用此选项。Oracle建议您不要禁用它,因为在Full GC之前清除年轻代可以减少从老年代空间到年轻代空间可访问的对象数量。禁用的话,请指定-XX:-ScavengeBeforeFullGC
-XX:+HeapDumpOnOutOfMemoryError Java.lang.OutOfMemoryError引发异常时,使用堆分析器(HPROF)启用将Java堆转储到当前目录中的文件的功能。您可以使用该-XX:HeapDumpPath选项显式设置堆转储文件的路径和名称。默认情况下,禁用此选项,并且在OutOfMemoryError引发异常时不转储堆。
-XX:HeapDumpPath=path 置-XX:+HeapDumpOnOutOfMemoryError选项时,设置用于写入由堆分析器(HPROF)提供的堆转储的路径和文件名。默认情况下,在当前工作目录中创建该文件,并将其命名为java_pidpid.hprof,其中pid是导致错误的进程的标识符。下面的示例演示如何显式设置默认文件(%p代表当前进程标识符):-XX:HeapDumpPath=./java_pid%p.hprof
-XX:LogFile=path 设置写入日志数据的路径和文件名。默认情况下,该文件在当前工作目录中创建,并且名为hotspot.log。
-XX:+PrintConcurrentLocks 启用java.util.concurrent在Control+C事件(SIGTERM)之后打印锁。默认情况下,此选项是禁用的。设置此选项等效于运行jstack -l命令或jcmd pid Thread.print -l命令,其中pid是当前Java进程标识符。
-XX:+PrintGCDetails 启用在每个GC上打印详细信息的功能。默认情况下,此选项是禁用的。
-XX:-UseCompressedOops 禁用压缩指针(普通对象指针压缩)的使用。默认情况下,此选项处于启用状态,并且当Java堆大小小于32 GB时,将使用压缩指针。启用此选项后,对象引用将表示为32位偏移量而不是64位指针,这通常在运行Java堆大小小于32 GB的应用程序时提高性能。此选项仅适用于64位JVM。
-XX:+UseCompressedClassPointers 类指针压缩,对象头的class Point部分的压缩。
-XX:+ PrintCommandLineFlags 当Java堆大小大于32GB时,也可以使用压缩指针。参见-XX:ObjectAlignmentInBytes选项。java -XX:+PrintCommandLineFlags -version可打印出JDK8的默认启用参数。
-XX:+PrintFlagsFinal 打印JVM中-XX的最终的参数列,默认不启用。
-XX:+PrintFlagsInitial 打印默认参数值

4. 调优工具使用

4.1 JDK自带

JDK为我们自带的一些JVM故障与诊断工具,我们可以在Oracle官方故障排除指南上查询相关的资料。

4.1.1 jps命令

Jps(JVM Process Status Tool),虚拟机进程状况工具。

它可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)。

对于本地虚拟机进程来说,LVMID与操作系统的进程ID(PID,Process Identifier)是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
# 虚拟机进程状况工具
litong@LT:/media/litong/软件/programs/tools/arthas$ jps -l # -l 输出主类的全名,若执行的是jar包,输出jar包的路径
1909 arthas-demo.jar
16058 sun.tools.jps.Jps
litong@LT:/media/litong/软件/programs/tools/arthas$ jps -q # -q 只输出LVMID,和jps不带选项参数结果一致
1909
21372
litong@LT:/media/litong/软件/programs/tools/arthas$ jps -v # -v 输出虚拟机进程启动时的JVM参数
30201 Jps -Dapplication.home=/media/litong/软件/programs/tools/jdk/jdk1.8.0_241 -Xms8m -Dawt.useSystemAAFontSettings=gasp
litong@LT:/media/litong/软件/programs/tools/arthas$ jps -m # -m 输出虚拟机进程中启动时传递给main()的参数
1909 jar
5032 Jps -m

4.1.2 jstat命令

jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。

它可以显示本地或者远程[插图]虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有GUI图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
litong@LT:~$ jstat -gc 1909 1000 2 # 查询Java进程为1909的gc情况(每秒查询,总共查两次,后面两个参数没有,默认只查询当前一次gc情况)
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
7680.0 7680.0 0.0 0.0 48640.0 5837.4 128512.0 0.0 4480.0 780.9 384.0 76.6 0 0.000 0 0.000 0.000
7680.0 7680.0 0.0 0.0 48640.0 5837.4 128512.0 0.0 4480.0 780.9 384.0 76.6 0 0.000 0 0.000 0.000
litong@LT:~$ jstat -gcutil 1909 1000 2 # 与-gc内容相似,区别在于这里显示的是已使用空间占总空间的百分比
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 12.00 0.00 17.43 19.94 0 0.000 0 0.000 0.000
0.00 0.00 12.00 0.00 17.43 19.94 0 0.000 0 0.000 0.000
litong@LT:~$ jstat -gccause 1909 # 与-gcutil内容类似,只不过多输出一列:上一次垃圾回收的原因
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC
0.00 0.00 14.00 0.00 17.43 19.94 0 0.000 0 0.000 0.000 No GC No GC
litong@LT:~$ jstat -gccapacity 1909 # 与-gc内容相似,区别在于输出主要关注Java堆各个区域使用的最大、最小空间
NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC
64000.0 1017856.0 64000.0 7680.0 7680.0 48640.0 128512.0 2035712.0 128512.0 128512.0 0.0 1056768.0 4480.0 0.0 1048576.0 384.0 0 0
litong@LT:~$ jstat -class 1909 # 监视类加载、卸载数量、总空间以及类装载所耗费的时间
Loaded Bytes Unloaded Bytes Time
554 1118.9 0 0.0 0.71
litong@LT:~$ jstat -gcnew 1909 # 监视新生代的垃圾回收情况
S0C S1C S0U S1U TT MTT DSS EC EU YGC YGCT
7680.0 7680.0 0.0 0.0 15 15 0.0 48640.0 6810.2 0 0.000
litong@LT:~$ jstat -gcnewcapacity 1909 # 与-gcnew内容类似,区别在于输出主要关注新生代各个区域使用的最大、最小空间
NGCMN NGCMX NGC S0CMX S0C S1CMX S1C ECMX EC YGC FGC
64000.0 1017856.0 64000.0 338944.0 7680.0 338944.0 7680.0 1016832.0 48640.0 0 0
litong@LT:~$ jstat -gcold 1909 # 监视老年代的垃圾回收情况
MC MU CCSC CCSU OC OU YGC FGC FGCT GCT
4480.0 780.9 384.0 76.6 128512.0 0.0 0 0 0.000 0.000
litong@LT:~$ jstat -gcoldcapacity 1909 # 与-gcold内容类似,区别在于输出主要关注新生代各个区域使用的最大、最小空间
OGCMN OGCMX OGC OC YGC FGC FGCT GCT
128512.0 2035712.0 128512.0 128512.0 0 0 0.000 0.000
litong@LT:~$ jstat -compiler 1909 # 输出即时编译器编译过的方法、耗时等信息
Compiled Failed Invalid Time FailedType FailedMethod
290 0 0 0.30 0
litong@LT:~$ jstat -printcompilation 1909 # 输出已经被即时编译的方法
Compiled Size Type Method
290 25 1 java/util/ArrayList$Itr hasNext
缩写符 说明
E、EC、EU 新生代Eden区,新生代中Eden的容量 (字节),新生代中Eden目前已使用空间 (字节)
S0、S0C、S0U 新生代Survivor0区,新生代第1个Survivor区的容量 (字节),新生代第1个Survivor区目前已使用空间的容量 (字节)
S1、S1C、S1U 新生代Survivor1区,新生代第2个Survivor区的容量 (字节),新生代第2个Survivor区目前已使用空间的容量 (字节)
O、OC、OU 老年代区以及老年代的容量和目前使用的容量(字节)
M、MC、MU 元空间区以及元空间的容量和目前使用的容量(字节)
CCS、CCSC、CCSU 类指针压缩空间以及类指针压缩空间的容量和目前使用的容量(字节)
YGC Young GC,新生代GC次数
YGCT Young GC Time,新生代GC总时长(毫秒)
FGC Full GC回收次数
FGCT Full GC Time,Full GC总时长(毫秒)
GCT GC Time,所有GC总耗时(毫秒)
GCC、LGCC GC Cause,当前GC的原因;Last GC Cause,上一次GC的原因;

4.1.3 jmap命令

jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。

不使用jmap命令,我们可以使用-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件,通过-XX:+HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成堆转储快照文件。

1
2
3
litong@LT:~$ jmap -dump:format=b,file=arthas.dump 1909 # 生成此时的dump堆转存储文件
Dumping heap to /home/litong/arthas.dump ...
Heap dump file created

4.1.4 jinfo命令

jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
litong@LT:~$ jinfo -flag MaxHeapSize 1909 # 查看1909这个JVM进程的最大堆大小
-XX:MaxHeapSize=3126853632
litong@LT:~$ jmap -histo 1909 | head -20 # 显示堆中对象统计信息,包括类、实例数量、合计容量
num #instances #bytes class name
----------------------------------------------
1: 63705 3518368 [C
2: 7790 3014168 [I
3: 36707 880968 java.lang.String
4: 12870 810888 [Ljava.lang.Object;
5: 9508 456384 java.nio.HeapCharBuffer
6: 4828 386240 [S
7: 14565 349560 java.lang.StringBuilder
8: 411 177088 [B
9: 2397 153408 java.text.DecimalFormatSymbols
10: 2397 153408 java.util.regex.Matcher
11: 4794 115056 java.util.Formatter$FixedString
12: 4754 114096 java.util.ArrayList
13: 2397 95880 java.util.Formatter$FormatSpecifier
14: 2397 76704 [Ljava.util.Formatter$FormatString;
15: 2397 76704 java.lang.IllegalArgumentException
16: 2397 76704 java.util.Formatter
17: 2345 75040 java.util.ArrayList$Itr

4.1.5 jhat命令

JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。

jhat内置了一个微型的HTTP/Web服务器,生成堆转储快照的分析结果后,可以在浏览器中查看。

实际用的不多,主要原因有两个方面:

一般不会在部署应用程序的服务器上直接分析堆转储快照,即使可以这样做,也会尽量将堆转储快照文件复制到其他机器上进行分析,因为分析工作是一个耗时而且极为耗费硬件资源的过程,既然都要在其他机器上进行,就没有必要再受命令行工具的限制了。

jhat的分析功能相当的简陋,相比其他的分析工具VisualVM以及专业用于分析堆转储快照文件的Eclipse MemoryAnalyzer、IBM HeapAnalyzer没有竞争力。

1
2
3
4
5
6
7
8
9
10
litong@LT:~$ jhat ./arthas.dump  # 在线分析dump文件,在浏览器打开http://localhost:7000即可分析
Reading from ./arthas.dump...
Dump file created Wed May 24 22:19:29 CST 2020
Snapshot read, resolving...
Resolving 186238 objects...
Chasing references, expect 37 dots.....................................
Eliminating duplicate references.....................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

4.1.6 jstack命令

jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。

线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
itong@LT:~$ jstack -l 1909 # 除过堆栈外,显示关于所的附加信息
2020-05-24 22:37:09
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.241-b07 mixed mode):

"Attach Listener" #9 daemon prio=9 os_prio=0 tid=0x00007f7db0001000 nid=0x77e waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

Locked ownable synchronizers:
- None

"Service Thread" #8 daemon prio=9 os_prio=0 tid=0x00007f7dec19b000 nid=0x837 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

Locked ownable synchronizers:
- None

"C1 CompilerThread2" #7 daemon prio=9 os_prio=0 tid=0x00007f7dec185800 nid=0x836 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

Locked ownable synchronizers:
- None

"C2 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f7dec184000 nid=0x835 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

Locked ownable synchronizers:
- None

"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f7dec181000 nid=0x834 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

Locked ownable synchronizers:
- None

"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f7dec17f800 nid=0x833 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

Locked ownable synchronizers:
- None

"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f7dec14f000 nid=0x832 in Object.wait() [0x00007f7ddc6f5000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000781e08ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x0000000781e08ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

Locked ownable synchronizers:
- None

"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f7dec14a800 nid=0x831 in Object.wait() [0x00007f7ddc7f6000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000781e06c00> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x0000000781e06c00> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

Locked ownable synchronizers:
- None

"main" #1 prio=5 os_prio=0 tid=0x00007f7dec00b800 nid=0x81e waiting on condition [0x00007f7df5ac1000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at demo.MathGame.main(MathGame.java:17)

Locked ownable synchronizers:
- None

"VM Thread" os_prio=0 tid=0x00007f7dec140800 nid=0x830 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f7dec021000 nid=0x82c runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f7dec022800 nid=0x82d runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007f7dec024800 nid=0x82e runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007f7dec026000 nid=0x82f runnable

"VM Periodic Task Thread" os_prio=0 tid=0x00007f7dec1a5800 nid=0x838 waiting on condition

JNI global references: 5

4.1.7 jconsole命令

图形化上述命令的结果,可以试用学习,生产环境不建议使用。

4.1.8 jvisualvm命令

与jcomsole类似,是jconsole高级版本,可以试用学习,生产环境不建议使用。

4.2 Arthas

Arthas是阿里开源的Java诊断工具。在这里就不多详讲,读者请移步这里先行学习,后面我会单独写一篇Arthas入门指南。

4.3 JProfiler

JProfiler直观的用户界面可帮助您解决性能瓶颈,查明内存泄漏并了解线程问题。但是遗憾的是jprofiler是收费的。如果想要学习请移步这里

相比JProfiler,我们更愿意推荐使用阿里开源的Arthas性能排查工具。

5. 面试连环炮

  • 内存泄露和内存溢出的区别,怎么判断内存泄露

  • 老年代与年轻代、元空间划分比例

  • 多大的对象会直接被分配到老年代里

  • JVM堆内存管理(对象分配过程)

  • 对象在内存中的布局

    在HotSpot虚拟机中,对象在内存中的布局分为三部分:对象头、实例数据、对齐填充。

    1. 对象头:包括两部分:Mark Word、类型指针、数组长度(如果是数组类型)
      1. Mark Word:存储对象自身的运行时数据(哈希码、GC年龄、锁标志、持有的锁等)。被设计成非固定数据结构,根据对象状态占用内部空间。
      2. Class Poniter:对象指向它的类元数据的指针。虚拟机通过这个指针确定对象是哪个类的实例。
      3. length:数组长度,对于数组对象,对象头中必须有一块数据记录数组长度,因为JVM无法从数组的元数据确定数组的大小。
    2. Instance Data:实例数据是对象真正存储的有效信息,就是代码中定义的各种类型的字段内容,包括从父类继承下来的和子类中定义的。
    3. Padding: HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍。当对象的实例数据部分没有对齐,用对齐填充补全。所以对齐填充不是必然存在的。
  • 为什么压缩指针超过32G就失效

  • Object o = new Object()在内存中占用了多少个字节
  • 对象怎么定位(直接,间接)
  • 吞吐量优先和响应时间优先垃圾回收器有哪些
  • CMS的回收流程是什么
  • CMS的并发预处理和并发可中断预处理是什么
  • CMS和G1的异同
  • G1什么时候引发Full Gc
  • G1两个Reign区不是连续的且可能有可达性的引用,怎么回收
  • 为什么HotSpot不使用C++对象来代表Java对象
  • Class对象是在堆还是在方法区?

6. 参考资料

支付宝打赏 微信打赏

请作者喝杯咖啡吧