目录
CMS(Concurrent MArk Sweep,并发标记清除)
概述
java语言是提供自动垃圾回收功能的,C++没有自动垃圾回收,垃圾回收也不是java首创的,java在垃圾回收这块一直在不断更新升级。
什么是垃圾?
没有被任何引用指向的对象。
哪些区域回收?
- 方法区:基本不回收方法区
- 堆:频繁回收新生代,较少回收老年代
例:
new User().toString();
Object obj = new Object();
obj.hashCode();
obj = null;
为什么要回收?
垃圾对象如果不回收,它就会一直占用内存空间,垃圾对象越积越多,从而可能导致内存溢出,堆内存空间中的碎片进行管理,如果不整理,需要存储象数组这样的对象下。
早期的垃圾是怎么进行回收?
早期像C++,需要程序员手动销毁垃圾对象
不足:麻烦,程序员手动删除,有时候可能会忘记删除,导致内存溢出
内存溢出
内存不够用,报内存溢出错误
内存泄露
有些对象已经不再被使用了,但是垃圾回收对象又不能回收它,这种悄悄的占用内存资源的现象称为内存泄露。
现在的java语言引进自动的内存管理
优点:降低了程序员的工作量,降低了内存溢出和内存泄露的风险
自动内存管理的担忧
自动的垃圾回收,降低了程序员对内存管理的能力,一旦出现问题,无法下手解决。
垃圾回收算法
标记阶段算法
标记哪些对象已经是垃圾
引用计数算法
Object obj = new Object(); 对象内部有一个计数器,有一个引用指向计数器+1 obj.hashCode(); Object object = obj; 又有一个引用指向对象,计数器 + 1 obj = null; 计数器 - 1 object = null; 计数器 -1
优点:实现简单
缺点:不能解决循环引用问题,需要维护计数器空间,赋值后对计数器进行更新操作
根可达算法(可达性分析算法)
List<Integer> list = new ArrayList();
while(true){
list.add(new Random().nextInt());
}
实现思路:
从一组GCRoots对象(一组活跃的对象,当前栈桢中使用的对象)开始向下查找,如果与GCRoots对象相关联的,那么就不是垃圾对象,否则判定为垃圾对象。
哪些元素可以作为GCRoots对象?
- 栈桢锁使用的对象(虚拟机栈、本地方法栈)
- 静态成员变量所引用的对象
- synchronized 同步锁对象
- JVM系统内部的对象
优点:可以避免对象循环引用的问题
finalize()
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();
}
}
}
执行结果:
finalize() 是Object 类中定义的方法,子类可以重写,但是不要主动自己去调用它,finalize()在对象回收前,有垃圾回收线程调用,只被调用一次。一般情况下,不需要重写此方法,如果在对象销毁前需要执行一些释放资源的操作,可以重写此方法,但是要注意,不要在此方法将对象复活或出现死循环。
生存还是死亡?
从垃圾回收的角度,对象可分为三种状态:
可触及的:从根节点开始,可以到达这个对象
可复活的:已经被标记为垃圾,finalize()方法还没有执行(对象有可能执行finalize())
不可触及的:当对象finalize()已经执行了,而且对象没有被复活,那么对象就进入到不可触及状态。
对象回收细节
如果一个对象第一次被标记为垃圾,且finalize()没有执行,将这些对象放到一个队列中,调用他们的finalize(),如果在finalize()方法,对象与GCRoots中的某个对象关联上了,从队列中移出。当第二次对象被标记为垃圾对象,那么直接就是不可触及状态,被回收掉。
final、finally、finalize的区别
final:修饰符(关键字)有三种用法:如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。将变量声明为final,可以保证它们在使用中不被改变,被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为final的方法也同样只能使用,不能在子类中被重写。
finally:通常放在try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
finalize:Object类中定义的方法,Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。
垃圾回收阶段算法
复制算法
复制:使用两块内存空间(标引为两个新生代),将当前空间的存活的对象复制到另一个空间中,整理排放整齐,清除原来的空间。
优点:内存的碎片少
缺点:需要的内存大,因为需要两块内存空间。G1垃圾回收器每个区域又分为多个小的区域,需要记录地址(时空开销较大)
标记-清除算法
清除:将根可达算法得到的垃圾对象进行标记而不是清除,将标记的垃圾对象的地址记录放入到一个空白的列表,当需要创建一个新的对象需要分配内存空间时,会先从空闲列表判断空间是否够用,如果够用,则将创建的对象替换掉垃圾对象。
优点:实现简单,不需要移动对象
缺点:内存碎片较多
复制与标记清除算法的区别
复制算法针对新生代,对象较少的情况,需要移动对象,效率高,不会产生内存碎片,但是对于老年代,对象较多的情况不使用。
标记-清除算法是针对于老年代存活的对象较多,不需要移动对象,但是不会整理对象,会产生内存对象。
标记-压缩算法(标记-清除-压缩算法)
为了解决标记清除算法的不足,(内存碎片太多)
将存活的对象进行整理,然后清除垃圾对象,这样就不会产生内存碎片。
优点:不会产生内存碎片,节省了内存空间(相比于复制算法)
缺点:从效率上看,要低于标记-清除算法,移动的时候需要暂停用户线程。
标记清除算法与标记压缩算法的区别
- 两者都要标记垃圾
- 标记清除不移动对象,产生内存碎片,将垃圾对象维护到一个空闲的列表中
- 标记压缩移动对象,不会产生内存碎片,清理掉对象并进行整理压缩。
标记清除 | 标记整理 | 复制 | |
---|---|---|---|
速率 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要存活对象的2倍空间(不堆积对象) |
移动对象 | 否 | 是 | 是 |
分代收集
年轻代(新生代):存活的对象生命周期短,需要频率回收,复制算法效率高,适合用于年轻代。
老年代:对象生命周期较长,不需要频率回收,使用标记清除和标记压缩算法。
STW(Stop The World)
在垃圾回收线程标记时,需要在某个时间点上,让所有的用户线程暂停一下,保证在判定对象是否垃圾时的准确性。性能好的垃圾回收器,发生STW次数少。
垃圾回收器
垃圾回收器是对垃圾回收的落地实现。
JVM中分为不同种类的垃圾回收器,可以根据场景选择对应的垃圾回收器。
垃圾回收器的分类
按照线程数量分
单线程垃圾回收器(Serial):垃圾回收线程只有一个,适用于小型的场景。
多线程垃圾回收器(Parallel):垃圾回收线程有多个同时执行,执行效率高。
按照工作模式分
独占式:垃圾回收线程工作时,用户线程全部暂停。例:STW
并发式:垃圾回收线程执行时可以不用暂停用户线程,从CMS这款垃圾回收开始引入并发执行。
按照内存的工作区域分
年轻代区域的垃圾回收器和老年代区域的垃圾回收器
GC 性能指标
- 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运 行时间+内存回收的时间)
- 垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- 内存占用:Java 堆区所占的内存大小。
- 快速:一个对象从诞生到被回收所经历的时间。
HotSpot 垃圾收集器
- 串行回收器:Serial、Serial Old
- 并行回收器:Parallel Scavenge、Parallel Old、ParNew
- 并发回收器:CMS、G1
CMS(Concurrent MArk Sweep,并发标记清除)
CMS之前不管是单线程还是多线程的垃圾回收器,都是独占的并发标记,清除首创用户线程可以和垃圾回收线程并发执行。
目的:追求低停顿。
垃圾回收过程
1. 初始化标记:单线程独占标记对象(STW)单线程
2. 并发标记:垃圾回收线程和用户线程并发执行 多线程
3. 重新标记:使用多线程独占进行标记对象 独占的
4. 并发清除:垃圾回收线程和用户线程并发执行
优点:可以做到并发收集
缺点:
- 用户线程和垃圾回收并发执行,导致吞吐量降低。
- 无法处理浮动垃圾
浮动垃圾:垃圾回收线程并发标记时,用户线程不暂停,标记完成后,随时会产生新的垃圾,无法处理,只能等到下次垃圾回收处理。
三色标记算法
将对象分为不同的状态:黑色、灰色、白色
黑色:已经被标记过的不是垃圾的对象,且该对象下的关联属性也标记过了。
灰色:已经被垃圾回收器扫描过,但是还存在没有标记过的
白色:没有被垃圾回收器扫描过的,表示不可达(标记为垃圾)
步骤:
- 刚开始,确定为GC Roots的对象是黑色
- 将GC Roots对象直接关联的对象置为灰色
- 遍历灰色对象,下面如果还有关联的对象,灰色变为黑色,下面关联的是灰色
- 重复标记
- 将白色的对象清除
问题:会出现漏标和错标的问题
漏标:A关联B,B关联D,当B是灰色,此时,A和B断开了联系,但是B已经是灰色的,B和D就是浮动垃圾,需要等待下次回收。
错标:A关联B,B是灰色,B和D断开,A和D建立连接,A是黑色的,不会再次扫描,就会将D清理掉(出现程序错误)
解决错标问题:
将发生变化的关系进行记录,重新标记。
G1(Garbage First 垃圾优先)
G1也是使用并发标记和清除的
将整个堆的每个区域划分为更小的区间,回收时可以根据每个区间的优先级(由里面的垃圾量),先回收优先级较高的空间,降低了用户线程的停顿,提高了吞吐量。对整堆进行统一的管理,没有年轻代和老年代。
因为 G1 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region) (物理上不连续的逻辑上连续的)。使用不同的 Region 来表示 Eden、幸存者 0 区,幸存者 1 区,老年代等。 G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
流程:
前三个步骤与CMS一样
- 初始化标记:单线程独占标记对象(STW)单线程
- 并发标记:垃圾回收线程和用户线程并发执行 多线程
- 最终标记:使用多线程独占进行标记对象 独占的(修改并标记因应用程序变动的内容(对象依赖关系发生变化)STW)
- 筛选回收:对各个 Region 的回收价值和成本进行排序,回收价值最大的 Region(用最少的时间来回收包含垃圾最多的区域)