垃圾回收相关零散知识
1、System.gc() 的理解
1.1、System.gc() 方法
此方法只是通知JVM要进行一次GC,但是JVM什么时候执行GC就不知道了;
代码示例:手动执行 GC 操作
/**
* @author shkstart shkstart@126.com
* @create 2020 14:49
*/
public class SystemGCTest {
public static void main(String[] args) {
new SystemGCTest();
/*
提醒jvm的垃圾回收器执行gc,但是不确定是否马上执行gc
与Runtime.getRuntime().gc();的作用一样。
*/
System.gc();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("SystemGCTest 重写了finalize()");
}
}
执行结果:
第一次执行:
"C:\Program Files\Java\jdk1.8.0_144\bin\java"
SystemGCTest 重写了finalize()
第二次执行:
Process finished with exit code 0
"C:\Program Files\Java\jdk1.8.0_144\bin\java"
从执行结果可以看到,有时候会调用 finalize() 方法,有时候没有调用,程序就结束了;也就是说JVM真正GC的时间是不可控的,有可能程序执行完了,JVM才进行GC
System.runFinalization() 强制调用失去引用的对象的finalize()
/**
* @author shkstart shkstart@126.com
* @create 2020 14:49
*/
public class SystemGCTest {
public static void main(String[] args) {
new SystemGCTest();
// 强制调用使用引用的对象的finalize()方法
System.runFinalization();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("SystemGCTest 重写了finalize()");
}
}
"C:\Program Files\Java\jdk1.8.0_144\bin\java"
SystemGCTest 重写了finalize()
Process finished with exit code 0
1.2、手动GC理解不可达对象的回收行为
public class LocalVarGC {
/**
* 触发Minor GC没有回收对象,由于是个大对象,然后在触发Full GC将该对象存入old区
*/
public void localvarGC1(){
byte[] buffer = new byte[10*1024*1024];//10MB
System.gc();
}
/**
* 触发YoungGC的时候,已经被回收了
*/
public void localvarGC2(){
byte[] buffer = new byte[10*1024*1024];//10MB
buffer = null;
System.gc();
}
/**
* 不会被回收,因为localvarGC3方法并没有结束!buffer 还存放在局部变量表索引为1的槽中,引用还在
*/
public void localvarGC3(){
{
byte[] buffer = new byte[10*1024*1024];//10MB
}
System.gc();
}
/**
* 会被回收,因为它还存放在局部变量表索引为1的槽中,但是后面定义的value把这个槽给替换了
*/
public void localvarGC4(){
{
byte[] buffer = new byte[10*1024*1024];//10MB
}
int value = 10;
System.gc();
}
/**
* localvarGC5中的数组已经被回收
*/
public void localvarGC5(){
localvarGC1();
System.gc();
}
public static void main(String[] args){
LocalVarGC local = new LocalVarGC();
local.localvarGC5();
}
}
2. 内存溢出与内存泄漏
2.1 内存溢出
内存溢出原因:
2.2 内存泄漏
https://www.bilibili.com/video/BV1PJ411n7xZ?p=158&spm_id_from=pageDriver
广义上来说,只要一些变量没用了,本该走完其生命周期的,但是延长了其生命周期;比如: 将一些 成员变量定义成了类变量,从而延长了其生命周期,这样的话也算内存泄漏
内存泄露图示
内存泄漏举例
- 单例模式
单例的生命周期和应用程序是一样长的,所以在单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。- 一些提供close()的资源未关闭导致内存泄漏
数据库连接 dataSourse.getConnection(),网络连接socket和io连接必须手动close,否则是不能被回收的。
3. Stop the World
https://www.bilibili.com/video/BV1PJ411n7xZ?p=159
关于 Stop the World 的理解
STW: GC过程中,发生的用户线程的停顿;
原因: 可达性分析算法中,在枚举GC Roots的时候必须保障在一个一致性快照中进行,如果枚举过程中对象的引用关系还在不断地变化,会影响枚举结果的准确性;
STW的特点
代码感受 Stop the World
/**
* @author shkstart shkstart@126.com
* @create 2020 15:50
*/
public class StopTheWorldDemo {
public static class WorkThread extends Thread {
List<byte[]> list = new ArrayList<byte[]>();
public void run() {
try {
while (true) {
for(int i = 0;i < 1000;i++){
byte[] buffer = new byte[1024];
list.add(buffer);
}
if(list.size() > 10000){
list.clear();//当数组满了清空数组,失去引用
System.gc();//会触发full gc,进而会出现STW事件
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static class PrintThread extends Thread {
public final long startTime = System.currentTimeMillis();
public void run() {
try {
while (true) {
// 每秒打印时间信息
long t = System.currentTimeMillis() - startTime;
System.out.println(t / 1000 + "." + t % 1000);
Thread.sleep(1000);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void main(String[] args) {
WorkThread w = new WorkThread();
PrintThread p = new PrintThread();
//w.start();
p.start();
}
}
关闭工作线程 w ,观察打印输出:当前时间间隔与上次时间间隔最多相差 0.1s
0.0
1.0
2.1
3.1
4.2
5.2
6.3
7.4
8.4
开启工作线程 w ,观察打印输出:当前时间间隔与上次时间间隔最多相差 0.4s ,可以明显感受到 Stop the World 的存在
0.1
1.2
2.4
3.5
4.6
5.6
6.10
7.11
8.14
9.15
10.15
4、垃圾回收的并行与并发
https://www.bilibili.com/video/BV1PJ411n7xZ?p=160&spm_id_from=pageDriver
4.1、并发的概念
4.2、并行的概念
4.3、垃圾回收的并行与串行
https://www.bilibili.com/video/BV1PJ411n7xZ?p=161
垃圾收集器的并行和串行
垃圾回收器的并发
5、安全点与安全区域
https://www.bilibili.com/video/BV1PJ411n7xZ?p=162&spm_id_from=pageDriver
5.1 安全点
安全点定义
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint)”。
安全点的选择
Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。 大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
-
抢先式中断:(目前没有虚拟机采用了)首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
-
主动式中断: 设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。(有轮询的机制)
5.2 安全区域
安全区域的引出
Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?例如线程处于sleep-状态或Blocked 状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域的定义
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
执行流程:
6、再谈引用
https://www.bilibili.com/video/BV1PJ411n7xZ?p=163
Reference子类中只有终结器引用是包内可见的,其他3种引用类型均为public,可以在应用程序中直接使用
6.0 四种引用类型概述
强引用(StrongReference)
最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。【引用关系还在的时候,即使要OOM,GC打死不回收】
软引用(SoftReference)
在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。【引用关系还在的时候,内存不足才回收】
弱引用(WeakReference)
被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。【引用关系还在的时候,不管内存够不够,只要GC就被回收】
虚引用(PhantomReference)
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。【引用关系还在的时候,不管内存够不够,只要GC就被回收】
对象回收和引用的关系
-
强引用对象
只要强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。
显式地将相应 强引用赋值为null 就是可以当做垃圾被收集了 -
其他引用对象
如果没有其他的引用关系,只要超过了引用的作用域,就是可以当做垃圾被收集了
软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。
具体回收时机还是要看垃圾收集策略。
所以,强引用是造成Java内存泄漏的主要原因之一。
6.1 强引用
https://www.bilibili.com/video/BV1PJ411n7xZ?p=164&spm_id_from=pageDriver
在Java程序中,最常见的引用类型是强引用(普通系统99%以上都是强引用),也就是我们最常见的普通对象引用,也是默认的引用类型。
当在Java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。(什么是强引用)
/**
* 强引用的测试
*
* @author shkstart shkstart@126.com
* @create 2020 16:05
*/
public class StrongReferenceTest {
public static void main(String[] args) {
StringBuffer str = new StringBuffer ("Hello,尚硅谷");
StringBuffer str1 = str;
str = null;
System.gc();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str1);
}
}
Hello,尚硅谷
1
局部变量str指向stringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是stringBuffer实例的强引用
对应内存结构:
如果此时,再运行一个赋值语句:
StringBuffer str = new StringBuffer("hello mogublog");
StringBuffer str1 = str;
在程序中我们将 str = null; 则 原来堆中的对象也不会被回收,因为还有其它对象指向该区域
总结
本例中的两个引用,都是强引用,强引用具备以下特点:
- 强引用可以直接访问目标对象。
- 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象。
- 强引用可能导致内存泄漏。
6.2 软引用
https://www.bilibili.com/video/BV1PJ411n7xZ?p=165
- 内存溢出和软引用没啥关系;因为内存要溢出了会GC掉软引用;
- 一句话概括:当内存足够时,不会回收软引用可达的对象。内存不够时,会回收软引用的可达对象
在JDK1.2版之后提供了SoftReference类来实现软引用
// 声明强引用
Object obj = new Object();
// 创建一个软引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引用,这是必须的,不然会存在强引用和软引用
软引用代码演示内存不够的时候才会回收软引用
/**
* 软引用的测试:内存不足即回收
* -Xms10m -Xmx10m
*
* @author shkstart shkstart@126.com
* @create 2020 16:06
*/
public class SoftReferenceTest {
public static class User {
public int id;
public String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "[id=" + id + ", name=" + name + "] ";
}
}
public static void main(String[] args) {
//创建对象,建立软引用
SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));
/*
上面的一行代码,等价于如下的三行代码
User u1 = new User(1, "songhk");
SoftReference<User> userSoftRef = new SoftReference<User>(u1);
u1 = null;//取消强引用
*/
//从软引用中重新获得强引用对象
System.out.println(userSoftRef.get());
System.gc();
System.out.println("After GC:");
//垃圾回收之后获得软引用中的对象
//由于堆空间内存足够,所有不会回收软引用的可达对象。
System.out.println(userSoftRef.get());
try {
//让系统认为内存资源紧张、不够
byte[] b = new byte[1024 * 1024 * 7];
} catch (Throwable e) {
e.printStackTrace();
} finally {
//再次从软引用中获取数据
//在内存不够,报OOM之前,垃圾回收器会回收软引用的可达对象。
System.out.println(userSoftRef.get());//
}
}
}
[id=1, name=songhk]
After GC:
[id=1, name=songhk]
java.lang.OutOfMemoryError: Java heap space
at com.atguigu.java1.SoftReferenceTest.main(SoftReferenceTest.java:51)
null
如上图所示,软引用被回收不一定报OOM;
6.3 弱引用
在JDK1.2版之后提供了WeakReference类来实现弱引用
// 声明强引用
Object obj = new Object();
// 创建一个弱引用
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; //销毁强引用,这是必须的,不然会存在强引用和弱引用
弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。
面试题:你开发中使用过WeakHashMap吗?
WeakHashMap用来存储图片信息,可以在内存不足的时候,及时回收,避免了OOM;
6.4 虚引用
- 软引用和弱引用都可以拿到对象,虚引用拿不到
- 虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。
- 由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
/**
* 虚引用的测试
*
* @author shkstart shkstart@126.com
* @create 2020 16:07
*/
public class PhantomReferenceTest {
public static PhantomReferenceTest obj;//当前类对象的声明
static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用队列
//守护线程 长轮询捕获虚引用队列抛出来的虚引用,表示该虚引用对象被回收了
public static class CheckRefQueue extends Thread {
@Override
public void run() {
while (true) {
if (phantomQueue != null) {
PhantomReference<PhantomReferenceTest> objt = null;
try {
//从虚引用队列中取出最近的虚引用
objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (objt != null) {
System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了");
}
}
}
}
}
@Override
protected void finalize() throws Throwable { //finalize()方法只能被调用一次!
super.finalize();
System.out.println("调用当前类的finalize()方法");
obj = this;
}
public static void main(String[] args) {
Thread t = new CheckRefQueue();
t.setDaemon(true);//设置为守护线程:当程序中没有非守护线程时,守护线程也就执行结束。
t.start();
//1.创建虚引用队列
phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
//2.创建强引用
obj = new PhantomReferenceTest();
//3.用obj 创建对象的虚引用,并指定了引用队列
PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
try {
//不可获取虚引用中的对象 打印 null
System.out.println(phantomRef.get());
//4.将强引用去除
obj = null;
//第一次进行GC,finalize将对象复活了,GC无法回收该对象
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用"); //打印 obj 可用
}
System.out.println("第 2 次 gc");
obj = null;
System.gc(); //一旦将obj对象回收,就会将此虚引用存放到引用队列中。
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null"); //打印obj 是 null
} else {
System.out.println("obj 可用");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
6.5 终结器引用