JVM系列:GC垃圾回收算法与垃圾回收器

如何判断一个对象已死?
  1. 引用计数器法
  • 在对象中添加一个引用计数器,一旦该对象被另一个地方引用时,引用计数器+1,失去引用时-1。没有引用的对象即可被回收
  • 无法解决循环引用等问题
  1. 可达性分析算法
  • 通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GCRoots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的
  • GC Roots的对象可包含以下几种
    • 栈帧中局部变量表引用的对象
    • 方法区类静态属性引用的对象
    • 方法区常量引用的对象
    • 本地方法栈中JNI引用的对象
    • 虚拟机内部的引用
    • 被同步锁持有的对象
引用
  1. 强引用(Strongly Reference)
  • Object obj = new Object()
  • 只要引用关系存在,在任何时候垃圾回收器都不会回收
  1. 软引用(Soft Reference)
  • 有用,但非必要的对象
  • 将要发生内存溢出前,将软引用的对象列入回收范围进行二次回收,二次回收后内存还不够才抛出内存溢出
  • SoftReference
  1. 弱引用(Weak Reference)
  • 弱引用的对象在每次垃圾回收时都会被回收掉
  • WeakReference
  1. 虚引用(Phantom Reference)
  • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例
  • 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
  • PhantomReference
对象的死亡过程
  1. 可达性分析后,如果没有被引用链,那么进行第一次标记
  2. 然后进行一次筛选,筛选条件是对象没有重写finalize()方法或者finalize()方法已经被执行过一次,如果没有稍后将被第二次标记
  3. 如果不满足2,对象将被防止到FQueue队列中,由低调度优先级的Finalizer线程执行
  4. 如果对象在finalize中进行了自救,比如把自己赋值给了某个静态变量,那它会在第二次标记时被移除可回收列表,否则它将在第二次标记时被标记为可回收。
方法区的回收
  1. 常量的回收
  • 失去引用时被清理出常量池
  1. 类型回收
  • 该类型的所有实例都已经被回收
  • 加载该类型的类加载器已经被回收
  • Class对象没有在任何地方被引用
  • 满足以上3个条件的类允许被回收,可以通过-Xnoclassgc【关闭类回收】进行控制,可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息
分代收集
  1. 个分代假说
  • 弱分代说:绝大部分对象都是朝生夕灭的
  • 强分代说:熬过约多次回收的对象约难以被回收
  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数,比如说新生代的对象被老年代的对象引用,这就使得新生代的对象在垃圾回收时难以为回收,经过几次回收后也晋升到老年代,因此跨代引用也随即不存在了,因此不需要针对老年代的每一个对象进行跨代引用检查,只需要在新生代建立全局数据结构(记忆集),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描
垃圾收集的定义

部分收集(Partial GC)

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集,目前只有CMS会有单独的老年代收集
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为

整堆收集(Full GC)

  • 收集整个Java堆和方法区的垃圾收集
垃圾收集算法
  1. 标记-清除算法:标记需要回收或不需要回收的对象,清除需要回收的对象
  • 缺点:
    • 需要回收的对象越多,执行过程越慢
    • 清除后造成碎片化内存,导致后续需要存入大对象是无法找到连续的内存空间

标记-清除算法.jpg

  1. 标记-复制算法:将内存分为相同大小的两半,只使用其中一半,回收时将存活的对象直接复制到未使用的另一半,然后清除掉原来使用的一半
  • 缺点:
    • 浪费一半空间
    • 对于存活数较多的情况下,复制的开销太大

标记-复制算法.jpg

  • 优化:通常情况下98%的对象活不过第一轮垃圾回收【Serial和ParNew使用了这种优化策略清除新生代】
    • 新生代划分为一块较大的Eden和两块较小的Survivor区,每次只是用Eden和其中一块Survivor
    • 垃圾回收时将Eden和Survivor上还存活的对象都复制到另一块空的Survivor上,然后整体清理掉Eden和使用的Survivor
    • 当Survivor不足以容纳存活对象时,通常将会依赖老年代进行分配担保
  1. 标记-整理算法:标记然后让存活对象往内存一端移动,边界以外的内存空间全部回收
  • 缺点
    • 整体移动所有存活对象,消耗性能
  • Parallel Scavenge采用标记整理算法,CMS采用标记-清除算法,但当内存无法分配给大对象时,再进行一次整理算法
根节点枚举
  1. 枚举阶段会停止用户线程
  2. 在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译(见第11章)过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用
安全点:

HotSpot没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点。

  1. 安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的
  2. 长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点

如何让所有线程都跑到安全点

  1. 抢先式中断:先让所有用户线程中断,然后没有跑到安全点的线程恢复直到跑到安全点再次中断
  2. 主动式中断:垃圾收集前设置一个标记位,各线程自己轮询标志,标志位为真时在最近的安全点挂起
安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。
当线程执行到安全区域时,首先标记自己已进入安全区,当垃圾回收时就不用管进入了安全区的线程,当线程要离开安全区时先判断根节点枚举是否已经完成,已完成就继续执行,没完成就等待,直到收到完成的信号。

记忆集

用户避免整个老年代跨代引用

垃圾回收器
  1. Serial收集器
  • 特点:
    • 单线程,垃圾收集时必须暂停用户线程(Stop The World)
    • 新生代采用标记-复制算法,老年代采用标记-整理算法

Serial收集器.jpg

  1. ParNew收集器:Serial收集器的多线程版本
  • -XX:+UseConcMarkSweepGC激活后的默认新生代垃圾回收器
  • ParNew在单核系统下性能还不如Serial,因为存在线程交互

ParNew收集器.jpg

  1. Parallel Scavenge收集器
  • 新生代垃圾收集器
  • 目标时增加吞吐量,即代码运行时间/(代码运行时间 + 垃圾回收时间),该值越大说明垃圾回收时间占用的比例就越低。
  • 参数设置
    • -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,>0的毫秒数
    • -XX:GCTimeRatio:吞吐量大小的参数,0-100
    • -XX:+UseAdaptiveSizePolicy:激活后不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)
  1. Serial Old收集器:
  • Serial收集器的老年代版本,
  • JDK1.5及以前与Parallel Scavenge搭配使用
  • CMS发生失败后的后备预案
  • 基于标记整理算法
  1. Parallel Old收集器:Parallel Scavenge收集器的老年代版本,与Parallel Scavenge收集器搭配使用
  2. CMS收集器
  • 过程
    • 初始标记:只标记GC Roots能直接关联到的对象,速度很快,会中断用户线程
    • 并发标记:并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
    • 重新标记:重新标记并发标记过程中用户线程造成标记变化的部分
    • 并发清除:清除掉被标记的对象

CMS收集器.jpg

  • 缺点
    • 并发阶段占用用户线程,造成吞吐量降低,CMS默认启动的回收线程数是(处理器核心数量+3)/4,比如核心数是2,就需要分一半CPU来执行回收线程。
    • 无法清除浮动垃圾,浮动垃圾在并发标记和并发清理时会因为用户线程并发执行产生,需要等下一次回收才能被清理,另外回收过程中用户线程也在执行,在回收的途中因此需要预留一定的空间给用户线程【-XX:CMSInitiatingOccupancyFraction】。如果预留的空间不满足对象的分配,那么就会导致一次并发失败,这是就会启用备用预案,使用SerialOld收集器进行收集,但STW的时间就变得比较长了。
    • 会造成内存碎片,-XX:+UseCMS-CompactAtFullCollection,默认开启,执行内存整理,但不能并发,-XX:CMSFullGCsBefore-Compaction参数可以设置多少次FullGC不整理,超过了才整理
  1. G1收集器
  • 思路:
    • 将内存分为大小相同的Region,每个Region可以根据策略扮演Eden,Suvivor,老年代空间,收集器根据角色选择策略回收
    • Region中有一类特殊的Humongous区域用来存储大对象,超过了Region大小一半的对象即被认为是大对象,每个Region的大小通过-XX:G1HeapRegionSize设置,值为1-32M中2的N次幂,超过Region的,会被存在N个连续的Humongous区域。
    • 收集器回跟踪每个Region的价值大小,价值大小即能回收到的内存大小和回收需要的时间,价值越高,越优先收集
  • 运作过程
    • ·初始标记:只标记GC Roots直接关联的对象,然后修改TAMS(Top at Mark Start:位于该指针位置之上新分配的内存不会被回收)
    • 并发标记:递归扫描对象图,找出要回收的对象,完成后还需要根据SATB(原始快照)重新处理并发阶段发生过引用变动的对象
    • 最终标记:短暂暂定线程,处理并发阶段遗留的记录
    • 筛选回收:更新各个Region的统计数据,根据价值和成本进行排序,根据用户期望的执行时间制定回收计划,需要回收的Region中存活的对象复制到空的Region中,再清理掉原来的Region
GC参数

GC参数1.jpg

内存的分配与回收
  1. 内存优先在Eden区分配
public class AllocationDemo {

    public static void main(String[] args) {
        byte[] a = new byte[2 * 1024 * 1024];
        byte[] b = new byte[4 * 1024 * 1024];
        byte[] d = new byte[4 * 1024 * 1024];
    }
}
  • 第一次创建a时存放到了Eden区
  • 创建b对象时,因为b对象大小 + a对象大小 + JVM启动的系统对象占用的量会超过8M,因此进行一次MinorGC,a对象应该被移动至Suvivor区,但因为Servivor区只有1M,不够放,因此通过分配担保提前移动至老年代,此时老年代占用2M多
  • 当创建d对象时,同样的,会将b对象往老年代移动
  • 因此最终内存分配排除掉系统自带对象大致为Eden:4M,老年代:6M
[GC (Allocation Failure) [DefNew: 5035K->896K(9216K), 0.0025250 secs] 5035K->2944K(19456K), 0.0025678 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 4992K->0K(9216K), 0.0029093 secs] 7040K->7014K(19456K), 0.0029350 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4342K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  53% used [0x00000000fec00000, 0x00000000ff03d8a0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 7014K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  68% used [0x00000000ff600000, 0x00000000ffcd99b8, 0x00000000ffcd9a00, 0x0000000100000000)
 Metaspace       used 3103K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 329K, capacity 392K, committed 512K, reserved 1048576K
  1. 大对象直接进入老年代
  • -XX:PretenureSizeThreshold设置直接进入老年代的大对象的大小(Serial和ParNew)
  1. 长期存活的对象进入老年代
  • -XX:MaxTenuringThreshold=N,设置进入老年代的对象的年龄,如果对象一直存活,需要在Suvivor来回复制N次后才会进入老年代
  1. 动态对象年龄判定
  • 如果Survivor区中的相同年龄的对象占用的内存大于Survivor区的一半,那么大于等于该年龄的对象将可以移动至老年代
  1. 空间分配担保

内存担保检查.jpg
本文参考《深入理解java虚拟机》第三版,大家也可以去看看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值