虚拟机就好比是一个有限空间的一个房子,在我们生活中,也会产生各种各样的垃圾(没有被任何引用指向的对象),虚拟机也不例外,垃圾满了会造成内存溢出等问题,那虚拟机是怎么进行垃圾回收的呢?让我们来揭开这神秘的面纱
1.概述
程序计数器、虚拟机栈、本地方法栈都是随着线程而生,随着线程而灭,当方法结束时或者是线程结束时,内存会自动进行回收。但是堆和方法区则不同,他们具有不确定性,比如在线程运行的时候,我们并不确定会创建多少个对象,多少个方法,每个对象或者方法会被分配多少内存,一切的一切都是未知因数,故为动态。
2.内存溢出和内存泄露
内存溢出:经过垃圾回收后,内存中仍然无法存储新创建的对象,内存不够而导致的溢出
内存泄漏:一些已经不用的对象,但是垃圾回收不能判断为垃圾,这些对象默默的占用的内存,称为内存泄露,大量的此类对象的存在,也是导致内存溢出的原因。
3.“死亡”还是“生存”?
下来我们来介绍两种判断对象是否死亡的方法
1.引用计数算法
String s1=new String("abc");//+1
String s2=s1;//+1
s2=null;//-1
s1=null;//-1
但是有一种情况不方便。就是两个对象互相引用循环,以至于计数器都不为0,不能回收,产生内存泄露。
2.可达性分析
通过一个GC Roots的根对象作为起始点,向下搜索,路径称为引用链,如果对象和这个GC Roots没有任何的引用链连接,就会被判定为可回收对象
GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象(方法堆栈中使用到的参数、局部变量、临时变量等)
- 在方法区中类静态属性引用的对象(java类的引用类型静态变量)
- 在方法区中常量引用的对象(字符串常量池中的引用)
- 在本地方法栈中(Native方法)引用的对象
- java虚拟机内部引用(基本类型对应的class对象,部分异常对象)、系统类加载器
- 所有被同步锁持有的对象
- 本地代码缓存
- 区域不同,其他临时性的对象
4.引用
一共分为四种引用:强引用、软引用、弱引用、虚引用
强引用的对象是不能被垃圾进行回收的,软引用、弱引用、虚引用是来标记对象的一种状态(已经是垃圾了)
- 强引用:在程序代码中普遍存在的引用赋值
Odject object=new Objet();
- 软引用:有用且非必要的对象,在内存溢出时会进行二次回收(如果在内存充足的情况下,可以保留软引用对象,如果内存不足,经过一次垃圾回收仍然不够,那么将清除软引用的对象)
- 弱引用:非必要对象,只能生存到下一次垃圾收集为止(弱引用管理的对象,只能存活到下一次垃圾回收)
- 虚引用:无实在性的作用,唯一目的只是在这个对象被收集时收到一个系统通知(和没有引用是一样的,只是为了系统的检测)
5.死亡前的最后一根稻草
在以前在刑场上经常会有关键的救命文书,在虚拟机里也有类似的情况,被可达性算法判定为不可达对象,对象只是判为“缓刑”,而不是真正的立即处死,它真正的判断死亡需要2次标记,具体看下图
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
//super.finalize();
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器,触发FULL GC 也不是调用后立刻就回收的,因为线程的执行权在操作系统
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
obj= null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
6.回收方法区
方法区回收主要是两部分:
1.废弃的常量
2.不在使用的类型
那如何判断一个常量是否废弃呢?下面3个条件至关重要
- 该类的所有实例均被回收
- 加载该类的加载器已被回收
- 该类的java.lang.class对象没有在任何地方进行引用