深入理解java虚拟机 第三章 垃圾收集器与内存分配策略
可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链
当一个对象到GC Roots没有任何引用链相连时,证明此对象不可用
可作为GC Roots的对象:
1 虚拟机栈中引用的对象
2 方法区中类静态属性引用的对象
3 方法区中常量引用的对象
4 本地方法栈中JNI(Native方法)引用的对象
引用分类:
1 强引用:在程序代码中普遍存在的,平时用的,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
2 软引用:还有用但非必需的对象,在系统发生内存溢出异常前,将会把这些对象列进回收范围进行第二次回收
用SoftReference类来实现软引用
3 弱引用:非必需对象,但强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前
当垃圾收集器工作时,就会收掉被弱引用关联的对象,用WeakReference类来实现弱引用
4 虚引用:最弱的一种引用关系,完全不对其生存时间构成影响,也无法通过虚引用来取得一个对象实例
唯一目的就是能在这个对象被收集器回收时收到一个系统通知,用PhantomReference来实现
对象死亡:
两次标记过程:
1 对象在进行可达性分析后发现没有引用链,那它将会被第一次标记并且进行一次筛选
筛选的条件是此对象是否有必要执行finalize()方法,
当对象没有覆盖finalize()方法,或已经被调用过,虚拟机将这两种情况都视为“没有必要执行”
有必要执行,那么这个对象将会放置在一个叫做F-Queue的队列中,
稍后一个虚拟机自动建立,低优先级的Finalizer线程去触发这个方法,但是不会等待它运行结束
finalize()方法是对象逃脱死亡命运的最后一次机会
2 稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象这时候还没有逃脱,那基本上就真被回收了
任何一个对象的finalize()方法只会被系统自动调用一次
垃圾收集算法
标记-清除算法
最基础的收集算法,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
复制算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块用完了,将还存活的对象复制到另一块
然后再把已使用过的内存空间一次清理掉。这样每次都对整个半区进行内存回收,也不用考虑内存碎片
但是代价是将内存缩小为原来的一半
现在的商业虚拟机都采用这种收集算法来回收新生代,不过将内存分为较大的Eden和两块较小的Survivor,
每次使用Eden和一块Survivor,回收时一次复制到另一块Survivor上,默认比例8:1:1,
当Survivor空间不够用时,需要其他内存(这里指老年代)进行分配担保
如果Survivor没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代
在对象存活率较高时要进行较多的复制操作,效率将会变低
如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对所有对象都100%存活的情况
所以,老年代不能直接选用这种算法
标记-整理算法
标记过程与“标记-清除”算法一样,但是后续步骤是让所有存活的对象都向一端移动,最后直接清理掉端边界以外的内存
主要用在老年代上
分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”算法,根据对象存活周期的不同将内存划分为几块
这样就可以根据各个年代的特点采用最合适的收集算法
HotSpot的算法实现
枚举根节点
HotSpot的实现中,有一组称为OopMap的数据结构,在类加载完成时,HotSpot就把对象内什么偏移量上是什么类型
的数据计算出来,在JIT编译过程中,也会记下栈和寄存器中哪些位置是引用,这样GC在扫描时就知道这些信息了
安全点
HotSpot也没为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置称为安全点
即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停
Safepoint的选定即不能太少让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷
安全点选定基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的
当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动轮询这个标志
发现中断标志为真时就自己中断挂起
安全区域
如果没有分配CPU时间,就会出问题,用安全区域来解决
安全区域是指在一段代码片段中,引用关系不会发生变化,在这个区域中的任何地方开始GC都是安全的
垃圾收集器
内存回收的具体实现
Serial收集器
单线程的收集器,必需暂停其他所有的工作线程,简单而高效
ParNew
Serial收集器的多线程版本,在单CPu环境中绝对不会有比Serial更好的效果,随着CPU的增加,效能增加
Parallel Scavenge收集器
目标是达到一个可控制的吞吐量,吞吐量就是CPU运行用户代码的时间和CPU总消耗时间的比值
适合在后台运算而不需要太多交互的任务
Serial Old收集器
老年代的Serial收集器
Parallet Old收集器
老年代的Parallel Scavenge收集器
CMS收集器
以获取最短回收停顿时间为目标的收集器,4个步骤
1 初始标记
2 并发标记
3 重新标记
4 并发清除
G1收集器
最前沿成果,面向服务端应用的垃圾收集器
1 并发与并行
充分利用多CPU、多核环境下的硬件优势,来缩短Stop-The-World停顿的时间,
可以通过并发的方式让java程序继续执行
2 分代收集
分不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象
3 空间整合
从整体来看是基于“标记-整理”算法实现的收集器,从局部上来看是基于“复制”算法实现的
运行期间不会产生内存空间碎片,收集后能提供规整的可用内存
4 可预测的停顿
建立可预测的停顿时间模型
G1收集器将整个java堆划分为多个大小相等的独立区域,虽然还有新生代和老年代的概念,但不再是物理隔离的了
都是一部分region(不需要连续)的集合
G1收集器能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个java堆中进行全区域的垃圾收集
G2跟踪各个Region里面垃圾堆的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的
运作步骤:
1 初始标记
2 并发标记
3 最终标记
4 筛选回收
内存分配和回收策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次GC
大对象直接进入老年代
大对象是指需要大量连续内存空间的java对象,典型的大对象是那种很长的字符串以及数组
长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活
并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄为1
对象在Survivor区中每过一次Minor GC ,年龄就增加1岁,当它年龄增加到一定程度,就晋升老年代
动态对象年龄判定
为了更好地适应不同程序的内存状态,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半
年龄大于或等于该年龄的对象就可以直接进入老年代
空间分配担保

浙公网安备 33010602011771号