Concept
- JVM是一份本地化的程序,本质上是可执行的文件,是静态的概念。程序运行起来成为进程,是动态的概念。
- java程序是跑在JVM上的,严格来讲,是跑在JVM实例上的,一个JVM实例其实就是JVM跑起来的进程,二者合起来称之为一个JAVA进程。
- 各个JVM实例之间是相互隔离的。
- 一般一个进程是一个java程序的实例。
JVM 内存分布
参考链接:
- https://www.cnblogs.com/myjavaboke/p/16424307.html
- https://blog.csdn.net/qq_27093465/article/details/79802884
- https://blog.csdn.net/u010859650/article/details/123819149
JVM暂停程序运行
当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。
为什么要设置两个Survivor区
设置两个Survivor区最大的好处就是解决了碎片化,下面我们来分析一下。
为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
复制算法的优化
内存划为1个Eden区,2个Survivor区,其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说Eden区有800MB内存,每一块Survivor区就100MB内存。
Survivor区,一块叫From,一块叫To,对象存在Eden和From块。当进行GC时,Eden存活的对象全移到To块,而From中,存活的对象按年龄值确定去向,当达到一定值的对象会移到年老代中,没有达到值的复制到To区,经过GC后,Eden和From被清空。之后,From和To交换角色,新的From即为原来的To块,新的To块即为原来的From块,且新的To块中对象年龄加1。
注:年龄阈值可通过-XX:MaxTenuringThreshold可设置
GC
以下图是 JDK7 以下。
Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Permanent Generation。
若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时jvm gc停止所有在堆中运行的线程并执行清除动作。
分代回收
JVM采用一种分代回收 (generational collection) 的策略,用较高的频率对年轻的对象(young generation)进行扫描和回收,这种叫做minor collection,而对老对象(old generation)的检查回收频率要低很多,称为major collection。这样就不需要每次GC都将内存中所有对象都检查
Young Gen
年轻代用来存放新近创建的对象,尺寸随堆大小的增大和减小而相应的变化,默认值是保持为堆大小的1/15,可以通过 -Xmn 参数设置年轻代为固定大小,也可以通过 -XX:NewRatio 来设置年轻代与年老代的大小比例,年青代的特点是对象更新速度快,在短时间内产生大量的“死亡对象”。
当一个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。 当对象在 Eden 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳,则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域中,然后清理所使用过的 Eden 以及 Survivor 区域,并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。 但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
Survivor
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。
Old Gen
老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。 另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
Permanent Generation
PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space中,它和存放类实例(Instance)的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的应用中有很多CLASS的话,就很可能出现PermGen space错误,这种错误常见在web服务器对JSP进行pre compile的时候。如果你的WEB APP下都用了大量的第三方jar, 其大小超过了jvm默认的大小(4M)那么就会产生此错误信息了。
在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间。
永久代或者“Perm Gen”包含了JVM需要的应用元数据,这些元数据描述了在应用里使用的类和方法。注意,永久代不是Java堆内存的一部分。永久代存放JVM运行时使用的类。永久代同样包含了Java SE库的类和方法。永久代的对象在full GC时进行垃圾收集。
对于Java8,HotSpots取消了永久代
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。
Metaspace
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。默认情况下,元空间的大小仅受 本地内存 限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize ,初始空间大小:达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间:默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集;
-XX:MaxMetaspaceFreeRatio ,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;
GC Trigger
Minor GC(新生代GC)的触发条件
当Eden区满时,触发Minor GC。
Full GC(老年代GC)的触发条件
- 直接调用System.gc
- 老年代空间不足(新生代存活下来的对象转入、大对象的创建等引起)
- 老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发Full GC。
- Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FGC。
对象什么时候被回收
- 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用失效,计数器就减1,当计数器为0的对象就是可以被回收的对象了。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
- 可达性分析法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
简单来说就是现在有一个地图在你手上,地图上有几个入口,你分别从这些入口进入并且沿着这路全部走一遍,把所有你能找到的房子标记一下,剩下的那些找不到的房子就是全部都要被拆除。
ClassLoader
当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的 class 文件当中,所以经常要从这个 class 文件中要调用另外一个 class 文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个 class 文件到内存当中的,从而只有 class 文件被载入到了内存之后,才能被其它 class 所引用。所以 ClassLoader 就是用来动态加载 class 文件到内存当中用的。
thread dump
thread state
Java线程的生命周期中,存在几种状态。在Thread类里有一个枚举类型State,定义了线程的几种状态,分别有:
- NEW: 线程创建之后,但是还没有启动(not yet started)。这时候它的状态就是NEW
- RUNNABLE: 正在Java虚拟机下跑任务的线程的状态。在RUNNABLE状态下的线程可能会处于等待状态, 因为它正在等待一些系统资源的释放,比如IO
- BLOCKED: 阻塞状态,等待锁的释放,比如线程A进入了一个synchronized方法,线程B也想进入这个方法,但是这个方法的锁已经被线程A获取了,这个时候线程B就处于BLOCKED状态
- WAITING: 等待状态,处于等待状态的线程是由于执行了3个方法中的任意方法。 1. Object的wait方法,并且没有使用timeout参数; 2. Thread的join方法,没有使用timeout参数 3. LockSupport的park方法。 处于waiting状态的线程会等待另外一个线程处理特殊的行为。 再举个例子,如果一个线程调用了一个对象的wait方法,那么这个线程就会处于waiting状态直到另外一个线程调用这个对象的notify或者notifyAll方法后才会解除这个状态
- TIMED_WAITING: 有等待时间的等待状态,比如调用了以下几个方法中的任意方法,并且指定了等待时间,线程就会处于这个状态。 1. Thread.sleep方法 2. Object的wait方法,带有时间 3. Thread.join方法,带有时间 4. LockSupport的parkNanos方法,带有时间 5. LockSupport的parkUntil方法,带有时间
- TERMINATED: 线程中止的状态,这个线程已经完整地执行了它的任务
jinfo
使用jinfo命令查看Java进程的启动参数,例如:
jinfo <PID>
是前面找到的Java进程的PID。运行该命令后,会列出Java进程的启动参数,包括命令行选项、系统属性和环境变量等。
jvm profiling
MemoryAnalyzer 使用
MAC 系统上安装 MemoryAnalyzer
到官网下载 MemoryAnalyzer-1.15.0.20231206-macosx.cocoa.aarch64.dmg
双击后,在弹出窗口中,拖拽 MemoryAnalyzer 进入 Applications 中。
修改 /Applications/MemoryAnalyzer.app/Contents/Info.plist
在 array 中增加 vm 这一项
<array>
<string>-vm</string><string>/Users/{user}/Dev/jdk-17.0.6.jdk/Contents/Home/bin</string>
<string>-keyring</string>
<string>~/.eclipse_keyring</string>
</array>
调大 MemoryAnalyzer 堆内存
不调整内存,在遇到较大 dump文件时,会报以下错误
An internal error occurred during: "Parsing heap dump from '/Users/xiangxshen/Dev/dumpDir/NameNode.hprof'".
java.lang.OutOfMemoryError
调整的配置文件如下:
/Applications/MemoryAnalyzer.app/Contents/Eclipse/MemoryAnalyzer.ini
将 -Xmx1024m 调整成 -Xmx8192m
具体使用和解读
先 dump 内存堆
jmap -F -dump:format=b,file=/data/eventlog/NameNode.hprof 886336
# 生成堆转储,但只包括活动对象。活动对象是指当前仍然可达、不会被垃圾回收器回收的对象。
jmap -F -dump:live,format=b,file=/data/eventlog/NameNode.hprof 886336
live 参数, 会先执行一次FullGC来清除可以被回收的部分。
参考链接
- https://blog.csdn.net/u010002184/article/details/114181699
- https://www.cnblogs.com/trust-freedom/p/6744948.html
-
dominator_tree中默认内存占用最大的对象排在第一位
-
Shallow Size (对象自身占用的内存大小)
对象自身占用的内存大小,不包括它引用的对象。
针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。当然这里面还会包括一些java语言特性的数据存储单元。
针对数组类型的对象,它的大小是数组元素对象的大小总和。
- Retained Size (被GC后Heap上释放的内存大小)
Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C, C就是间接引用)。
Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。
释放的时候还要排除被GC Roots直接或间接引用的对象。他们暂时不会被被当做Garbage。
- 右击对象可以List Objects列出次对象引用信息
with outgoing references,列出此对象引用了哪些对象
with incoming references,列出此对象被那些对象引用
- OverView大概信息
Size 内存大小
Classes : class对象
Objects : Objects个数,也就是实例个数
Unreachable Objects Histogram :可被回收的对象,扔在内存当中
Remainder Retained Size: 剩余的部分,意思是其他的,没有一一例举的部分。
- Histogram视图
以下方式可以打开Histogram柱状图:
(1)点击Overview页面Actions区域内的“Histogram视图”链接
(2)点击工具栏的“histogram按钮”
Histogram视图:
该视图以Class类的维度展示每个Class类的实例存在的个数、 占用的 [Shallow内存] 和 [Retained内存] 大小,可以分别排序显示。
从Histogram视图可以看出,哪个Class类的对象实例数量比较多,以及占用的内存比较大,Shallow Heap与Retained Heap的区别会在后面的概念介绍中说明。
不过,多数情况下,在Histogram视图看到实例对象数量比较多的类都是一些基础类型,如char[](因为其构成了String)、String、byte[],所以仅从这些是无法判断出具体导致内存泄露的类或者方法的,可以使用 List objects 或 Merge Shortest Paths to GC roots 等功能继续钻取数据。如果Histogram视图展示的数量多的实例对象不是基础类型,是有嫌疑的某个类,如项目代码中的bean类型,那么就要重点关注了。
JVM 命令
查看 jvm 的 默认的垃圾回收器 GC
$ java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=2147483648 -XX:MaxHeapSize=32210157568 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
openjdk version "1.8.0_372"
OpenJDK Runtime Environment (Tencent Kona 8.0.14) (build 1.8.0_372-b1)
OpenJDK 64-Bit Server VM (Tencent Kona 8.0.14) (build 25.372-b1, mixed mode, sharing)
jmap -heap
$ jmap -heap 468837
Attaching to process ID 468837, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.372-b1
using thread-local object allocation.
Parallel GC with 2 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 28991029248 (27648.0MB)
NewSize = 1073741824 (1024.0MB)
MaxNewSize = 14495514624 (13824.0MB)
OldSize = 1073741824 (1024.0MB)
NewRatio = 1
SurvivorRatio = 1
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 358612992 (342.0MB)
used = 192026152 (183.13040924072266MB)
free = 166586840 (158.86959075927734MB)
53.54690328676101% used
From Space:
capacity = 357564416 (341.0MB)
used = 0 (0.0MB)
free = 357564416 (341.0MB)
0.0% used
To Space:
capacity = 357564416 (341.0MB)
used = 0 (0.0MB)
free = 357564416 (341.0MB)
0.0% used
PS Old Generation
capacity = 1086324736 (1036.0MB)
used = 23225424 (22.149490356445312MB)
free = 1063099312 (1013.8505096435547MB)
2.1379816946375785% used
13818 interned Strings occupying 1366888 bytes.
jstat -gcutil
$ jstat -gcutil 468837 1000 10
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 54.98 2.14 96.45 94.72 2 0.029 2 0.079 0.108
0.00 0.00 54.98 2.14 96.45 94.72 2 0.029 2 0.079 0.108
0.00 0.00 54.98 2.14 96.45 94.72 2 0.029 2 0.079 0.108
0.00 0.00 54.98 2.14 96.45 94.72 2 0.029 2 0.079 0.108
0.00 0.00 54.98 2.14 96.45 94.72 2 0.029 2 0.079 0.108
0.00 0.00 54.98 2.14 96.45 94.72 2 0.029 2 0.079 0.108
0.00 0.00 54.98 2.14 96.45 94.72 2 0.029 2 0.079 0.108
0.00 0.00 54.98 2.14 96.45 94.72 2 0.029 2 0.079 0.108
0.00 0.00 54.98 2.14 96.45 94.72 2 0.029 2 0.079 0.108
0.00 0.00 54.98 2.14 96.45 94.72 2 0.029 2 0.079 0.108