JVM 是一个虚构出来的计算机,通过在实际计算机上仿真模拟各种计算机功能来实现的。Java 源程序(.java 文件)在经过 Java 编译器编译后成为 Java 字节码文件(.class 文件)。但是 CPU 只认识机器码,不认识字节码,所以 Java 程序要想执行,必须通过 JVM 把字节码转换为机器码。在不同的平台上,字节码是一样的但是虚拟机不同,运行在不同平台上的虚拟机将相同的字节码解释给不同平台上的 CPU,这样,Java 程序就可以做到一次编译、处处运行了。
第 1 章 类加载器
类加载器将字节码文件加载到内存中的方法区,然后在堆中生成这个类的 Class 对象,作为方法区中类数据的访问入口。
1.1 类的生命周期
-
加载:
- 根据全类名获取该类的二进制字节流
- 将二进制字节流转换成方法区中的运行时数据结构
- 在堆中生成一个代表这个类的 java.lang.Class 类对象,作为方法区这个类的各种数据的访问入口
-
验证:保证被加载类的正确性(文件格式验证、元数据验证、字节码验证、符号引用验证)
-
准备:为类变量分配内存,并且设置类变量的默认初始值
- 实例变量此时不会初始化
- 这里不包含用 final 修饰的类变量,因为 final 在编译的时候就分配了,准备阶段会显示初始化
-
解析:把常量池中的符号引用转换为直接引用
-
初始化:执行类构造方法 的过程
- 类的初始化顺序:父类静态变量 -> 父类静态代码块 -> 子类静态变量 -> 子类静态代码块 -> 父类非静态变量 -> 父类构造函数 -> 子类非静态变量 -> 子类构造函数
-
使用:包括主动引用和被动引用,主动引用会引起类的初始化,而被动引用不会引起类的初始化
-
卸载:类需要同时满足下面 3 个条件就进行卸载
- 堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收
- 对应的 Class 类对象没有在任何地方被引用
1.2 类加载器
- 引导类加载器 Bootstrap ClassLoader:由 C / C++ 编写,只加载 Java 的核心类
- 扩展类加载器 Extension ClassLoader:识别 jre/lib/ext 扩展目录,加载目录下的 jar 包
- 系统类加载器 System ClassLoader:加载应用程序中的类
- 自定义类加载器:继承 ClassLoader 类,自定义加载满足一些特殊的需求
1.3 双亲委派机制
双亲委派机制避免类的重复加载,同时防止核心 API 被随意篡改。
- 向上委派:自下而上查看该类是否加载过。在收到类加载请求后,把加载请求向上委派给父类加载器去执行,保证加载类的有序性
- 向下委派:自上而下尝试加载该类。如果父类加载器可以完成类加载,则成功返回;如果不能,就交给子加载器加载,它保证所有类被加载
1.4 沙箱安全机制
沙箱安全机制就是 Java 代码限定在虚拟机 JVM 特定的运行范围中,并且严格限制代码对本地资源的访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
沙箱主要限制系统资源访问,例如 CPU、内存、文件系统、网络。沙箱机制引入了域(Domain)的概念。虚拟机把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互。而应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。
通俗来说就是虚拟机把代码加载到拥有不同权限的域里,然后代码就拥有了该域的所有权限。这样就能控制不同代码拥有不同调用操作系统和本地资源的权限:
第 2 章 JVM 运行时数据区
2.1 JVM 运行时数据区(⭐)
-
线程私有:
- 程序计数器:存储指向下一条指令的地址
- 虚拟机栈:内部栈帧与 Java 调用方法一一对应,每个方法从调用直至完成的过程,都对应着一个栈帧从入栈到出栈的过程。每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧
- 本地方法栈:为虚拟机使用到的 native 方法(C 语言实现)服务
-
线程共享:
- 堆:存储着几乎所有的实例对象,是所有内存区域中最大的
- 元空间(方法区):存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码缓存等数据
2.2 栈(⭐)
栈保存方法的局部变量、部分结果,并参与方法的调用和返回。方法调用结束后自动释放栈空间,不需要 GC 处理。
2.2.1 栈的大小
JVM 允许栈的大小是动态的或者固定不变的:
- 如果栈固定大小(-Xss 参数),不能满足线程请求分配的栈容量,就会抛出 StackOverflowError
- 如果栈可以动态扩展大小,在尝试扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError
- 栈是高地址向低地址扩展的数据结构,是一块连续的内存的区域
2.2.2 栈帧
栈中的数据都是以栈帧的格式存在,每个栈帧中存储着:
- 局部变量表,存储方法的参数和局部变量(基本数据类型、堆中对象的引用地址)
- 操作栈(表达式栈),保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。如果方法有返回值,该返回值会压入当前栈帧的操作栈中
- 动态链接,指向运行时常量池中该栈帧所属方法的引用,将符号引用转换为调用方法的直接引用
- 方法返回地址,存放调用方法的程序计数器的值作为返回地址,即调用方法指令的下一条指令地址
…
2.3 堆(⭐)
一个 JVM 实例中只存在一个堆内存,用来存放对象实例,由程序员手动申请和释放内存空间,如果没有释放则由 GC 处理。
2.3.1 堆内存大小
- -Xms 设置堆内存最小值,-Xmx 设置堆内存最大值,开发中将 -Xmx 和 -Xms 设置一样大避免内存自动扩展
- 堆是用链表来存储内存地址的, 所以是不连续的,遍历方向由低地址向高地址。
2.3.2 常量池
常量池一旦被装入内存就变成运行时常量池,字节码指令文件中的符号引用转变为内存区域代码的直接引用,也就是我们说的动态链接。
-
class 文件常量池:class 常量池可以理解为是 class 文件中的资源仓库,用于存放编译期生成的各种字面量和符号引用。我们一般可以通过 javap -v XXX.class 命令生成更可读的 JVM 字节码指令文件。
-
字符串常量池:
- 为字符串开辟一个字符串常量池
- 创建字符串常量时,首先查询字符串常量池是否存在该字符串。存在该字符串,返回引用实例
- 不存在,实例化该字符串并放入池中
2.3.3 新生代和老年代
新生代默认占堆空间的 1/3,老年代默认占 2/3。新生代里有 3 个分区:Eden、From Survivor、To Survivor,它们的默认占比是 8:1:1。新生代和老年代相关参数:
参数 | 说明 |
---|---|
-XX:NewRatio | 设置新生代与老年代的比值。一般设置为 2,新生代与老年代占比为 1:2 |
-XX:SurvivorRatio | 设置 Eden 区与 Survivor 区的比值。一般设置为 8,Eden 区与两个 Survivor 区的比值为 8:2 |
2.3.4 分代回收算法
- new 的对象先放在 Eden 区
- Eden 区满了,GC 对 Eden 区进行 Minor GC,将不再被引用的对象销毁
- 然后将 Eden 区和 From Survivor 区的剩余对象移动到空的 To Survivor 区
- 清空 Eden 和 From Survivor 区,From Survivor 变成 To Survivor,To Survivor 变成 From Survivor
- 每次在 From Survivor 到 To Survivor 移动时都存活的对象,对象年龄就 +1,当年龄到达 15 时,升级为老年代。大对象也会直接进入老年代。老年代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法
注意:为什么年龄阈值默认是 15 呢?
因为对象头采用 4 个 bit 位来保存 GC 年龄,4 个 bit 位能表示的最大数就是 15。
2.3.5 Minor GC(Young GC)、Major GC、Full GC
-
Minor GC 指新生代 GC ,即发生在新生代的 GC 操作,当新生代无法为新对象分配内存空间的时候,会触发 Minor GC
- 因为新生代中大多数对象的生命周期都很短,所以发生 Minor GC 的频率很高
- Minor GC 会引发 STW,暂停其它用户线程,等待 GC 结束后用户线程恢复
-
Major GC 指老年代 GC,即发生在老年代的 GC 操作
-
Full GC 指整个堆全局范围的 GC,发生情况有:
- 在发生 Minor GC 时,JVM 会检测之前每次晋升到老年代的平均对象大小是否大于老年代的剩余空间,如果大于,则直接进行 Full GC
- 老年代、永久代的空间不足,会触发 Full GC
- System.gc() 会触发 Full GC,但是并不保证 GC 一定会马上执行,具体什么时候执行取决于虚拟机
2.3.6 TLAB(Thread Local Allocation Buffer)
JVM 中对象实例的创建非常频繁,并发时从堆中划分内存空间是线程不安全的,为了避免多个线程操作同一堆空间,在 Eden 区为每个线程分配了一个私有缓存区域:
实际开发中可以通过 -XX:UseTLAB 设置开启 TLAB。
2.3.7 堆空间的常用参数
-XX:+PrintFlagsInitial | 查看所有参数的默认初始值 |
---|---|
-XX:+PrintFlagsFinal | 查看所有参数的最终值 |
-XX:+PrintGCDetails | 查看详细的 GC 处理日志 |
-XX:+PrintGC | 查看简要的 GC 处理信息 |
2.4 逃逸分析
- 当一个对象只在方法内部使用,则认为没有发生逃逸,没有发生逃逸的对象可以分配到栈上
- 当一个对象在方法外部引用,则认为发生了逃逸
2.5 元空间(方法区)
元空间是对 JVM 规范中方法区的实现,元空间与 JDK8 之前的永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
2.5.1 方法区大小
方法区可以固定大小或者可扩展,方法区内存溢出会报 java.lang.OutOfMemoryError:Metaspace。元数据区大小可以使用以下参数设置:
参数 | 说明 |
---|---|
-XX:MetaspaceSize | 元空间初始大小,Windows 下默认是 21M |
-XX:MaxMetaspaceSize | 元空间最大容量,默认是 -1,即没有限制 |
2.5.2 方法区内部结构
- 类型(class、interface、enum、annotation)信息
- 域信息(属性信息)
- 方法信息
第 3 章 对象
3.1 创建对象的过程(⭐)
- 判断对象的类是否加载、链接、初始化
- 在堆空间为对象分配内存
- 处理并发安全问题,CAS、TLAB
- 初始化分配内存空间,所有属性设置默认值
- 设置对象头
- 执行 init 方法,对象按照程序员的意愿进行初始化
3.2 对象的内存布局
- 运行时元数据(Mark Word):存储对象的锁状态标记、GC 分代年龄、哈希码、偏向线程 id、偏向时间戳
- 类型指针:确定对象所属的类
- 实例数据:存放类的数据信息,父类的信息,对象字段属性信息
- 对齐填充:为了字节对齐填充的数据
3.3 四种引用程度(⭐)
- 强引用的对象, JVM 宁愿抛出内存泄漏的异常,也不会进行 GC,
- 软引用的对象,只要内存空间足够,就不会进行 GC;如果内存空间不足了进行 GC
- 弱引用的对象,一旦被 GC 扫描发现,就会回收它的内存
- 虚引用的对象,就和没有任何引用一样,在任何时候都可能被 GC
3.4 对象的访问方式
3.4.1 句柄访问
JVM 在堆内划分出一块内存来存储句柄池,对象引用当中存储的就是句柄地址,句柄池中才会存储对象实例数据和对象类型的数据地址:
3.4.2 直接引用(Hotspot 采用)
对象引用直接指向的就是对象实例数据:
第 4 章 执行引擎
执行引擎将字节码指令编译为对应平台上的本地机器指令。执行引擎既有解释器,又有即时编译器(JIT 编译器)。
4.1 解释器和编译器
- 解释器:对字节码采用逐行解释,翻译为对应平台的本地机器指令。解释器响应速度快,JVM 启动后解释器可以马上发挥作用,而不必等待 JIT 编译器全部编译完成后再执行,省去不必要的编译时间
- JIT 编译器:在程序运行过程中,将字节码文件编译成机器码
- AOT 编译器:在程序运行之前,将字节码文件编译成机器码
4.2 热点代码及探测方式
- 热点代码:一个多次调用的方法,或者是一个方法体内循环次数较多的循环体
- 热点探测方式:基于计数器的热点探测,方法调用计数器用于统计方法的调用次数,回边计数器用于统计循环体执行的次数
4.3 JIT 编译器分类
- C1 编译器:JVM 运行在 Client 模式下,C1 编译器优化简单,耗时短,编译快
- C2 编译器:JVM 运行在 Server 模式下,C2 编译器优化激进,耗时长,但优化的代码执行效率高
第 5 章 GC
5.1 标记阶段
在标记阶段需要进行 GC 对象的标记,即判断一个对象是否存活常用,有以下两种办法:
5.1.1 引用计数
每个对象有一个引用计数属性,新增一个引用时计数 +1,引用释放时计数 -1,计数为 0 时回收。
引用计数实现简单,但是此方法无法解决对象相互循环引用的问题。
5.1.2 可达性分析
从根对象集合 GC Roots 开始向下搜索,当一个对象不是 GC Roots 且未被 GC Roots 引用,则此对象是不可用的。GC Roots 有以下几种:
-
虚拟机栈中引用的对象
-
本地方法栈中引用的对象
-
方法区中静态属性引用的对象
-
方法区中常量引用的对象
-
JVM 内部的引用对象,如基本数据类型对应的 Class 对象,以及系统类加载器
-
所有被同步锁持有的对象
5.2 清除阶段
5.2.1 标记-清除算法(Mark-Sweep)
- 从 GC Roots 开始遍历,在对象的对象头中标记可达的对象
- 从 GC Roots 开始遍历,清除对象头中没有被标记的不可达对象
- 缺点:两次遍历,效率比较低;清除后的内存空间不连续,可能出现很多碎片空间
5.2.2 复制算法
- 将内存平均分成两部分,每次只使用其中的一部分
- 当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空。循环操作
堆中的新生代采用复制算法。复制算法的优点是不会出现内存碎片,缺点是每次运行可使用的内存空间只有原来的一半。
5.2.3 标记-压缩算法(Mark-Compact)
- 从 GC Roots 开始遍历,在对象的对象头中标记可达的对象
- 将所有存活的对象压缩到区域内某处,然后清除剩下的所有不可达对象。
堆中的老年代采用标记-压缩算法。优点是不会产生大量的碎片空间,同时也不会减少可运行的内存。缺点是效率不如复制算法。
5.3 STW(Stop The World)
GC 时用户正常的应用程序会被暂停,这个停顿就叫 STW。可达性分析算法在遍历根节点时会导致 STW,如果分析过程中对象引用关系还在不断变化,则可达性分析结果的准确性无法保证。所以我们需要减少 STW 的发生。
开发中尽量不使用 System.gc(),会导致 STW 的发生。
5.4 GC 性能指标
- 吞吐量:用户程序运行的时间占总运行时间的比例(总运行时间 = 用户程序运行时间 + GC 时间)
- 暂停时间:执行 GC 时,用户的工作线程被暂停的时间
- 内存占用:堆所占的内存大小
5.5 垃圾回收器
新生代:Serial GC、Parallel Scavenge GC、ParNew GC
老年代:Serial Old GC、Parallel Old GC、CMS GC
整堆:G1 GC
5.5.1 Serial GC / Serial Old GC
- Serial 垃圾回收器采用复制算法、串行回收和 STW 机制的方式进行新生代的内存回收
- Serial Old 垃圾回收器采用标记-压缩算法、串行回收和 STW 机制的方式进行老年代的内存回收
5.5.2 ParNew GC
ParNew 垃圾回收器采用复制算法、并行回收和 STW 机制的方式进行新生代的内存回收。
5.5.3 Parallel Scavenge GC
- Parallel Scavenge 垃圾回收器采用复制算法、并行回收和 STW 机制的方式进行新生代的内存回收。它被称为吞吐量优先的垃圾回收器,适用于后台运算而不需要太多交互的任务(批量处理、订单处理)
- Parallel Old 垃圾回收器采用标记-压缩算法、并行回收和 STW 机制的方式进行新生代的内存回收
5.6 CMS GC
CMS(Concurrent-Mark-Sweep)是让垃圾回收线程和用户线程同时工作的并发垃圾回收器。CMS 关注尽可能缩短暂停时间(低延迟),采用标记-清除算法和 STW 机制。
- 初始标记:所有工作线程因 STW 机制暂停,仅仅标记 GC Roots 能直接关联的对象,该阶段耗时很短
- 并发标记:从 GC Roots 的直接关联对象开始遍历整个对象图,耗时较长但是不会有 STW,用户线程与垃圾回收线程并发运行
- 重新标记:在并发标记期间,用户程序运行可能导致对象标记变动,在重新标记阶段对对象标记进行修正
- 并发清除:清理标记已经不可达的对象,可以与用户线程并发执行
CMS 的缺点:会产生内存碎片、占用部分线程导致总吞吐量下降。如果 CMS 运行期间预留内存无法满足,会出现一次 “Concurrent Mode Failure”,就需要临时启动 Serial Old GC 来对老年代进行 GC。
5.7 G1 GC
G1 是一个并行回收器,它将堆内存分成很多不相关的区域 Region,表示 Eden、Survivor 、Old 等。 G1 跟踪每个 Region 里面垃圾回收的价值大小(回收获得的空间大小以及回收所需时间),在后台维护一个邮箱列表,每次根据允许的回收时间,优先回收价值最大的 Region。
G1 的 Region 之间使用复制算法,但从整体上看 G1 使用的是标记-压缩算法,两种算法都能避免内存碎片化。
5.7.1 Region
所有的 Region 大小相同,且在 JVM 生命周期内不会改变。一个 Region 可能是 Eden、Survivor、Old(老年代)、Humongous(大对象)区。
5.7.2 G1 GC 过程
- Young GC:当 Eden 区满了开始 Young GC,G1 GC 暂停所有用户线程(STW),启动多线程并行进行新生代回收,然后将存活对象移动到 Survivor 区或 Old 区
- Concurrent Mark:当堆内存达到 45% 时,老年代开始并发标记过程
- Mixed GC:一次扫描 / 回收一部分老年代,将存活对象移动到空闲区间
5.7.3 记忆集
每个 Region 都对应一个记忆集。在 GC Roots 的枚举范围加入记忆集,就可以保证不进行全局扫描。
第 6 章 字节码文件
class 文件结构包括:
- 4 个字节的魔数 magic:0xCAFEBABE,判断是否是 class 文件
- 2 个字节的副版本号 minor_version 和 2 个字节的主版本号
- 常量池 constant_pool:包含该 class 用到的所有字符串常量、类、接口名等常量
- fields:成员变量
- methods:成员方法
6.1 加载存储指令
加载存储指令用来交换局部变量表和操作数栈中的数据,以及将常量加载到操作数栈。
6.1.1 局部变量从局部变量表中加载到操作数栈
n 为局部变量表中的栈帧的序号,注意 double 和 long 占用两个栈帧。
参数 | 说明 |
---|---|
iload_n | int 类型局部变量加载到操作数栈 |
aload_n | 引用类型局部变量加载到操作数栈 |
6.1.2 操作数栈的栈顶元素存储到局部变量表
参数 | 说明 |
---|---|
istore_n | int 类型栈顶元素存储到局部变量表的索引 n 处 |
astore_n | 引用类型栈顶元素存储到局部变量表的索引 n 处 |
6.1.3 常量加载到操作数栈栈顶
参数 | 说明 |
---|---|
iconst_0、iconst_1、iconst_2、iconst_3、iconst_4、iconst_5 | int 型常量 0 ~ 5 加载到操作数栈 |
iconst_m1 | int 型常量 -1 加载到操作数栈 |
bipush | int 型常量 -128 ~ 127 加载到操作数栈 |
ldc | int 型常量 -2147483648 ~ 2147483647 加载到操作数栈 |
6.2 运算指令
由于没有直接支持 byte、short、char 和 boolean 类型的算术指令,使用操作 int 类型的指令代替。
参数 | 说明 |
---|---|
iadd / isub / imul / idiv | 弹出栈顶的两个值进行加减乘除 |
irem | 求余 |
ineg | 取反 |
iinc | 自增 |
6.3 类型转换指令
参数 | 说明 |
---|---|
i2b / i2s / i2c | int -> byte、short、char |
l2i | long -> int |
d2l | double -> long |
6.4 对象的创建与存取指令
参数 | 说明 |
---|---|
new | 创建对象 |
newarray / anewarray / multianewarray | 创建基本数据类型的数组 / 创建引用数据类型的数组 / 创建多维数组 |
getfield / putfield | 获取实例对象的属性的值 / 设置实例对象的属性的值 |
getstatic / putstatic | 获取类的静态属性的值 / 设置类的静态属性的值 |
6.5 方法调用和返回指令
参数 | 说明 |
---|---|
invokevirtual | 调用对象的实例方法 |
invokeinterface | 调用接口方法 |
invokespecial | 调用需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法 |
invokestatic | 调用类方法 |
invokedynamic | 在运行时动态解析出调用点限定符所引用的方法,并执行该方法 |
return | 方法没有返回类型(void)、实例初始化方法、接口的类初始化方法 |
ireturn | 方法返回 int 类型 |
6.6 操作数栈管理指令
参数 | 说明 |
---|---|
pop | 操作数栈的栈顶元素出栈 |
pop2 | 操作数栈的栈顶 2 个 slot 的元素出栈 |
dup | 复制操作数栈栈顶的元素,再将其压入栈,即对栈顶的内容做了个备份 |
dup2 | 复制操作数栈栈顶 2 个 slot 的元素,再将其压入栈 |
dup_x1 | 复制操作数栈栈顶的元素,再将其插入到栈顶 1+1 = 2 个 slot 下 |
dup_x2 | 复制操作数栈栈顶的元素,再将其插入到栈顶 1+2 = 3 个 slot 下 |
dup2_x1 | 复制操作数栈栈顶 2 个 slot 的元素,再将其插入到栈顶 2+1 = 3 个 slot 下 |
dup2_x2 | 复制操作数栈栈顶 2 个 slot 的元素,再将其插入到栈顶 2+2 = 4 个 slot 下 |
6.7 控制转移指令
条件分支 | 说明 |
---|---|
ifeq / ifne | 如果操作数栈顶 = 0 / ≠ 0 则成功 |
iflt / ifle | 如果操作数栈顶 < 0 / ≤ 0 则成功 |
ifgt / ifge | 如果操作数栈顶 > 0 / ≥ 0 则成功 |
ifnull / ifnonnull | 判断操作数栈顶的引用是否为空 |
if_icmpeq / if_icmpne | 如果操作数栈顶两个值 v1 = v2 / v1 ≠ v2 则成功 |
if_icmplt / if_icmpgt | 如果操作数栈顶两个值 v1 < v2 / v1 > v2 则成功 |
if_icmple / if_icmpge | 如果操作数栈顶两个值 v1 ≤ v2 / v1 ≥ v2 则成功 |
if_acmpeq / if_acmpne | 如果栈顶两个引用 v1 = v2 / v1 ≠ v2 则成功 |
无条件分支 | 说明 |
---|---|
goto | 无条件跳转到指定行 |
6.8 异常处理指令
显式抛出异常的操作(throw语句)都由 athrow 指令来实现。try-catch-finally 使用异常表
第 7 章 JVM 性能监控、分析、调优
7.1 性能监控指标
- 平均响应时间:提交请求和返回响应之间使用的时间
- 吞吐量:单位时间内完成的请求量
- 并发数:同一时刻对服务器有实际交互的请求数
- 内存占用:堆区所占内存大小
7.2 内存泄漏的几种情况
7.2.1 静态集合类 / 单例模式
集合 HashMap、LinkedList 等用 static 修饰,那么它们的生命周期与 JVM 一致,容器中的对象在程序结束之前都不能被释放,造成内存泄漏。
单例模式由于静态属性,生命周期也与 JVM 一致,同样会造成内部泄露。
public class OOMTest {
static List list = new ArrayList<>();
public void oomTest(){
Object obj = new Object();
list.add(obj);
}
}
7.2.2 内部类持有外部类
外部类的实例对象方法返回了内部类的实例对象,由于内部类对象被长期引用,导致外部类对象不会被 GC,造成内存泄露。
7.2.3 数据库连接、网络连接、IO 连接(⭐)
没有对这些连接进行显示关闭,就会造成大量对象无法回收,引起内存泄漏。
7.2.4 变量不合理的作用域
一个变量定义的作用范围大于其实际的使用范围,造成内存泄漏。
7.2.5 对象的哈希值改变
对象修改后的哈希值与最初存储进集合的哈希值不同,无法检索到该对象进行删除,造成内存泄漏。
7.2.6 缓存泄漏
对象引用放入缓存中容易被遗忘,项目上线时几百万条数据就会造成内存泄漏。
7.2.7 监听器和回调
在客户端实现的 API 中注册回调,却没有显式取消,就会积聚造成内存泄漏。
7.3 Java 命令行监控工具
7.3.1 jps
获取正在运行的 Java 进程的 pid:
jps
7.3.2 jinfo
jinfo 打印和修改运行中 Java 进程的系统变量和命令行参数:
jinfo -flags <pid> # 查看全部参数
jinfo -sysprops <pid> # 查看系统属性
7.3.3 jstat
jstat 查看 Java 进程的类加载、JIT 编译、内存、GC 信息:
jstat [-命令选项] <pid> [间隔时间(毫秒)] [查询次数]
# 输出类加载信息
jstat -class <pid>
# Loaded 加载 class 的数量;Bytes 所占用空间大小
# Unloaded 未加载数量;Bytes 未加载占用空间
# 输出编译统计信息
jstat -compiler <pid>
# Compiled 编译数量;Failed 失败数量
# Invalid 不可用数量;Time 时间
# FailedType 失败类型;FailedMethod 失败的方法
jstat -gc <pid> # 输出堆内存和 GC 信息
# S0C/S1C 两个幸存区的大小
# S0U/S1U 两个幸存区的使用大小
# EC Eden区的大小;EU Eden区的使用大小
# OC 老年代的大小;OU 老年代的使用大小
# MC 方法区的大小;MU 方法区的使用大小
# CCSC 压缩类的大小;CCSU 压缩类的使用大小
# YGC 新生代垃圾回收次数;YGCT 新生代垃圾回收消耗时间
# FGC 老年代垃圾回收次数;FGCT 老年代垃圾回收消耗时间
# GCT 垃圾回收消耗总时间
7.3.4 jstack
如果线程长时间停顿(线程死锁、等待资源、等待锁),jstack 用于查看 Java 进程的线程信息,包括线程状态、堆栈信息:
jstack <pid>
7.3.5 jmap
jmap 常用于分析内存泄漏问题,打印堆内存 / 共享对象内存信息、dump 堆:
# 生成 JVM 的堆转储快照文件(dump 文件)
jmap -dump:format=b,file=<file-name.hprof> <pid>
# 查看 JVM 堆信息
jmap -heap <pid>
7.3.6 jcmd(多功能命令行工具)
jcmd 可以用来实现以上除了 jstat 以外所有命令的功能。
# 查看所有 JVM 进程
jcmd -l
# 查看指定进程可以执行的 command
jcmd <pid> help
# 生成进程的某个信息到 hprof 文件中
jcmd <pid> [command] <filename.hprof>
7.4 Java 图形化监控工具(⭐)
7.4.1 shallow size 和 retained size
- shallow size:对象本身占用内存大小,不包含引用的对象
- retained size:对象的 shallow size 加上对象直接或间接引用的对象的 shallow size(当前对象被 GC 后,堆上能释放的内存大小)
7.4.2 MAT
下载地址:https://www.eclipse.org/mat/downloads.php。
- Histogram:列出对象实例数目和占用内存
- Dominator Tree:列出内存中存活的大对象列表
- Top Consumers:展示占用内存较多的对象分布
- Duplicate Classes:检测一个类是否被多个类加载器加载了
- Leak Suspects:报告一些可能存在内存泄漏的可疑点
- Top Components:列出大于 1% 堆内存对象报告
7.4.3 JProfile
下载地址:https://www.ej-technologies.com/download/jprofiler/files。
编写好代码程序,点击下方图标即可:
7.4.4 Arthas
前面的工具虽然非常好用,但是都需要在服务器项目进程中配置相关监控参数,再远程连接到项目进程获取相关数据。这样会带来一些不便,比如线上网络是隔离的,本地的监控工具根本连不上线上环境。
Arthas 不需要远程连接,也不需要配置监控参数,采用命令行交互模式,可以在线排查问题,动态跟踪 Java 代码,实时监控 JVM。
Linux 下载:
wget https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar <pid>
Arthas 的指令:
指令 | 说明 |
---|---|
dashboard | 当前系统的实时数据面板 |
thread | 当前 JVM 的线程堆栈信息 |
jvm | 当前 JVM 信息 |
sysprop | 查看和修改当前 JVM 属性 |
sysenv | 查看 JVM 环境变量 |
heapdump | 生成 dump 文件 |
sc | 查看 JVM 已加载的类信息 |
sm | 查看已加载的类方法信息 |
sysenv | 查看 JVM 环境变量 |
heapdump | 生成 dump 文件 |
sc | 查看 JVM 已加载的类信息 |
7.4.5 JMC
JMC 对应用性能的影响非常小,非常适合做压测。
7.4.6 Flame Graphs 火焰图
7.5 进程挂起排查
7.5.1 死锁
7.5.2 CPU 过高
- top 命令查看 CPU 占用最高的进程 id:
top -H
2. 使用 jstack 命令转储该进程的线程信息,找到 CPU 占用高的线程 pid:
jstack <进程 pid> > <filename.txt>
3. 将线程 pid 转换为 16进制:
printf '%x\n' <线程 pid>
- 在 dump 线程信息文件中查找该线程 id 对应的线程信息:
cat <filename.txt> | grep -A 10 <16 进制线程 id>
- 我们可以在该线程信息中定位到代码所在的类和行数,找到问题代码所在
7.5.3 内存泄漏排查
线上 GC 频率报警,GC 回收的内存越来越少,重启仍不能解决,此时我们需要进行内存泄漏排查。
- jps 命令获取正在运行 Java 进程 pid
- jmap 命令转储服务内存信息:
jmap -dump:format=b,file=<filename.hprof> <pid>
- 使用 MAT 工具分析 dump 文件,找到占据内存最大的对象实例
- 查看代码分析内存泄漏的原因
第 8 章 JVM 参数
8.1 标准参数
参数 | 说明 |
---|---|
-client | 客户端模式,使用 C1 编译器 |
-server | 服务器模式,使用 C2 编译器,编译耗时长但是性能好 |
8.2 -X 参数
cmd 中输入 java -X 可以查看所有 -X 参数:
参数 | 说明 |
---|---|
-Xms | 设置初始 Java 堆大小 |
-Xmx | 设置最大 Java 堆大小 |
-Xss | 设置 Java 线程堆栈大小 |
8.3 -XX 参数
参数 | 说明 |
---|---|
-XX:+ | 启动该属性 |
-XX:- | 禁用该属性 |
-XX:= | 设置属性值 |
-XX:+PrintFlagsFinal | 输出所有参数的名称和默认值 |
8.3.1 OOM 相关
参数 | 说明 |
---|---|
-XX:+HeapDumpOnOutofMemoryErr | 内存出现 OOM 时生成 Heap 转储文件 |
-XX:+HeapDumpBeforeFullGC | 在 FullGC 之前生成 Heap 转储文件 |
-XX:HeapDumpPath=
| 设置 Heap 转储文件的存储路径 |
-XX:OnOutOfMemoryError | 内存出现 OOM 时执行脚本 |
8.3.2 GC 回收器相关
ParallelGC:
参数 | 说明 |
---|---|
-XX:+PrintCommandLineFlags | 查看设置参数及使用的垃圾回收器 |
-XX:+UseParallelGC | 手动指定新生代使用 Parallel 垃圾回收器 |
-XX:ParallelGCThreads | 设置 Parallel GC 的并行线程数,最好等于 CPU 数 |
-XX:MaxGCPauseMillis | 设置 GC 的 STW 时间,单位毫秒(谨慎使用) |
-XX:GCTimeRatio | 设置 GC 时间占总时间比例 1 /(N+1),默认 99 表示 GC 时间不超过 1% (衡量吞吐量的参数) |
-XX:MaxGCPauseMillis | 设置 GC 的 STW 时间,单位毫秒 |
CMS:
参数 | 说明 |
---|---|
-XX:+UseConMarkSweepGC | 手动指定使用 CSM 垃圾回收器 |
-XX:CMSInitiatingOccupanyFraction | 设置内存使用率阈值,达到就开始 GC |
-XX:+UseCMSCompactAtFullCollection | 开启内存碎片整理 |
-XX:CMSFullGCsBeforeCompaction | 设置在多次 Full GC 后开启内存碎片整理 |
-XX:ParallelCMSThreads | 设置 CMS 并发线程数 |
G1:
参数 | 说明 |
---|---|
-XX:+UseG1GC | 手动指定使用 G1 垃圾回收器 |
-XX:G1HeapRegionSize | 设置 region 大小,必须是 2 的幂次,大小在 1M-32M 之间,默认是堆内存的 1/2000 |
-XX:MaxGCPauseMills | 期望最大 GC 停顿时间,默认 200 ms |
-XX:ParallelGCThread | 设置 STW 时 GC 并发线程数量,最多 8 个 |
-XX:ConcGCThreads | 设置并发标记的线程数,为上一个参数的 1/4 左右 |
-XX:InitiatingHeapOccupanyPercent | 设置内存使用率阈值百分比,达到就开始 GC,默认 45 |
-XX:G1NewSizePercent | 设置新生代占堆内存最小百分比,默认 5% |
-XX:G1MaxNewSizePercent | 设置新生代占堆内存最大百分比,默认 60% |
-XX:G1ReservePercent=10 | 保留内存区域,防止 to 幸存区溢出 |
8.3.3 GC 日志相关
参数 | 说明 |
---|---|
-XX:+PrintGC | 输出简化 GC 日志信息 |
-XX:+PrintGCDetails | 输出详细 GC 日志信息 |
-XX:+PrintGCTimeStamps | 输出 GC 发生时的时间戳 |
-XX:+PrintGCDateStamps | 以日期形式输出 GC 发生时的时间戳 |
-XX:+PrintHeapAtGC | 每一次 GC 前后都打印堆信息 |
-Xloggc: | 把 GC 日志写到文件中 |