JVM——GC机制
- 1、什么是GC?
- 2、GC算法的总体概述
- 3、JVM所处的位置
- 4、JVM整体结构
- 5、JVM架构模型
- 6、Java垃圾回收机制优缺点
- 7、GC主要关注的区域
- 垃圾回收算法:标记阶段,引用计数
- 循环引用
- 标记阶段:可达性分析算法
- GC root可以是哪些?
- 总结①
- 8、对象的finalization机制
- 注意
- 生存还是死亡?
- 具体的过程如何?
1、什么是GC?
JVM垃圾收集(Java garbage collection)。
GC采用的分带收集算法:
- 次数上频繁收集
Young
区。 - 次数上较少收集
Old
区。 - 基本不会动
Perm
区。
2、GC算法的总体概述
JVM在进行gc
时,并非每次都会对上面的三个内存区域一起回收,大部分时候回收的都只是新生代
。
GC按照回收的区域分了两种类型:
- 普通GC(又称之为minor gc):只针对
新生代区域的
gc。 - 全局GC(major gc or full gc):针对
老年代
的gc,偶尔伴随着新生代的gc以及对永久代
的gc。
3、JVM所处的位置
JVM
运行在操作系统OS之上,与硬件并没有直接的交互。
4、JVM整体结构
HotSpot VM
是当前市面上高性能Java虚拟机的代表作之一。- 采用
解释器
和即时编译
并存的架构,解释器也就是虚拟机处理字段码
的CPU; - JVM运行整体逻辑框图如下:
其中,执行引擎包含三部分:解释器、即时编译器器、垃圾回收GC。
5、JVM架构模型
Java编译器输入的指令流基本上都是按照基于栈的指令集架构
,另外一种指令集架构则是基于寄存器
的指令集架构,具体差异如下:
基于栈指令集架构:
- 设计实现简单,适用于资源受限的系统;
- 避开了寄存器的分配难题,使用
零地址
指令方式分配; - 指令流中的指令大部分都是
零地址指令
,执行过程依赖于操作栈。指令集更小,编译器更容易实现; - 不需要硬件级支持,可移植性好,更好实现跨平台。
基于寄存器指令集架构:
- 典型的应用是X86的
二进制指令集
,例如传统PC以及Android的Davlik 虚拟机
。 - 指令集架构则完全依赖于硬件,可移植性差。
- 性能优秀和执行高效。
- 花费了更少的指令去完成 一项操作 。
- 大部分情况下,基于
寄存器架构的指令集
往往都是以一地址指令、二地址指令
和三地址指令为主
,而基于栈结构的指令集却往往以零地址指令
为主要。
6、Java垃圾回收机制优缺点
优点:
- 自动内存管理,无序开发人员手动参与内存的分配和回收,降低内存泄露或者溢出的风险。
- 自动内存管理机制,让开发人员着重业务逻辑的实现。
- oracle官网关于GC机制的介绍:GC机制概述。
7、GC主要关注的区域
GC主要关注方法区
和堆
的垃圾收集。
垃圾收集可以对年轻代
回收,也可以对老年代回收
,甚至是全栈和方法区 的回收。
垃圾回收算法:标记阶段,引用计数
堆里存放的几乎所有Java对象
,在GC执行垃圾回收之前,首先休要区分内存哪些是存活的对象,哪些是已经死亡的对象。
一般判定对象存活的方式有两种:引用计数
、可达性分析算法
。
引用计数算法(reference counting)对每一个对象保存一个整型的引用
计数器属性,记录对象被引用的情况。
- 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器+1;当引用失效时,引用计数器-1;
- 若对象A的引用计数器数值0,则表示对象A不可能被使用,可以被GC。
- 优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟。
- 缺点:需要单独的字段存储计数器,增加了存储空间的开销。
每次赋值都需要更新计数器,伴随着加法和减法操作,增加了时间开销。
引用计数器有一个严重的问题,无法处理循环引用
的情况,这是致命缺陷。
循环引用
若p的指针断开的时候,内部的引用形成了一个循环,这就是循环引用。从而造成了内存的泄露。
给出测试代码:
public class RefCountGC {
// 这个成员属性的唯一作用就是占用内存空间
private byte[] bigSize = new byte[5 * 1025 * 1024];
// 引用
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;
obj2 = null;
// 显式的执行GC行为,判定obj1和obj2是否被gc了
System.gc();
}
}
若使用了引用计数算法,则这两个对象都无法被GC
,但是测试会发现均被回收了,则说明Java使用的并不是上述提及的算法。
标记阶段:可达性分析算法
可达性分析算法:可以称之为根搜索算法、追踪性能垃圾收集
。
相比较于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效
等特点,更重要的是可以有效解决循环引用
的问题,防止内存泄露的发生。
实现基本思路:
- 可达性分析算法是以
根对象集合(GCroot)
为起始点,按照从上至下方式搜索被根对象集合所连接的目标对象是否可达。 - 使用了可达性分析算法后,
内存存活对象都会被根对象直接或者间接连接着
,搜索所走过的路径称之为引用链(Reference Chain)。 - 若目标对象没有
任何引用链相连
,则不可达,意味着该对象已经死亡,可以被GC。 - 在可达性分析算法中,只有能够被
根对象
集合直接或者简介链接的对象才是存活对象。
GC root可以是哪些?
- 虚拟机栈中引用的对象,例如各个线程调用的方法中使用到的参数、局部变量等等。
- 本地方法的栈内JNI(也就是通常说的本地方法 )
引用的对象方法区中类静态属性引用的对象
,例如Java类的引用类型的静态变量。 - 方法区中的
常量引用的对象
,例如字符串常量池String Table
存在的引用。 - 所有被
同步锁synchronized
所持有的对象。 - Java虚拟机内部的引用,例如:基本数据类型对应的Class对象、一些常驻的异常对象(NullPointerException、outofMemoryError)、==类加载器 ==。
- 反映Java虚拟机内部情况的
JMXBean
、JVMTI
中 注册的回调、本地代码缓存
等等。
总结①
除了堆空间
的一些结构,例如:虚拟机栈
、本地方法栈
、方法区
、字符串常量池
等地方对堆空间
进行引用的,都可以拿来作为GC root
进行可达性分析。
除了这些固定的GC Root
集合之外,根据用户所选择的GC收集器以及当前回收的内存区域不同,还可以有其他对象临时性 的加入,共同构成完整的GC Root
集合,例如:分代收集 和局部回收(PartialGC)。
由于
root
采用了栈方法存放变量和指针,若是一个指针,则保存了堆内存的对象,但是自己又不存放在堆内存里面,那么其就是一个Root。
注意:若要使用可达性分析来判定内存是否可以被回收,那么分析工作必须要在一个能保证一致性的快照中
进行。若无法满足这点则分析结果的准确度存疑。
以上这点也是导致GC进行时必须stop the world 的一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点
也是必须要停顿的。
8、对象的finalization机制
Java语言提供了对象终止(finalization)机制来允许开发人员 提供对象被销毁之前的自定义处理逻辑 。
若GC发现没有引用指向一个对象,也就是:GC该对象之前,总是会调用该对象的finalize
方法。
finalize
方法允许在子类中被重写,用于在对象被回收之前进行资源的释放。通常会在这个方法中进行一些资源释放和清理的工作,例如:关闭文件IO流、套接字或者是数据库连接等等。
注意
永远不要 主动调用某一个对象的finalize
方法,应该交给GC使用,原因如下:
- 在
finalize
方法时可能会导致该对象复活。 finalize
方法执行的时候时间是没有保障的,完全由GC线程来决定,在某些极端情况下,若不发生GC,则finalize
完全没有执行的机会;由于优先级比较低,即使主动调用了这个方法,也不会因此直接进行回收。- 一个糟糕的
finalize
会严重的影响GC性能。
从功能角度来说,finalize
类似C++的析构函数
,但是Java采用的是基于GC的自动内存管理机制,所以finalize
方法在本质上不同于C++中的析构函数。
由于finalize
方法的存在,虚拟机中的对象一般处于三种可能的状态。
生存还是死亡?
若从所有的根节点都无法访问到某一个对象的时候,说明对象已经无法使用了,则一般来说该被GC回收。
但是实际上 ,一个无法触及的对象可能在某一个条件下复活自己,若是这样,GC回收就是不合理的,因此,定义虚拟机中的对象可能的三种状态如下:
- 可触及的:从
根节点
开始,可以到达的对象。 - 可复活的:对象的所有引用都被释放了,但是对象可能在
finalize
中复活。 - 不可触及的:对象的
finalize
方法被调用了,并且没有复活,那么进入不可触及状态。不可触及对象不可能被复活,因此finalize只会被调用了一次。
以上的三种状态中,由于finalize
方法的存在,只有在对象不可触及的时候才能被GC。
具体的过程如何?
判定一个对象objA是否能被GC,至少需要经历两次标记过程:
- 若对象objA到达
GC Root
没有引用链,则进行了第一次的标记过程。 - 开始进行筛选,判定该对象是否有必要执行
finalize
方法。 -
- 若对象objA没有重写
finalize
方法,或者finalize
方法已经被虚拟机调用过了,则虚拟机将会视之为没有必要执行,objA判定不可触及。
- 若对象objA没有重写
-
- 若对象objA重写了
finalize
方法,并且还没有执行过,那么objA会被插入到F-Queue
队列中,由一个虚拟机自动创建的、低优先级的Finalizer
线程触发其finalize
方法执行。
- 若对象objA重写了
-
finalize
方法是对象逃脱或者 死亡的最后机会,稍后GC会对F-Queue
队列中的对象进行了第二次标记;若objA在finalize
方法中引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移除即将GC的集合。在这之后,对象再次出现没有引用的情况,这种情况下,finalize
方法不会被再次调用,对象直接变成不可触及 的状态,也就是说,一个对象的finalize
方法只会被调用一次。