本文是《深入理解Java虚拟机(周志明)》这本书的重点摘要。
本笔记仅作为复习,不过多的对内容进行讲解。
本笔记按照书的目录进行,如遇到需要细看的,可以到书中找对应内容。
本笔记并不是按照书中原话进行摘要,而是根据自己的理解使用大白话进行记录,同时进行了少部分扩展。如有错误欢迎指出。
由于内容较多,一共分为三篇:
篇幅 | 链接 |
---|---|
深入理解Java虚拟机(周志明)(1)第一部分 走进Java(2)第二部分 自动内存管理机制 | https://blog.csdn.net/zhaohongfei_358/article/details/134927759 |
深入理解Java虚拟机(周志明)(3)第三部分 虚拟机执行子系统(4)第四部分 程序编译与代码优化 | https://blog.csdn.net/zhaohongfei_358/article/details/135067398 |
深入理解Java虚拟机(周志明)(5)第五部分 高效并发 | https://blog.csdn.net/zhaohongfei_358/article/details/135111650 |
无重点
Java按运行平台可分为四种:
无重点
无重点
无重点
无重点
学习Java虚拟机的重要原因:当出现内存泄漏时,知道怎么排查。
数据区有两种:
如上图,JVM内存被分为了如下区域:
OutOfMeoryError
(OOM)。(第3章详解)当JVM遇到一条new
指令时,会进行如下过程:
一个对象在内存中有三块区域,分别是:
我们使用的对象变量仅存储该对象的引用(reference),因此在实际访问对象时需要根据reference去堆内存中查找。
查找方式有两种(不同的JVM采用方式不同):
除了程序计数器,其他区域都可能会出现OOM。
以下是不同内存区域OOM的原因:
-Xmx
与-Xms
参数)StackOverflowError
异常。一般出现在“异常的递归”中,正常方法间调用的深度不至于溢出。-Xss
参数增大栈内存容量。List<String>
,然后你一直往里扔“不同的”String,就会出现常量池溢出。sun.misc.Unsafe.allocateMemory(Native Method)
)学习垃圾收集(Garbage Collection, GC)的原因:可以帮助我们避免和解决OOM问题、由JVM引起的性能瓶颈等
思路:记录每个对象当前被多少对象引用。若为0,说明没有被引用,就可以被回收了。
优点:简单、高效
缺点:无法解决循环引用问题。(例如:A、B两个垃圾互相引用,导致无法被回收)
思路:从“GC Roots”对象出发,若无法访问到某个对象,说明这个对象可以被回收了。
以下变量都会作为GC Roots:
Java中的引用可以按强弱程度分为四种,JVM对不同程度的引用回收策略不同:
强引用(Strong Reference):我们平时用的都是强引用。例如:MyObject myObj = new MyObject();
软引用(Soft Reference):使用SoftReference
显式声明。
MyObject myObject = new MyObject("Amy"); // 从数据库中获取数据
SoftReference<MyObject> reference = new SoftReference<>(myObject); // 增添软引用
// do something ...
myObject = reference.get(); // 尝试获取myObject对象
if (myObject == null) {
// 没获取到,说明已经被JVM回收了
myObject = new MyObject("Amy"); // 重新从数据库中获取数据
} else {
// 没有被JVM回收
}
弱引用(Weak Reference):使用WeakReference
显式声明。
SoftReference
一样,把SoftReference
改成WeakReference
即可。虚引用(Phantom Reference):也称为“幽灵引用”、“幻影引用”等。
ReferenceQueue
)进行回收前的一些操作reference.get()
方法一定会返回null
(源码就是直接return null
,这也是为什么叫虚引用的原因。注:一个对象可以同时存在多种引用。例如:
MyObject myObject = new MyObject("Amy"); // 此时myObject存在强引用
SoftReference<MyObject> reference = new SoftReference<>(myObject); // myObject同时存在强引用和软引用
myObject = null; // 去掉myObject的强引用,其只剩下软引用
引用队列:在定义软/弱/虚引用时,可以传个引用队列(虚引用必须传),这样对象在被回收之前会进入引用队列,可以显式的对其进行一些操作。(引用队列只能获取到“引用对象”即XxxReference
,获取不到原对象,因为可能已经被JVM释放了)。
对着四种引用更详细的讲解可参考:Java中的强引用、软引用、弱引用、虚引用与引用队列 通俗举例实战详解
对象被释放会经历两次标记:
finalize
或JVM已经调用过finalize
。注意:这里只是调用过,执行成没成功甚至是否执行完成都不管当两次标记结束后,就会正式释放对象。
若用户重写了finalize,则对象会在第一次标记后经历如下过程:
F-Queue
队列Finalizer
线程去调用finalize
方法finalize
是否执行完,都释放对象。若用户在
finalize
方法中重新给对象增加了引用(不推荐这么做),那么这个对象就不会被释放了。
由于JVM并不保证finalize是否执行完,因此不推荐使用finalize
方法。如果要释放资源,try-with和虚引用都比finalize
更好。
方法区通常有两种东西要回收:
不同的虚拟机会采用不同的垃圾收集算法。也可能会同时使用不同的收集算法。
(图片来源:JVM 内存结构)
JVM根据对象的存活时长,将堆区域分为了新生代(Young)和老年代(Old Generation),而新生代又被分为了Eden区和两个Suvivor区:
思路:分两步:① 先对垃圾对象进行标记; ② 对标记的对象进行清理
缺点:会产生大量内存碎片
思路:将内存分成两个区域。一次只使用一个区域,当该区域满时,直接将该区域存活的对象复制到另一个区域,然后使用另一个区域,因此这个区域就可以直接清空了。
优点:没有内存碎片
缺点:内存可用区域减少。
思路:与标记清除类似。分三步:① 标记;② 将存活对象往一端挪,避免内存碎片。③ 清理另一端内存。
缺点:慢
优点:无内存碎片
思路:将内存分为新生代和老年代。
为了保证GC过程中引用不能发生变化,因此在枚举根节点时,所有的Java线程都会暂停,称为Stop The World
。
在开始枚举根节点前,必须要保证所有的线程都处在一个安全点(safepoint)上,以便可以快速准确完成GC。若线程不在安全点上,那就需要等它执行到安全点。
没有最好的垃圾收集器,只有最适合自己的。
古老的收集器,现在不用了。
特点:单线程
在Serial收集器的基础上增加了多线程。
在ParNew收集器的基础上,增加了对吞吐量的关注。即:一定时间内,让JVM更多的运行Java代码。
主要方式就是:自适应条件新生代老年代内存大小、GC频次等。(也可以手动配)
因此,Parallel Scavenge收集器适用于计算型任务。
无重点
无重点
CMS(Concurrent Mark Sweep)目标:致力于回收停顿时间最短。常用于服务端,
CMS采用“标记-清理”算法,共分为4步:
CMS收集器的缺点:
GI收集器在CMS收集器的基础上进一步进化。
GI收集器面向服务端应用。JDK1.9作为默认收集器。
GI收集器在上述收集器的基础上,实现了“可指定最大停顿时间”。官方称为全能收集器。
GI收集器基本思路:
① GI将堆分为了许多同等大小的Region,每个小格子就是一个Region,每个Region取值范围为1~32MB。
② 这样Eden区、Survivor区、Old区都是逻辑连续,实际物理不连续。
③ 各个区的region数量不固定,运行过程中可以灵活调节。
④ Humongous区用于存放大对象。若一个对象的大小超过了Region大小的50%,就认为是大对象。
GI的三种垃圾回收模式:
GC日志样例:
33.125:[GC[DefNew: 3324K- >152K(3712K),0.0025925 secs] 3324K- > 152K( 11904K), 0. 0031680 secs]
100.667:[Full GC[Tenured: 0 K- > 210K( 10240K), 0. 0149142secs] 4603K- > 210K( 19456K),[Perm: 2999K->2999K( 21248K)], 0. 0150007 secs][Times: user= 0.01 sys= 0.00, real=0.02 secs]
含义如下:
33.125
/ 100.667
:GC发生的时间。该数字为JVM启动后经历的秒数。[GC
/ [Full GC
:GC的类型。Full GC会Stop-The-World[DefNew
/ Tenured
/ Perm
:GC发生的区域(不同的虚拟机名字会有差异)3324K->152K(3712K)
:GC前该内存区域已使用容量
-> GC后该内存区域已使用容量
(该内存区域总容量
)0.0025925 secs
:GC所使用的时间[Times: user=0.01 sys=0.00, real=0.02secs]
:user=用户态耗时、sys=内核态耗时、real总耗时(包括从准备开始GC到真正开始GC消耗的时间,这部分也会STW,见5.2.7节)。SurvivorRatio
:调整Eden区和Survivor区的比值。默认为8:1:1。例如:我们经常要产生临时大对象时,可以将PretenureSizeThreshold
:直接晋级到老年代对象的大小。CMSInitiatingOccupancyFraction
:设置CMS收集器GC时给用户留多少%的空间对象优先在Eden区分配
GC分两种:
大对象“可能”会直接进入老年代,三种情况:
长期存活的对象将进入老年代:对象每躲过一次Minor GC,年龄就会+1,当年龄到15岁时就会进入老年代。
无重点
JDK在bin
目录下提供了各种用于诊断程序的命令行工具:
jps
:查看当前机器都运行了哪些Java程序。
样例:
> jps
181584 Launcher
180296 KotlinCompileDaemon
182216 RemoteMavenServer
176556 MySpringBootApplication
185036 Jps
前面的数字是该程序的虚拟机唯一ID(Local Virtual Machine Identifier, LVMID),后面排查问题需要用到。
后面的是Java程序的main
方法类名。
jstat
是一个监视各种状态的通用命令。
使用方式为:jstat -[工具] [vmid]
vmid就是上面提到的LVMID,使用jps命令查看。
使用样例:
监视GC情况:
> jstat -gc 176556
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
47616.0 42496.0 0.0 38949.0 968704.0 505022.9 165376.0 112599.7 92032.0 86841.6 11904.0 10893.4 15 0.255 3 0.185 0.440
监视类“装/卸载”情况:
> jstat -class 176556
Loaded Bytes Unloaded Bytes Time
16799 31461.8 7 7.1 18.95
jstat主要工具:
jinfo
:用于实时查看和调整虚拟机各项参数
使用方式:
jinfo [vmid]
jinfo -flag +[配置项]
jinfo -flag -[配置项]
jinfo -flag [配置项]=[修改后配置]
。注意:大部分配置是不可以动态修改的。若不能(或者是拼写错误),都会报flag 'XXX' cannot be changed
错误。使用样例:
查看虚拟机各项参数:
> jinfo 176556
Attaching to process ID 176556, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.281-b09
Java System Properties:
jboss.modules.system.pkgs = com.intellij.rt
.... # 这里省略若干参数
VM Flags:
Non-default VM flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote .... # 省略若干参数
运行时增加打印GC详细信息:
> jinfo -flag +PrintGCDetails 176556
# 执行完这条命令后,再观察程序,就会发现发生GC时就会在控制台打日志了
运行时取消打印GC详细信息:
> jinfo -flag -PrintGCDetails 176556
# 执行完后,发现程序又不打印日志了
运行时修改Dump文件路径:
> jinfo -flag HeapDumpPath=D:/ 176556
jmap
:用于查看/导出Java内存使用情况
使用方式:jmap [option] <vmid>
jmap工具的主要选项:
使用样例:
导出Dump文件到指定位置:
> jmap -dump:file=D:\test.dump 176556
Dumping heap to D:\test.dump ...
Heap dump file created
jhat
(Java Heap Analysis Tool):用于分析dump文件。
由于功能简陋,目前已经不怎么使用了。一般用VisualVM或其他更专业的工具
jstack
:生成虚拟机当前时刻的线程快照。
使用方式:jstack <vmid>
使用样例:
> jstack 176556
# ... 生成了许多堆栈信息
XXX.class
字节码只是描述了程序在虚拟机中应该怎么执行,但使用不同的虚拟机运行过程还不太一样。如果我们想知道真正怎么执行,可以用JIT命令生成汇编文件,然后看虚拟机是怎么执行的。
JConsole是将上述的各种命令行工具的结果可视化出来了。
JConsole
使用方式:打开bin
目录下的JConsole.exe
,选择你要查看的程序即可。
JConsole会展示如下内容:
概述:
内存使用情况:
线程使用情况:
类加载情况:
虚拟机状况与参数:
MBean属性与执行MBean操作:
MBean:Java中可以将对象注册成MBean,这样外部程序就可以通过JMX查看、修改该对象的属性,也可以执行该对象的方法。例如:上面图片中我们可以利用JConsole执行SpringApplication对象的shutdown方法
VisualVM是官方强大的运行监视和故障处理程序(上面能干的,它基本上都能干)。其支持插件,因此有无限可能。
启动方式:执行jdk的bin
目录下的jvisualvm.exe
文件。进入后,再左侧选择你的Java程序。
VisualVM的插件安装:选择工具
->插件
,然后在可用插件出选择要安装的插件即可。
常用功能举例:
Visual GC:查看GC情况,清晰的看到堆的每个区域的使用情况
监视:查看CPU、堆、类、线程的基本情况
线程:查看线程的运行情况
其他常用:
无重点
异常场景:网站15万PV/天 (PV=Page View,可以理解为请求量)。每隔十几分钟,网站就卡十几秒。
机器情况:4个CPU,16GB内存,Java堆固定12GB
经过排查后:
解决方法:
异常场景:程序是集群部署,隔一段时间就会内存溢出(OOM)。
排查后发现:
因此,我们在开发或排查问题时,也要考虑会不会存在大量对象释放不掉导致OOM的问题。
异常场景:系统经常产生OOM,但堆内存的各个区域都很稳定,并且有发现内存不足现象。
排查后发现:
System.gc()
,尝试让JVM去回收一下堆外内存。解决方法:合理的调节JVM内存大小,给系统预留足够的内存。
异常场景:系统CPU占用过高,但排查发现并不是Java程序占用高。
排查后发现:
fork
系统调用。该调用是创建进程用的。Runtime.getRuntime.exec()
方法执行shell脚本,且非常高频。因此导致高频的创建进程。解决方案:降低这段代码频率,或采用其他的替代方案。总之,不要高频创建进程。
Runtime.getRuntime.exec()
的执行逻辑:
异常场景:Java程序总是异常崩溃(不是OOM,是直接崩掉了)
排查后发现:
解决方案:远程调用增加超时时间,避免线程阻塞。或采用其他异步方案,例如MQ。
异常场景:由于业务需要每10分钟加载一个80M的文件进行分析,导致分析期间Minor GC过于频繁,且时间较长。
原因:分析文件期间,Eden区很快就被占满,但由于这些对象要用一段时间,导致还清理不掉,因此会出现频繁Minor GC。同时,分析文件时使用的是HashMap<Long, Long>
,因为key, value都是Long,空间利用率过低(HashMap每个节点还有有其他数据去维护数据结构)。
解决方案:① 调整JVM参数,让这些数据直接进入老年代(不推荐)。② 优化分析代码,使用空间利用率高的数据结构。
异常情况:一个简单的GUI程序,平时GC都很快,一最小化,GC就会耗时很久。
根本原因:在windows程序被最小化时,该程序就改用虚拟内存了。
解决方案:启动时增加-Dsun.awt.keepWorkingSetOnMinimize=true
参数。
无重点