在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的垃圾回收部分
我们会分为以下几部分进行介绍:
本小节将会介绍如何判断垃圾回收对象
首先我们先来介绍引用计数法的定义:
但该方法存在一个致命问题:
同样我们先来简单介绍可达性分析算法:
然后我们来简单介绍一下Root对象的分类(来自MAT工具统计):
可达性分析算法就是目前Java虚拟机所使用的垃圾回收器判定方法
下面我们将会介绍JVM中常用的五种引用方法,他们分别对应着不同的回收对象判定情况:
我们下面来一一介绍
上述图片中的A1对象就是强引用示例
我们下面介绍强引用的概念:
然后我们介绍强引用的回收概念:
上述图片中的A2对象就是软引用示例
我们下面介绍软引用的概念:
然后我们介绍软引用的回收概念:
此外我们的软引用对象也是会占用内存的,所以我们也需要采用其他方法将软引用对象回收:
我们首先给出软引用对象的相关测试代码:
package cn.itcast.jvm.t2;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_3 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
// 这部分是强引用对象,我们会发现所有内存都放在内部,导致内存不足
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
// 调用下列方法(软引用)
soft();
}
// 软引用
public static void soft() {
// 软引用逻辑:list --> SoftReference --> byte[]
// 创建软引用
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
// 首先new一个SoftReference并赋值
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
// 将SoftReference加入list
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
/*
调试过程:
如果我们采用强引用方法,正常情况下会在第五次循环时报错
但是如果我们采用软引用,我们会在第五次循环时发生gc清理,这时我们前四次的添加(list的前四位)就会被软引用清除
所以我们在最后循环结束后查看数组会发现:
null
null
null
null
[B@330bedb4
*/
我们再给出软引用对象回收的相关测试代码:
package cn.itcast.jvm.t2;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示软引用, 配合引用队列
*/
public class Demo2_4 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// 这里设置了List,里面的SoftReference是软引用对象,再在里面添加的数据就是软引用对象所引用的A2对象
List<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[]> reference : list) {
System.out.println(reference.get());
}
}
}
/*
和之前那此调试相同,前四次正常运行,在第五次时进行了gc清理
但是在循环结束之后,我们将软引用对象放入到了引用队列中并进行了清理,所以这时我们的list中前四次软引用对象直接消失
我们只能看到list中只有一个对象:
[B@330bedb4
*/
上述图片中的A3对象就是弱引用示例
我们下面介绍强弱引用的概念:
然后我们介绍弱引用的回收概念:
此外我们的弱引用对象也是会占用内存的,所以我们也需要采用相同方法将弱引用对象回收:
我们同样给出弱引用对象的垃圾回收示例代码:
package cn.itcast.jvm.t2;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示弱引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_5 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
}
/*
这时我们的小型gc(新生代gc)是不会触发弱引用全部删除的(新生代我们后面会讲到)
只有当内存全部占满后,触发的Full gc才会导致弱引用的必定回收
例如我们在第5,7次新生代发生内存占满,这时触发了新生代的gc,但是只会删除部分WeakReference
当我们第9次新生代,老生代内存全部占满后会发生一次Full gc,这时就会引起全部弱引用数据删除,所以我们的数据会变成:
null
null
null
null
null
null
null
null
null
[B@330bedb4
*/
上述图片中的ByteBuffer对象就是虚引用示例
我们下面介绍虚引用的概念:
然后我们介绍虚引用的回收概念:
我们需要注意的是:
上述图片的A4对象就是终结器引用
我们下面介绍终结器引用的概念:
然后我们介绍终结器引用的回收概念:
我们需要注意的是:
本小节将会介绍垃圾回收的三种基本回收算法
我们首先给出简单图示:
我们来做简单解释:
该算法的优缺点:
我们首先给出简单图示:
我们来做简单解释:
该算法的优缺点:
我们首先给出简单图示:
我们来做简单解释:
该算法的优缺点:
本小节将会介绍垃圾回收的常用机制
我们前面已经介绍了三种垃圾回收算法,但实际上我们的垃圾回收采用的是三种方法的组合方法:
我们首先对大概念进行介绍:
然后我们对小概念进行介绍:
然后我们对整个回收机制进行介绍:
我们下面介绍一下分代垃圾回收机制的相关参数:
含义 | 参数 |
---|---|
堆初始大小 | -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 (小gc) | -XX:+ScavengeBeforeFullGC |
我们通过一个简单的实例来展示分代垃圾回收的实际演示:
// 相关配置信息:配置默认大小,设置回收方法,显示GC详情,开启FullGC前进行gc
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
/*
首先我们展示不添加内存的状况
*/
package cn.itcast.jvm.t2;
import java.util.ArrayList;
public class Demo2_1 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
}).start();
System.out.println("sleep....");
Thread.sleep(1000L);
}
}
/*
其中def new generation,eden space是新生代,tenured generation是老年代,from,to幸存区
Heap
def new generation total 9216K, used 4510K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 55% used [0x00000000fec00000, 0x00000000ff067aa0, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 4362K, capacity 4714K, committed 4992K, reserved 1056768K
class space used 480K, capacity 533K, committed 640K, reserved 1048576K
*/
/*
然后我们展示添加1mb的情况
*/
package cn.itcast.jvm.t2;
import java.util.ArrayList;
public class Demo2_1 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_1MB]);
}).start();
System.out.println("sleep....");
Thread.sleep(1000L);
}
}
/*
我们可以发现新生代数据增加,老年代未发生变化
Heap
def new generation total 9216K, used 5534K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 67% used [0x00000000fec00000, 0x00000000ff167a40, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 4354K, capacity 4714K, committed 4992K, reserved 1056768K
class space used 480K, capacity 533K, committed 640K, reserved 1048576
*/
/*
最后需要补充讲解一点:当我们的新生代不足以装载数据内存时,我们会直接将其装入老年代(老年代能够装载情况下)
*/
package cn.itcast.jvm.t2;
import java.util.ArrayList;
public class Demo2_1 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep....");
Thread.sleep(1000L);
}
}
/*
我们会发现eden的值未发生变化,但是tenured generation里面装载了8192K
Heap
def new generation total 9216K, used 4510K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 55% used [0x00000000fec00000, 0x00000000ff067a30, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 4360K, capacity 4714K, committed 4992K, reserved 1056768K
class space used 480K, capacity 533K, committed 640K, reserved 1048576K
*/
/*
当然,当我们的新生代和老年代都不足以装载时,系统报错~
*/
package cn.itcast.jvm.t2;
import java.util.ArrayList;
public class Demo2_1 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep....");
Thread.sleep(1000L);
}
}
/*
我们首先会看到他在Full gc之前做了一次小gc,然后做了一次Full gc,可是这并无法解决问题
[GC (Allocation Failure) [DefNew: 4345K->999K(9216K), 0.0016573 secs][Tenured: 8192K->9189K(10240K), 0.0022899 secs] 12537K->9189K(19456K), [Metaspace: 4352K->4352K(1056768K)], 0.0039931 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 9189K->9124K(10240K), 0.0018331 secs] 9189K->9124K(19456K), [Metaspace: 4352K->4352K(1056768K)], 0.0018528 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
然后系统进行报错
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at cn.itcast.jvm.t2.Demo2_1.lambda$main$0(Demo2_1.java:20)
at cn.itcast.jvm.t2.Demo2_1$$Lambda$1/1023892928.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)
最后我们可以看到老年代占用了89%,第一个数据仍旧保存,但第二个数据无法保存导致报错
Heap
def new generation total 9216K, used 366K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 4% used [0x00000000fec00000, 0x00000000fec5baa8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 9124K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 89% used [0x00000000ff600000, 0x00000000ffee93c0, 0x00000000ffee9400, 0x0000000100000000)
Metaspace used 4379K, capacity 4704K, committed 4992K, reserved 1056768K
class space used 480K, capacity 528K, committed 640K, reserved 1048576K
我们还需要注意的是:即使内存不足发生报错,但该程序不会结束;系统只会释放自己当前项目的进程而不会影响其他进程
*/
前面我们已经介绍了垃圾回收机制,现在我们来介绍常用的垃圾回收器
我们在正式讲解垃圾回收器之前,我们先来回顾一个概念STW:
我们首先来介绍串行垃圾回收器的特点:
我们给出串行垃圾回收器的展示图:
我们所需配置:
// 设置 新生代回收方法复制 老年代回收方法为标记整理法
-XX:+UseSerialGC = Serial + SerialOld
我们来简单解释一下:
我们首先来介绍吞吐量优先垃圾回收器的特点:
我们给出吞吐量优先垃圾回收器的展示图:
我们所需配置:
// 设置垃圾回收器方法
XX:+UseParallelGC ~ -XX:+UseParallelOldGC
// 自适应新生代晋升老年代的阈值处理
-XX:+UseAdaptiveSizePolicy
// 设置垃圾回收时间占总时间的比例(与-XX:MaxGCPauseMillis=ms冲突)
-XX:GCTimeRatio=ratio
// 设置最大STW时间(与-XX:GCTimeRatio=ratio冲突)
-XX:MaxGCPauseMillis=ms
// 设置最大同时进行CPU个数
-XX:ParallelGCThreads=n
我们来简单解释一下:
我们首先来介绍响应时间优先垃圾回收器的特点:
我们给出响应时间优先垃圾回收器的展示图:
我们所需配置:
// +UseConcMarkSweepGC:设置并发标记清除算法,允许用户进程单独进行,但部分时间还需要阻塞
// -XX:+UseParNewGC:设置新生代算法,
// SerialOld:当老年代并发失败,采用单线程方法
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
// -XX:ParallelGCThreads=n:并行数设置为n
// -XX:ConcGCThreads=threads:并发线程最好设置为CPU的1/4个数,相当于只有1/4个CPU在处理垃圾回收
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
// 预留空间(因为并发清理时其他进程可能会产生一些垃圾,这些垃圾目前无法处理,我们需要预留一定空间进行储存)
-XX:CMSInitiatingOccupancyFraction=percent
// 我们在重新标记阶段前,先对新生代进行垃圾回收,节省其标记量
-XX:+CMSScavengeBeforeRemark
我们来简单解释一下:
下面我们将会针对jdk1.9默认垃圾回收器做一个详细的介绍
首先我们先来简单介绍一下G1垃圾回收器:
下面我们来介绍G1垃圾回收器的特点:
相关JVM参数:
我们通过一张图来简单介绍G1垃圾回收器的过程:
我们可以看到整个流程分为三个阶段:
我们首先给出该阶段的展示图:
我们对其进行解释:
我们首先给出该阶段的展示图:
我们对其进行解释:
我们给出并发标记阈值控制语句:
// 阈值控制
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
我们首先给出该阶段的展示图:
我们对其进行解释:
我们需要注意一点:
我们需要重新总结一下Full GC操作:
我们在前面已经提到了我们将堆划分为多个Region
但其实这个Region并不仅仅只分为了E,S,O三个空间,此外还包括以下空间:
/*
每一个Region都会划出一部分内存用来储存记录其他Region对当前持有Rset Region中Card的引用
针对G1的垃圾回收时间设置较短,在进行标记过程中可能会导致时间过长,所以我们设置了RSet来储存部分信息
我们可以直接通过扫描每块Region里面的RSet来分析垃圾比例最高的Region区,放入CSet中,进行回收。
*/
/*
收集集合代表每次GC暂停时回收的一系列目标分区。
在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。
年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。
*/
由于我们的初次标记时会去寻找Root部分
但其实大部分的Root都放入了老年代,但老年底数据较多难以查找,所以G1提供了一种方法:
我们给出简单图示:
同时如果该Root地址发生变化,G1给出了另外的方法进行更换:
我们在进行标记时通常采用三色标记法:
我们做简单介绍:
我们针对上述进行分析:
这时我们就会发现一个问题:
所以我们设计了Remark重新标记操作:
下面我们将会针对G1垃圾回收器在各个版本的重要更新做个介绍
我们首先要明白字符串在底层是采用char数组形成的:
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
如果重复的字符串都存放在内存中肯定会导致内存多余占用,所以提供了解决方案:
其优缺点:
当所有的类都经过并发标记后,就会直到哪些类不再被使用
这时如果一个类加载器的所有类都不再使用时,我们就可以卸载它所加载的所有类
首先我们介绍一下巨型对象的定义:
本小节将会介绍垃圾回收的调优机制
我们进行调优需要掌握的基本知识:
我们调优的领域并非只有垃圾回收,但是这个部分的调优确实会给项目带来很大的速率优化,此外还有其他方法的调优:
此外我们需要确定调优的目标:
首先我们需要明白GC是花费时间的,如果我们能够控制好内存保证不发生GC,那么才是最快的
如果我们频繁发生GC操作,那么我们就需要先进行自我反思:
/*
例如我们是否设置了相同元素筛选?错误账号禁止缓存?
*/
/*
例如我们调取数据时是否只调取了我们所需数据还是全盘托出?
例如我们选择数据类型时是否是以最低标准为要求,数据库能采用tiny不要使用int
*/
/*
例如我们是否设置缓存数据时采用了Static形式的Map并不断存储数据?
*/
首先我们先来回顾一下新生代的优点:
那么我们该怎么进行调优呢:
那么官方认可的新生代大小为多少:
首先我们需要直到新生代幸存区存放的数据主要分为两部分:
首先我们需要保证具有一定的幸存区大小:
其次我们需要控制晋升标准:
最后我们介绍一下老年代调优,我们这里以CMS为例:
最后我们介绍三个调优方法的案例:
/*
主要是因为新生代空间不足
因为新生代空间不足,经常发生minor GC,同时幸存区空间不足导致大量数据直接进入到老年代,最后导致老年代也产生Full GC
*/
/*
首先我们已经直到是CMS的垃圾回收方法
我们在之前的学习中得知Full GC主要分为三个阶段:初始标记,并发标记,重新标记
在请求高峰期期间,数据较多,我们的重新标记由于需要重新扫描所有数据空间,所以会导致单次暂停时间长
我们只需要保证在进行重新扫描前先进行一次Minor GC消除掉无用数据就可以加快暂停速度:-XX:+CMSScavengeBeforeRemark
*/
/*
首先我们需要注意是jdk1.7版本
在1.7版本是由永久代负责管理方法区以及常量池,如果永久代内存满了也会产生Full GC
所以我们只需要增加永久代的内存大小即可
*/
到这里我们JVM的垃圾回收篇就结束了,希望能为你带来帮助~
该文章属于学习内容,具体参考B站黑马程序员满老师的JVM完整教程
这里附上视频链接:01_垃圾回收概述_哔哩哔哩_bilibili