如何判断一个引用是否存活
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
优点:可即刻回收垃圾,当对象计数为0时,会立刻回收;
弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。JVM不用这种算法
可达性分析算法
通过 GC Root 对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到 GC Root没有任何的引用链相连时,说明这个对象是不可用的。
- JVM中的垃圾回收器通过可达性分析来探索所有存活的对象
- 扫描堆中的对象,看能否沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以回收
可以作为GC Root的对象有哪些?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
GC算法
GC定义:
GC( Garbage Collection ),垃圾回收,是Java与C++的主要区别之一。作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码。这是因为在Java虚拟机中,存在自动内存管理和垃圾清理机制。对JVM中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,保证JVM中的内存空间,防止出现内存泄露和溢出问题。
finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法
Minor GC 和 Full GC的区别?
- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC:回收老年代和新生代,老年代的对象存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
FullGC危害: 在发生FULL GC的时候,意味着JVM会安全的暂停所有正在执行的线程(Stop The World),来回收内存空间,在这个时间段内,所有除了回收垃圾的线程外,其他有关JAVA的程序,代码都会静止,反映到系统上,就会出现系统响应大幅度变慢,卡机等状态。
标记清除算法
定义: 标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间
- 这里的腾出内存空间并不是将内存空间的字节清0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存
从根出发,所有活动对象构成的数据结构是一颗多叉树。标记阶段就是遍历这颗树,给每个节点打上"存活"标记。标记阶段会遍历所有活动对象,因此,标记阶段耗时与活动对象数成正比。清除阶段需要遍历整个堆,遍历过程中会完成两件事:
- 将非活动对象回收
- 清除活动对象的标记,为下次GC做准备
优点:
- 实现简单,与其他算法组合也简单
- 与保守式GC算法兼容,因为他们都不需要移动对象。
缺点:
- 碎片化。简单来说就是随着分配和回收的进行会产生很多小的空闲对象散落在堆中,彼此也不连续。碎片化带来的问题是无法分配大的空闲空间,尽管总的空闲空间是够用的。碎片化带来的另一个问题是局部性原理失效,因为具有引用关系的数据分配到的空闲空间并不连续。
- 分配速度慢。因为空闲链表是单链表结构,分配时需要遍历链表,时间复杂度是O(n)。
- 与写时复制不兼容。因为标记阶段会修改堆内对象,导致大量拷贝。
标记整理
GC标记压缩算法分为标记阶段和压缩阶段。它是将GC标记清除算法的清除阶段换成了压缩,而且这里的压缩不是将活动对象从一个空间复制到另一个空间,而是将活动对象整体前移,挤占非活动对象的空间。
优点:
- 堆的利用率高
- 分配速度快
- 不会产生碎片化
GC标记压缩算法缺点是吞吐量低。因为在压缩阶段我们需要遍历堆3次,耗费时间与堆大小成正比,堆越大,耗费时间越久。
复制算法
GC复制算法的思路是将堆一分为二,暂时叫它们A堆和B堆。申请内存时,统一在A堆分配,当A堆用完了,将A堆中的活动对象全部复制到B堆,然后A堆的对象就可以全部回收了。这时不需要将B堆的对象又搬回A堆,只需要将A和B互换以下就行了,这样原来的A堆变成B堆,原来的B堆变成了A堆。经过这一轮复制,活动对象搬了新家,垃圾也被回收了。
GC复制算法就是在两个堆之间来回倒腾。JVM中的新生区就是使用的这种方式,总结为:在幸存区中,谁空谁是to
由于对象地址发生了变化,GC复制算法在复制过程中还需要重写指针。从复制的角度来看,活动对象是从A堆复制到B堆。因此我们也将A堆称为From空间,将B堆称为To空间。经过复制,原本散落在From空间中的活动对象被集中放到了To空间开头的连续空间内,这一过程也叫做压缩。
优点:
- 吞吐量优秀。这得益于GC复制算法只会搜索复制活动对象,能在较短时间内完成GC,而且时间与堆的大小无关,只与活动对象数成正比。相比于需要搜索整个堆的GC标记清除算法,GC复制算法吞吐量更高,而且堆越大,差距越明显。
- 分配速度快。因为不需要搜索空闲链表,在O(1)的时间复杂度就能完成分配。
- 不会发生碎片化。因为每次复制都会执行压缩。
- 与缓存兼容。因为复制过程中使用了深度优先遍历,具有引用关系的对象会被复制到相邻的位置,局部性原理可以很好发挥作用。
缺点:
- 堆的使用效率低。这是一个最显眼的问题,因为要留一半的空间用来复制,所以堆的利用率总小于50%。
- 不兼容保守式GC。因为GC复制算法需要移动对象。
- 复制时存在递归调用,需要消耗栈空间,并可能导致栈溢出。
复制算法使用的最佳场景:对象存活度较低的时候,即新生区
分代回收
开始新创建的对象都被放在了新生代的伊甸园中
当多创建几个对象的后发现伊甸园装不下了。
当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做轻GC Minor GC。一次小的垃圾回收,根据可达性算法找到不能被回收的,把这些不能被回收的对象复制到幸存区To中(用的复制算法),然后把幸存的对象的寿命+1,然后回收掉伊甸园里面的全部对象。
再把From区和To区的指向互换,那么这是to区就又空出来了
再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),
这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,From也会被垃圾回收检查,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1。
这里1是从伊甸园区新进幸存区的对象,2是原本就存活在幸存区寿命+1后为2
这时就有对象在两次GC中存活下来那么他的存活次数就会是2,如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),那么就会晋升到老年代中去
因为老年代的垃圾回收频率比较低,这个对象在新生代里面反复GC都没有回收掉说明长时间在用,那么就没有必要在新生代中反复GC
如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收。如果两次GC后还是放不下, 就会报OOM异常
在报堆内存溢出之前,还会去尝试minorGC一次如果minorGC了释放出来的空间还是放不下,那么就会触发一次fullGC(类似于大扫除,老年代和新生代都会被GC),如果还是没办法放下那么就会报java.lang.OutOfMemoryError: Java heap space
总结: 在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,使用复制算法比较合适,只需要付出少量存活对象的复制成本就可以完成收集。老年代对象存活率高,适合使用标记-清理或者标记-整理算法进行垃圾回收。