标记阶段:引用计数算法
对象死亡:没有被任何活的对象引用 引用计数:对每个对象保存一个整型的引用计数器属性,用于记录对象被引用情况 优点:实现简单;判断效率高,回收没有延迟性 缺点:
- 需要单独的字段存储计数器,增加存储开销
- 每个赋值都要更新计数器,增加时间开销
- 无法循环引用
Python使用引用计数算法进行垃圾回收,解决循环引用: - 手动解除
- 弱引用
标记阶段:可达性分析算法
根搜索算法、追踪性垃圾收集 实现简单、执行高效、解决循环引用问题,防止内存泄露 实现思路:
- 根对象集合(一组必须活跃的引用)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
- 使用可达性分析算法后,内存中的存活对象都会被根对象直接或间接连接,搜索所走过的路径称为引用链
- 如果目标对象没有任何引用链,则是不可达的,表示对象已经死亡
- 在可达性分析算法中,只有能够被跟对象集合直接或者间接连接的对象才是存活对象
GC Roots包含以下几类元素
- Java 虚拟机栈中引用的对象
- 本地方法栈(JNI)内引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁Synchronized持有的对象
- JVM内部的引用
- 反映JVM内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存
- 临时性对象: 分代收集和局部回收
小技巧:由于GC Root采用栈方式存放变量和指针,所以如果一个指针,他保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它是一个Root
对象的finalization机制
对象的finalization机制:对象被销毁之前的自定义处理逻辑
-
在垃圾回收之前,先调用finalize方法,允许被重写,用于对象被回收时进行资源释放 -
永远不要主动调用对象的finalize方法 1、导致对象复活 2、调用时不保证马上执行,不发生GC,finalize方法将没有执行机会 3、糟糕的finalize方法会影响GC性能 -
虚拟机中对象一般处于是那种可能的状态 1、 可触及的:从根节点可访问 2、可复活的:对象所有引用都被释放,但对象可能在finalize中复活 3、不可触及的:finalize调用,但没复活。finalize方法只会被调用一次。(垃圾) -
判断一个对象objA是否回收,至少经历两次标记过程 1、如果objA到GCRoots没有引用链,则进行一次标记 2、进行筛选,判断此对象是否必要执行finalize方法 1)如果对象objA没有重写finalize方法,或者finalize方法已经被调用过,则objA不可触及的 2)如果objA重写了finalize方法,且还没执行过,那么objA会被插入到F-Queue中,由一个VM自动创建的、低优先级的Finalizer线程触发finalize方法执行 3)finalize方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue中的对象进行第二次标记。如果objA在finalize方法中与引用链上的人一个对象建立了联系,则移除即将回收队列。
MAT(Memory Analyzer)与JProfiler的GC Roots溯源
获取dump文件: jmap -dump:format = b,live,file = test1.bin pid jvisualvm
package chapter15;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Scanner;
public class GCRootsTest {
public static void main(String[] args) {
List<Object> numList = new ArrayList<>();
Date birth = new Date();
for (int i = 0; i < 100; i++){
numList.add(String.valueOf(i));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据添加完毕,请操作");
new Scanner(System.in).next();
numList = null;
birth = null;
System.out.println("numList,birth已置空,请操作");
new Scanner(System.in).next();
System.out.println("结束");
}
}
}
在第一次用户输入前导出一个dump文件,输入后在导出一个dump文件,导出过程为
- 在cmd中输入jvisualvm 打开visualvm
- 运行 GCRootsTest 代码
- 点击堆dump 然后右键dump文件另存为(因为是个缓存文件)
- 使用MAT打开具体的dump文件
清除阶段:标记-清除算法(Mark-Sweep)
执行过程: 当堆中的有效空间被耗尽的时候,就会停止整个程序(Stop the world),然后标记,清除
- 标记:从根节点出发,标记所有被引用的对象
- 清除:对堆内存从头到尾进行线性遍历,清除所有没有被标记的对象
优点:基础常见 缺点:
- 效率不高;
- GC时要停止整个应用程序,用户体验差;
- 清理出的空间不连续,产生内存碎片,需要维护一个空闲列表
何为清除? 不是真的置空,而是把清除对象的地址放在空闲列表中,新对象要放的时候会覆盖原有数据
清除阶段:复制算法
优点:
缺点:
- 需要两倍的内存空间
- G1这种分拆成大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
特别的:系统中垃圾对象比较多,复制算法要复制的存活对象较多,效率较低。新生代中使用较多,老年代中不适合使用
清除阶段:标记-压缩算法
又称为标记整理、Mark-Compact
执行过程: 标记阶段:同标记清除算法 整理阶段:将所有存活对象压缩到内存的一端,按顺序存放 又称为标记-清除-压缩,移动式的 优点:
- 不需要使用空闲列表,直接指针碰撞
- 消除内存减半的高额代价
缺点:
- 效率上标记-整理算法低于复制算法
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 需要STW
小结
时间效率:复制最快 内存利用率:标记-清除、复制 内存整齐度:标记整理、复制 综合:标记整理好
分代收集算法
新生代: 对象生命周期短、存活率低、回收频繁-----复制算法 老年代: 对象生命周期长、存活率高、回收不及新生代频繁—混合
- 标记阶段的开销与存活对象数量成正比
- 清除阶段开销与所管理区域的大小成正比
- 整理阶段开销与存活对象的数据成正比
增量收集算法、分区算法
增量收集算法:标记清除和复制算法 让垃圾收集线程和应用程序线程交替执行,处理线程间冲突 缺点:
- 间断性执行应用程序代码,减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会造成垃圾回收的总体成本上升,造成吞吐量下降
分区算法:将一个大的内存区域分割成多个小块,每个小区间独立使用,独立回收。
|