JVM学习笔记(三)垃圾回收
标签: JVM学习笔记(三)垃圾回收
2023-07-15 18:23:54 103浏览
相关文章:
- JVM中的新生代和老年代(Eden空间、两个Survior空间)_jvm eden_样young的博客-CSDN博客
- JAVA命令行工具(一)--JAVA - 简书
- JAVA命令行工具(二)-jps - 简书
- JAVA命令行工具(三)-jstat - 简书
- 记CPU占用过高的排查 - 简书
- jstat命令详解_zhaozheng7758的博客-CSDN博客
- 深入浅出JVM调优,看完你就懂_Lemon-的博客-CSDN博客
- JVM性能调优_jvm调优_滴哩哩哩滴哩哩哩哒哒的博客-CSDN博客
- 性能调优-------(三)1分钟带你入门JVM性能调优_小诚信驿站的博客-CSDN博客
- JVM 面试点: 新生代的内存大小参数设置_jvm设置新生代大小_困知勉行1985的博客-CSDN博客
- JVM中FGC和YGC分析_jvm fgc_水月清辉的博客-CSDN博客
- Java GC 变量含义(S0 S1 E O P YGC YGCT FGC FGCT GCT)_fgc ygc_厚积_薄发的博客-CSDN博客
- jvm优化技巧,Java堆,old区,Eden区,s0和s1区,老年代,新生代_jvm s0 s1_互联网全栈开发实战的博客-CSDN博客
- jvm优化技巧,Java堆,old区,Eden区,s0和s1区,老年代,新生代_jvm s0 s1_互联网全栈开发实战的博客-CSDN博客
- JVM参数调优-设置堆、新生代、老年代、持久代大小_jvm调整老年代,新生代的大小_坚持是一种修行的博客-CSDN博客
- JVM 新生代和老年代介绍_jvm新生代老年代_Codetots的博客-CSDN博客
- 34
- 34
- 34
虚拟机栈:存储基本数据类型、引用对象的变量、局部变量表等,这是线程私有的,每个线上线程的大小默认为1Mb。
程序计数器:存储字节指令的地地址,如果是本地方法栈,则存储undefined。
本地方法栈:由于java是表层语言,无法直接访问硬件,需要调用第三方语言,比如C、C++来操作硬件,比如创建内核线程,操作文件等。
方法区:存储jvm编译后地字节码文件,静态变量,常量,类信息等。
堆:
- 这是一块很重要的存储区域,也是我们性能调优重要依据,其用来存储java对象,gc回收地也是这一块数据。其分为老年代和新生代,默认数据大小为2 :1。
- 新生代又分为Eden区,s0区,s1区,默认数据大小为8 : 1 : 1。
- 新创建一个对象,首先判断能否放到Eden区,如果Eden区满了,会触发mirrorGc「所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程,但这段时间可以忽略不计」。此时Eden区和s0区中存活的对象移至s1区,并标志对象的分代年龄,eden区和s0区清空,如果此时对象还无法放置eden区,则直接放置老年代。反之亦然。
- 分代年龄存储到java对象头中。如果old区满了,会触发fullGc,我们尽量避免fullGc「fullGc暂停所有正在执行的线程(Stop The World),来回收内存空间,这个时间需要考虑地」。
因而,我们所说的性能调优,那么就是堆中的性能调优。
笔记参考文章:
JVM 学习笔记(二)垃圾回收_CodeAli的博客-CSDN博客
一、如何判断对象可以回收
1. 引用计数法
当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。这个引用计数法听起来不错,但是有一个弊端,如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。(java虚拟机垃圾回收没有采用它)
2. 可达性分析算法
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
具体做法是:扫描堆中的对象,看是否能够沿着 GC Root对象为起点的引用链找到该对象,找不到,表示可以回收。
2.1 哪些对象可以作为 GC Root ?
堆分析工具:MAT,eclipse官网下载地址:Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation
2.2 案例
public static void main(String[] args) throws IOException {
ArrayList<Object> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add(1);
System.out.println(1);
System.in.read();
list = null;
System.out.println(2);
System.in.read();
System.out.println("end");
}
第一步:运行上面的代码后,找到.class文件的位置,选中->右键->Open In->Terminal,然后使用命令jps查看当前系统中有哪些 java 进程;
第二步:执行如下命令,生成转储文件1.bin,然后在Run窗口回车让程序继续执行下面的代码,最后再回到Terminal窗口执行命令 jmap -dump:format=b,live,file=2.bin 10396 这时便生成了两个转储文件;
jmap -dump:format=b,live,file=1.bin 10396
参数解释:
- 参数"-dump:":表示不是查看堆内存的使用情况,而是要把当前堆内存的状态转储到文件;
- format=b:表示指定转储文件的格式为二进制格式;
- live:抓快照时只关心存活的对象,过滤掉那些被垃圾回收掉的;且这个参数表示抓快照前会进行一次垃圾回收,因为进行垃圾回收后才知道哪些是存活对象;
- file=1.bin:指定转储文件的文件名;(当前目录下)
- 10396:进程id
第三步:用刚刚下载的堆内存分析工具MAT打开上面生成的文件进行分析;对比两个文件发现,ArrayList对象被回收掉了。
延伸:区分引用变量和对象
MAT工具使用遇到的问题
双击运行程序时,提示下面的错误。此时,需要在官网下载一个JDK11的压缩包,解压后放在磁盘中某个位置,如D:\JavaTools\jdk-11.0.19;然后打开MAT安装目录下的MemoryAnalyzer.ini文件,添加下方两行代码,表示指定以JDK11方式启动。这样,再次双击运行MAT工具时,便可正常启动。
-vm
D:/JavaTools/jdk-11.0.19/bin/javaw.exe
MAT工具使用的文章
3. 四种引用
强引用
- 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
软引用(SoftReference)
- 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象;
- 可以配合引用队列来释放软引用自身。
弱引用(WeakReference)
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象;
- 可以配合引用队列来释放弱引用自身。
虚引用(PhantomReference)
- 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
终结器引用(FinalReference)
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象。
3.1 演示软引用
/**
* 演示 软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Code_08_SoftReferenceTest {
public static int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
method2();
}
// 设置 -Xmx20m , 演示堆内存不足,
public static void method1() throws IOException {
ArrayList<byte[]> list = new ArrayList<>();
for(int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
}
// 演示 软引用
public static void method2() throws IOException {
ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
for(int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for(SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
method1 方法解析:
首先会设置一个堆内存的大小为 20m,然后运行 mehtod1 方法,会抛异常,堆内存不足,因为 mehtod1 中的 list 都是强引用。
method2 方法解析:
在 list 集合中存放了 软引用对象,当内存不足时,会触发 full gc,将软引用的对象回收。细节如图:
上面的代码中,当软引用引用的对象被回收了,但是软引用还存在,所以,一般软引用需要搭配一个引用队列一起使用。
修改 method2 如下:
// 演示 软引用 搭配引用队列
public static void method3() throws IOException {
ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for(int i = 0; i < 5; i++) {
// 关联了引用队列,当软引用所关联的 byte[] 被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while(poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("=====================");
for(SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
3.2 弱引用演示
public class Code_09_WeakReferenceTest {
public static void main(String[] args) {
// method1();
method2();
}
public static int _4MB = 4 * 1024 *1024;
// 演示 弱引用
public static void method1() {
List<WeakReference<byte[]>> list = new ArrayList<>();
for(int i = 0; i < 10; i++) {
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB]);
list.add(weakReference);
for(WeakReference<byte[]> wake : list) {
System.out.print(wake.get() + ",");
}
System.out.println();
}
}
// 演示 弱引用搭配 引用队列
public static void method2() {
List<WeakReference<byte[]>> list = new ArrayList<>();
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for(int i = 0; i < 9; i++) {
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB], queue);
list.add(weakReference);
for(WeakReference<byte[]> wake : list) {
System.out.print(wake.get() + ",");
}
System.out.println();
}
System.out.println("===========================================");
Reference<? extends byte[]> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
for(WeakReference<byte[]> wake : list) {
System.out.print(wake.get() + ",");
}
}
}
首先学习老师视频52~56,若想进一步学习java的四种引用,可参考以下资料:
二、垃圾回收算法
1. 标记清除
定义: Mark Sweep
特点:①速度较快;②会造成内存碎片
2. 标记整理
定义:Mark Compact
特点:①速度慢;②没有内存碎片
3. 复制
定义:Copy
特点:①不会有内存碎片;②需要占用双倍内存空间
三、分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
- minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
3.1 相关 VM 参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前MinorGC | -XX:+ScavengeBeforeFullGC |
更多参数的理解,请移步: JAVA命令行工具(一)--JAVA垃圾回收选项 - 简书
3.2 GC分析
(1)main中任何代码时
问题一:上图中,新生代总大小total为什么只有9M多?
答:JVM参数新生代大小参数【-Xmn10M】明明划分了10M,可是为什么只有9M多呢?因为幸存区比例参数【-XX:SurvivorRatio=ratio】添加后,8M会分给伊甸园,幸存区from和幸存区to各占1M。这里就认为幸存区to那一兆的空间要始终空着,是不能用的,所以计算空间时,那1M就没有计算在内。
扩展:-XX:SurvivorRatio=ratio参数详解
设置Survivor和Eden的相对大小,该选项作用于新生代内部。为了明白该选项的含义,需要了解新生代的空间划分,一个简单的示意如下:
| Eden | From(S0) | To(S1) | // Trom和To都是Survivor且大小相同
该选项表示Eden区和两个Surivior区的比率,默认值为8,即各区的比例为Eden:S0:S1=8:1:1。如果Eden区过大,在一次GC后幸存的对象过多以至于Survivor区不能容纳,那么大部分对象在一次GC后就会转移到老年代;如果Eden区过小,意味着新对象的产生将很快耗尽Eden区,从而增加新生代GC次数。
问题二:上面代码中只有main方法,内部什么都没有,为什么eden伊甸园空间已经使用了28%?
因为哪怕是再简单的一个java程序运行时,都要加载一些类,创建一些对象;这些对象刚开始也是使用的伊甸园区域。
(2)main中创建list集合并分配7M
注意:如何区分老年代GC还是新生代GC呢?
- 上图中如果是GC开头时,表示新生代GC;如果为full GC时,表示老年代GC;
四、垃圾回收器
五、垃圾回收调优
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论