GC垃圾回收
什么是GC回收
GC是作用在方法区和Java堆中的一种垃圾回收机制。当我们启动了 main
函数后,GC线程就已经开始跑在后台了。所以一般情况下,每一个程序至少有两个线程。
public class Test {
public static void main(String[] args) {
// 一旦运行,就有两个线程:main + GC
}
}
什么对象要回收
垃圾对象判断
引用计数法 Reference Counting
这种方法在Java在很少用,此算法让每一个对象维护一个引用计数器,引用数量为零的对象就是垃圾对象,要被清除。这种方法有两个很明显的缺点:
- 每次对象赋值后都要维护对应的计数器,消耗空间资源
- 不能处理循环引用,所谓循环引用就是A对象中的某个字段引用着B对象,B对象中的某个字段引用着A对象。这样当A和B的引用变量同时变为空后,仍然有对用户不可见的引用分别指向AB,这样就无法回收了。
// 循环引用
Person objA = new Person();
Person objB = new Person();
objA.field = objB;
objB.field = objA;
objA = null; // objA对象的field仍然指向objB
objB = null; // objB对象的field仍然指向objA
可达性分析法 Reachable Analysis
通过 GC Root
作为根进行搜索,不能够搜索达到的对象就是垃圾对象。 GC Root
一般是:
- Java栈中的引用变量
- 方法区中的常量
- 方法区中的静态变量
- 本地方法栈的引用变量。
- 虚拟机内部自带的引用,如基本数据类型的Class对象,常见异常、错误
四种引用
- 强引用,只要强引用存在就算OOM也不会被回收。
Object obj = new Object();
- 软引用,在要发生OOM之前会被回收,即内存不够的时候就干掉。
SoftReference<Object> sf = new SoftRefernce<>(obj);
。
根据这个特性,软引用很适合用作内存敏感的高速缓存,当内存足够的时候,很容以获取缓存;当内存紧张的时候就将缓存干掉。
// 浏览器对象
Browser browser = new Browser();
// 后台加载浏览页面
BrowserPage page = browser.getPage();
// 将浏览完毕的页面设置为软引用
SoftReference<BrowserPage> sf = new SoftReference<>(page);
... ...
// 回退或者再次浏览此页面
if (sf.get() != null) {
page = sf.get(); // 内存充足,可以直接获取缓存
} else {
page = browser.getPage(); // 内存不足,缓存的页面已经回收,重新获取
sf = new SoftReference<>(page); // 再次将页面缓存
}
- 弱引用,活不过下一次垃圾回收,比软引用生命更加短暂。
WeakReference
- 虚引用,不会对对象的生命周期构成影响,也无法通过虚引用获取一个对象,一个对象的虚引用就是形同虚设,任何时候都可能被回收。唯一目的就是在回收的时候获得一个系统通知。
PhantomReference
常用于追踪对象被垃圾回收的活动,由于虚引用必须和引用队列(ReferenceQueue)联合使用,当一个有虚引用的对象持有虚引用的时候,在对象回收之前就会把虚引用加入到队列中。所以可以检查引用队列中是否存在该虚引用来知道对象是否将要进行垃圾回收,这样就可以在对象被垃圾回收之前进行一些操作。
GC回收日志
基本格式:[名称:GC前的内存占用 -> GC后的内存占用(该区的总内存大小)]
GC回收4种算法
复制算法 Copying
复制算法就是维护两个空间,一个空间在某个时刻必然为空(A),一个空间用于存储对象(B)。每一次GC回收会先将B区中的剩余对象复制到A区,然后一把刀杀清B区。这种方式的优点很明显:
- 由于对存储对象的区域一次性清空,所以不会有内存碎片。可以使用
bump-the-pointer
实现复制到To区时快速分配内存。 - 对象的存活率低的时候,效率高。
但缺点也很明显:
- 需要一个干净的空间作为To区,可用空间缩小为原来的1/2,空间利用率低
复制算法主要应用于堆中年轻代的垃圾回收MinorGC,因为这个区域的对象正常情况下存活概率低(大概2%)。所以年轻代中,只用一小部分To区来作为存储被复制的幸存对象的区域。注意From和To的逻辑角色在每一次GC后是动态交换的。
标记清除法 Mark Sweep
标记清除顾名思义,标记出要清除的对象,之后将对象清除就可以了。所以不需要额外的空间,但是缺点是:
- 会产生内存碎片,即删除掉零散的对象后会有不连续的空闲空间散布在各处,有时无法利用来存储一个完整的新对象。
- 效率不稳定,扫描两次空间分别标记、清除。
标记压缩法 Mark Compact
标记压缩法,就是在标记清除法的基础上,将剩余幸存的对象进行重新整理,往一端滑动存活对象,使得最终腾出的空间是连续的。优点就是:
- 没有内存碎片,提高了空间的利用率。
但是缺点就是:
- 效率低下。效率比较:标记复制 > 标记清除 > 标记压缩。
分代收集
分代收集就是GC回收使用的策略,这也是为何堆分为多个区域的原因:
- 次数上频繁的收集年轻代,复制算法。因为年轻代的生产率、死亡率都高,所以需要高效的清除算法,而存储幸存对象的空间不需要太大,故有
。
- 次数上较少收集老年代,标清和标整相结合(多次GC后才整理)。老年代的存活率较高,区域大,所以一定时间内的内存碎片是可以接受的,
。
- 元空间基本上不会动。