文章目录
JVM-垃圾回收
仅做学习内容的简单记录
1.垃圾回收相关算法
1.1 对象存活判断
1.1.1 引用计数算法
引用计数算法(Reference Counting) 比较简单, 对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
优点: 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
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;
obi2 =null;
System.gc();
}
}
因循环引用这个缺点,而不使用引用计数算法。
1.1.2 可达性分析算法
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
- 可达性分析算法是以根对象集合(GC Roots) 为起始点, 按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着, 搜索所走过的路径称为引用链(Reference Chain)
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
1.1.3 对象的finalization机制
Java语言提供了对象终止(finalization) 机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
finalize() 方法允许在子类中被重写, 用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
永远不要主动调用某个对象的finalize() 方法, 应该交给垃圾回收机制调用。理由包括下面三点:
- 在finalize()时可能会导致对象复活。
- finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC, 则
finalize() 方法将没有执行机会。 - 一个糟糕的finalize() 会严重影响GC的性能。
1.2 垃圾清除
目前在JVM中比较常见的三种垃圾收集算法是标记-清除算法(Mark-Sweep) 、复制算法(Copying) 、标记-压缩算法(Mark-Compact)
1.2.1 标记-清除算法
当堆中的有效内存空间(available memory) 被耗尽的时候, 就会停止整个程序(也被称为stop the world) ,然后进行两项操作,第一项则是标记,第二项则是清除。
-
标记:Collector从引用根节点开始遍历, 标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
-
清除:Collector对堆内存从头到尾进行线性的遍历, 如果发现某个对象在其Header中没有标记为可达对象, 则将其回收。
缺点
- 效率不算高
- 在进行GC的时候,需要停止整个应用程序,导致用户体验差
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
1.2.2 复制算法
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1这种分拆成为大量region的GC, 复制而不是移动, 意味着GC需要维护region之间对象引用关系, 不管是内存占用或者时间开销也不小。
复制算法用在年轻代,minorGC后将存活的对象复制到s1区,整齐排列,然后交换s0和s1角色,然后存活的的对象就整齐排列在s0区并且没有内存碎片。本身s0和s1加起来只占新生代20%的空间,内存占用和时间开销的问题就没有太大影响,所以复制算法适用于新生代的垃圾回收。
1.2.3 标记-整理算法
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact) 算法。
指针碰撞:
如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump the Pointer) 。
优点
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时, JVM只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
缺点
- 从效率上来说,标记-整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
- 移动过程中,需要全程暂停用户应用程序。即:STW
1.3 分代回收
年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题, 通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
- Mark阶段的开销与存活对象的数量成正比。
- Sweep阶段的开销与所管理区域的大小成正相关。
- Compact阶段的开销与存活对象的数据成正比。
1.4 增量收集算法
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程
序线程交替执行。每次,垃圾收集线程只收集一小块区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
缺点
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
1.5 分区算法
分区算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
1.5 分区算法
分区算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
仅做学习记录与交流,如有错漏敬请指出