目录
- 前言
- 正文
- 垃圾标记算法
- 引用类型
- 强引用
- 软引用
- 弱引用
- 虚引用
- 引用计数法
- 循环引用问题
- 根可达性分析法
- 虚拟机栈(栈帧的局部变量表)中的引用
- 方法区中类静态属性引用
- 方法区中常量引用
- 本地方法栈(Native方法)引用
- 垃圾回收算法
- 标记清除算法
- 复制算法
- 复制算法和标记清除算法如何选择?
- 标记整理算法
- 分代收集算法
- 总结
前言
为什么程序跑久了有时会变卡,如果你看过笔者的 《JVM运行时内存模型》那么应该知道用户线程在跑时会将new出来的对象放到虚拟机堆中,当堆满了之后会发生垃圾回收,而垃圾回收过程中会STW,也就是停止用户线程运行,这种在程序层面看来就是没反应,所以如果出现这种问题,我们就要想办法让STW的时间尽量的变短;
这分为几种情况,垃圾回收时间长或者垃圾回收的频率高;
垃圾回收时间长的情况?
可能是因为GC要扫描的区域比较大有,清理时间较长,或者说我们选择的垃圾回收器不适合当下情况
GC频率过高
可能有部分对象发生内存泄漏,导致无法回收,占用堆空间,此时的堆空间变小,满了就触发GC,频率自然高。也可能是年轻代和老年代空间分配不合理,空间过小导致频繁GC;
正文
认清原理,并接受原理,才能有效地做事情,所以我们要知道有哪些垃圾回收算法、哪些垃圾回收器,了解其特性,才可以帮忙我们进行程序优化。
垃圾标记算法
程序运行时,堆中存储了一堆对象,那垃圾回收器怎么知道哪些需要回收,不会发生误回收的情况呢?当一个实例对象有被引用时可以认为它是个活跃的状态,不进行回收。所以根据这个特性,可以添加一个标记专门记录被引用的次数,当次数不为0的就不进行垃圾回收,这种方式就是引用计数法。还可以在根节点挨个遍历,遍历到的对象进行标记,那些没遍历到的则被当作垃圾处理,这里的根节点可以简单理解为main方法里面开始的都是根,这种方式就是根可达性分析法;目前使用最多的是根可达性分析法;
引用类型
强引用
强引用我们平常用得最多,强引用在内存不足时,垃圾回收器不会进行回收,即使抛出OOM,因为该实例对象被引用,证明不是垃圾数据。
如:
Person person=new Person();
软引用
软引用在内存不足时(证明老年代满了,触发FULL GC),垃圾回收器会进行回收,所以垃圾标记算法在扫描标记时,判断如果引用为软引用,则把它当垃圾处理;
如:
Person person=new Person();
SoftReference<Person> sPerson=new SoftReference<>(new Persion());
弱引用
弱引用不管内存够不够时,这里内存够触发GC则证明年轻代满了触发YGC,内存不够则证明是老年代满了触发FULL GC,只要进行垃圾回收就会被当垃圾清除;
如:
Person person=new Person();
WeakReference<Person> sPerson=new WeakReference<>(new Persion());
虚引用
虚引用在垃圾回收时被清理,一般配合队列使用,在垃圾回收时会将其加入队列,此时队列可以拿到虚引用,但是虚引用指向的强引用Person已经被回收了,调用phantomReference.get()方法时获取不到实例对象;
ReferenceQueue<Person> QUEUE = new RegferenceQueue<>();
Person person=new Person();
PhantomReference phantomReference = new PhantomReference(person, QUEUE);
引用计数法
我们在引用指向对象时就给该对象引用次数加1,在指引指向别的对象或变量销毁时将其-1,在垃圾回收器进行回收时,判断对象的引用标记数是否为0,如果为0则认为它是垃圾数据,对其进行回收;
优点:算法简单,判断搞笑
缺点:无法解决循环引用问题
循环引用问题
public class MyObject {
public Object ref = null;
public static void main(String[] args) {
Person person1 = new Person();
Person person2 = new Person();
person1.friend = person2;
person2.friend = person1;
person1 = null;
person2 = null;
}
}
虽然person1对象和person2对象已经没有任何引用指向它们了,但是此时它们的引用标记数依旧不为0,垃圾回收器认为它们不是垃圾,导致空间无法得到释放;
根可达性分析法
根可达性法以GC ROOTS节点为起点,开始往下扫描,遍历属性引用关系来判断对象是否为可回收的垃圾对象;如果把ROOTS比喻成江河的源头,那么那些未跟源头相通的河或者内湖就可以理解为垃圾对象,三色标记法是比较常用的根可达性分析法;
那么,GC Roots 是什么呢?
虚拟机栈(栈帧的局部变量表)中的引用
public class Demo {
public static void main(String[] args) {
// 创建实例A
// a1:就是虚拟机栈(栈帧的局部变量表)中引用
A a1 = new A();
// 当 a1 不再指向实例A的时候,实例A 将不可达
a1 = null;
}
}
当开启一个新线程时,会创建线程私有的虚拟机栈,栈帧中维护着局部变量表,线程执行方法中的变量会记录到局部变量表里面,所以可以把局部变量表的变量理解为GC Roots,往下遍历其引用链,在引用链上的都是可用对象;
方法区中类静态属性引用
public class Demo {
// 创建实例A
// a2:方法区中类静态属性引用
public static A a2 = new A();
public static void main(String[] args) {
// 当 a2 不再指向实例A的时候,实例A 将不可达
a2 = null;
}
}
当属性被static修饰时,其变量会放到方法区中的静态变量区中,静态变量区的变量指向的引用可以看作GC ROOTS;
方法区中常量引用
public class Demo {
// 创建实例A
// a3:方法区中常量引用
public static final A a3 = new A();
public static void main(String[] args) {
// 当 a3 不再指向实例A的时候,实例A 将不可达
a3 = null;
}
}
当使用final修饰后,其变量会存储到方法区中的常量池中,所以方法区中常量引用可以看作GC ROOTS;
本地方法栈(Native方法)引用
本地方法栈和虚拟机栈类型,也会维护局部变量表,当本地方法区引用了堆中的对象时,可以把其当作GC ROOTS;
垃圾回收算法
标记清除算法
标记清除法分为了标记和清除两个步骤,首先采用可达性分析法进行标记后,对被标记为垃圾的对象进行回收;
黑色为已经扫描过的,有被引用的对象,灰色代表待扫描对象,白色代表待垃圾回收对象;当垃圾回收完成后,此时的空间就变得很碎片化,如果此时需要存入一个对象,该对象占用3个格子的空间,那么第一行的两格空间就没法被利用到;所以碎片化太严重的情况下,空间利用率低,GC的频率就会变高;
复制算法
复制算法会开辟两块内存空间,当标记完成之后,将存活对象拷贝到另外一个空间中,并将当前空间数据清除。这种方式可以解决内存碎片化问题,而且效率高。但意味着我需要更多的内存空间,比如1G的空间,由于需要复制就得拆分成两块500M的空间,所以也是很耗费内容的;
复制算法和标记清除算法如何选择?
一般如果程序对象一般占用空间较小的话,可以采用标记清除算法,这样空间利用率会比复制算法高;如果对象是比较大的话,选择复制算法的空间利用率会比较高,算法没有好坏,只有适不适合;
标记整理算法
标记整理算法在进行标记之后,将存活对象移动到一块,剩下的全部清理掉。这样就有碎片化带来的问题,也不像复制算法那样浪费空间;
分代收集算法
由于在垃圾回收时,会STW暂停用户线程,导致程序没响应,如果停顿时间过长,会导致服务宕机;所以堆空间过大会导致垃圾回收时间过长,所以将堆进行分代管理,分为年轻代和老年代;
当new对象时,会尝试将数据存储到Eden区,如果Eden区满了之后会触发YGC,年轻代采用复制算法,第一次将存活对象复制到survivor0区,第二次会将survivor0和Eden区存活的对象复制到survivor1区,就这样反复在survivor0和survivor1间复制。
当年轻代的存活对象存活时间较长时,导致年轻代剩余空间变小,YGC就会变得很频繁;针对这种情况,给每个存活对象设置存活年龄,每次YGC后未被回收将年龄+1,当年龄到达阈值时(默认15),将其迁移到年轻代中;
当老年代空间满了之后会触发FULL GC,此时会对年轻代和老年代进行垃圾回收,老年代的回收算法不同的垃圾回收器采用的机制不同;
总结
垃圾回收算法有多种,每种针对的场景不一样,没法说其好坏,只有综合业务场景,设备信息等因素挑选出适合程序的算法,现市面上垃圾回收器有多种就是针对不同场景而定制的。