对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1,只要对象A的引用计数器的值为0,即表示对象A不可能在被使用,可进行回收
循环引用示意图
代码演示
public class RefCountGC {
//这个成员属性唯一的作用就是占用一点内存
private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB
Object reference = null;
public static void main(String[] args) {
RefCountGC obj1 = new RefCountGC();
RefCountGC obj2 = new RefCountGC();
obj1.reference = obj2;
obj2.reference = obj1;
obj1 = null;
obj2 = null;
//显式的执行垃圾回收行为,语句1
System.gc();
}
}
如上图,如果不小心直接把Obj1-reference
和Obj2-reference
重置为null,则在Java堆当中的两块内存依然保持着互相引用,无法回收
GC Roots
根集合就是一组必须活跃的引用可达性算法示意图
在Java语言中,GC Roots包括以下几类元素
虚拟机栈中引用的对象
例如:各个线程被调用的方法中使用到的参数、局部变量等
本地方法栈内JNI(本地方法)引用的对象
方法区中类静态属性引用的对象
例如:Java类的引用类型静态变量
方法区中常量引用的对象
例如:字符串常量池(String Table)里的引用
所有被同步锁synchronized
持有的对象
Java虚拟机内部的引用
基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointException
、OutOfMemoryError
),系统类加载器
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾回收器以及当前回收的内存区域不同,还可以有其他对象临时性
地加入,共同构成完整GC Roots集合。例如,分代收集和局部回收(Partial GC)
技巧:由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存 里面,那它就是一个Root
特殊说明
Stop The World
的一个重要原因。即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的finalization
)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑finalize()
方法finalize()
方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等finalize()
方法,应该交给垃圾回收机制调用。理由有以下几点:
finalize()
时可能会导致对象复活finalize()
方法的执行时间是没有保障的,他完全由GC线程决定,极端情况下,若不发生GC,则finalize()
方法将没有执行的机会finalize()
会严重影响GC的性能finalize()
方法与C++中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize()
方法在本质上不同于C++中的析构函数finalize()
方法的存在,虚拟机中的对象一般处于三种可能的状态非死不可
的,这时候他们暂时处于缓刑
阶段。一个无法触及的对象有可能在某一个条件下复活
自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态,如下
finalize()
中复活finalize()
被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为==finalize()
只会被调用一次==finalize()
方法的存在,进行的区分。只有在对象不可触及时才可以被回收具体过程
判定一个对象A是否可回收,至少要经历两次标记
过程
如果对象A到GC Roots没有引用链,则进行第一次标记
进行筛选,判断此对象是否有必要执行finalize()
方法
①如果对象A没有重写finalize()
方法,或者finalize()
方法已经被虚拟机调用过,则虚拟机视为没有必要执行,对象A被判定为不可触及的
②如果对象A重写了finalize()
方法,且还未执行过,那么对象A会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()
方法执行
finalize()
方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记,如果A在finalize()
方法中与引用链上的任何一个对象建立联系,那么在第二次标记时,A会被移出即将回收
集合,之后,对象会再次出现没有引用存在的情况,在这种情况下,finalize()
方法不会被再次调用,对象会直接编程不可触及的状态,也就是说,一个对象的finalize()
方法只会被调用一次
获取dump文件
**方式1:通过命令行的形式获取,命令行使用jmap**
方式2:使用JVisualVM导出
演示代码
public class GCRootsTest {
public static void main(String[] args) {
List<Object> numList = new ArrayList<>();
Date birth = new Date();
for (int i = 0; i < 100; i++) {
numList.add(String.valueOf(i));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("数据添加完毕,请操作:");// 在此处,使用JvisualVM导出dump文件
new Scanner(System.in).next();
// 释放引用
numList = null;
birth = null;
System.out.println("numList、birth已置空,请操作:");// 在此处,再次使用JvisualVM导出dump文件
new Scanner(System.in).next();
System.out.println("结束");
}
}
使用MAT来分析两次导出的dump文件
从上图可知,只有30多行的代码就存在这1700个GC Root。
在实际情况下,很少获取全部的GC Root,一来是GC Root节点太多,获取时间太长;二来获取全部后分析与定位不方便
标记-清除算法(Mark-Sweep)是一种非常基础和常见得垃圾收集算法,该算法被J.McCarthy等人在1960年提出并应用于Lisp语言
当堆中的有效内存空间(available memory
)被耗尽的时候,就会停止整个程序(也就是Stop The World),然后进行两项工作:第一项是标记,第二项则是清除
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的活对象复制到未被使用的内存块中,之后清除正在使用的内存块中所有对象,交换两个内存的角色,最后完成垃圾回收
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行
在新生代,对常规应用的垃圾回收,一次通常可以回收70%~99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法
两者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销
Mark-Sweep算法 | Mark-Compact算法 | Copying算法 | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存
而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,他比复制算法多了一个标记阶段,比标记-清除多了一个整理内存的阶段。
有没有一种最优算法呢?——没有最好的算法,只有最合适的算法
前面的算法中,并没有一种算法可以完全替代其他算法,他们都具有自己独特的优势和特点。分代收集算法应运而生
分代收集算法,是基于不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长,但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收
目前几乎所有的GC都是采用分代收集(Generational Collecting)算法执行垃圾回收的。
在HotSpot中,基于分代的概念,GC所使用的的内存回收算法必须结合年轻代和老年代各自的特点
以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施;当内存回收不佳(碎片导致的Concurrent Mode Failure时)将采用Serial Old执行Full GC以达到对老年代内存的整理
上述现有的算法,在垃圾回收过程中,应用软件将处于一种STW的状态,在STW状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生
使用这种方式,由于垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,由于线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降