深入学习理解JVM系列(三) -- 深入浅出垃圾收集器与内存分配策略

一、垃圾收集(Garbage Collection GC)

  1. 问题:哪些内存需要回收;什么时候回收;如何回收?
  2. 针对堆和方法区的内存分配和回收,其它三个区域是线程私有的(生命周期和线程一致)
  3. 分配和回收是动态的(不确定性)

二、对象死亡判断(判断是否需要回收)

回收堆内存前,需要对对象实例进行判断,有以下算法对对象是否存活进行判断:

1. 引用计数算法(Reference Counting)

  • 原理:在对象中添加一个引用计数器,每当有地方引用该对象,计数器值就加一,引用失效就减一,回收计数器值为零的对象
  • 缺点:当两个对象之间互相引用时,引用计数算法失效,导致无法回收(JVM不使用该算法)

2. 可达性分析算法(Reachability Analysis)

  • 原理:从一系列根对象(GC Roots)节点开始,根据引用关系向下搜索,若某个对象到GC Roots间没有引用链(Reference Chain)相连,即从GC Roots到该对象不可达,则该对象判定死亡(可回收)
  • 可作为GC Roots的对象:全局性的引用(常量、类静态属性)、执行上下文(栈帧中的本地变量表
    • 虚拟机栈(栈帧中的本地变量表) 中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量
    • 本地方法栈中JNI(Native方法)引用的对象
    • 方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
    • 方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
    • 虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
    • 所有被同步锁(synchronized关键字)持有的对象
    • 反映Java虚拟机内部情况的JM XBean、JVM TI中注册的回调、本地代码缓存等
  • 除了以上固定的GC Roots,其他对象也可临时性加入GC Roots(分代收集和局部回收

3. 引用类型(Reference)

  • 强引用(Strongly Reference):传统引用定义,new出来的新对象(不会回收)
  • 软引用(Soft Reference):非必须的对象(内存溢出前才进行回收)
  • 弱引用(Weak Reference):非必须的对象,强度比软引用弱一点(一定会被回收)
  • 虚引用(Phantom Reference):幽灵引用,最弱的引用,无法通过虚引用获得对象实例(为对象设置回收前系统通知)

4. finalize()方法

  • 对象回收前两次标记:
    • 第一次:可达性分析,没有与GC Roots相连
    • 第二次:是否有必要执行finalize()方法
      • 有必要:对象进入F-Queue队列中,由Finalizer线程执行对象的finalize()方法(可在方法中实现自救)
      • 没必要:对象没有覆盖finalize()方法、finalize()方法已被调用过——>对象被回收
  • finalize()方法只能执行一次(自救机会只有一次)
  • finalize()方法不等于C/C++中的析构函数,不建议使用,可以使用try-finally

5. 回收方法区

  • 回收性价比较低
  • 回收两部分内容:废弃的常量(常量池回收)和不再使用的类型(类的卸载)
  • 判断一个类型是否不再被使用(类的卸载条件):
    • 该类所有实例已经被回收,包括子类实例
    • 该类的类加载器已经被回收,通常很难达成
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  • HotSpot提供-Xnoclassgc 参数来控制是否对类进行卸载

三、垃圾收集算法(追踪式垃圾收集 Tracing GC)

1. 分代收集理论(Generational Collection)

  • 分代假说:
    • 弱分代假说:绝大多数对象都是朝生夕灭
    • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡(按照对象年龄回收)
    • 跨代引用假说:跨代(新老代)引用相对于同代引用仅占少数
  • 将Java堆按对象划分为不同区域:新生代(Young Generation)和老年代(Old Generation)
  • 记忆集(Remembered Set):新生代上建立的数据结构,标识出老年代的哪一块内存存在跨代引用(解决跨代引用问题时,不需要再扫描整个老年代)
  • 整堆收集(Full GC):收集整个Java堆和方法区
  • 部分收集(Partial GC):收集Java堆中的某一块区域(局部回收)
    • 新生代收集(Minor GC/Young GC):收集新生代区域
    • 老年代收集(Major GC/Old GC):收集老年代区域
    • 混合收集(Mixed GC)收集整个新生代和部分老年代(只有G1收集器)

2.标记—清除(Mark-Sweep)算法(老年代)

  • 两个阶段:
    • 标记:标记出所有需要回收的对象(判定对象是否需要回收)
    • 清除:统一回收所有被标记的对象
  • 缺点:
    • 执行效率不稳定(对象数量多且大部分需要回收时,需要大量的标记清除操作)
    • 产生大量不连续的内存碎片,导致内存空间碎片化(无法储存大对象)

3. 标记—复制算法(新生代)

  • 解决标记-清除算法,大量可回收对象时效率低的问题
  • 将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间一次清理掉
  • 缺点:
    • 只能使用一半内存(空间浪费)
    • 多数对象存活时,需要复制大量对象(开销大)
  • 优点:不会出现空间碎片化问题,分配内存时只要移动堆顶指针,实现简单,运行高效
  • Java虚拟机大多采用标记-复制算法,回收新生代(朝生夕灭特点:98%对象会被第一轮GC)
  • Appel式回收:将新生代分为一块较大的Eden空间和两块较小的Survivor空间
    每次只使用 Eden 空间和其中一块 Survivor,回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,然后清理 Eden 和已使用过的那块Survivor空间
  • HotSpot的Serial和ParNew新生代收集器,使用Appel式回收策略;
    Eden和Survivor大小默认比例为8:1(新生代可用空间为总内存的90%,空出10%)
  • 分配担保(Handle Promotion):当Minor GC后有多于 10% 的对象存活,一块Survivor空间不足时,需要依赖老年代进行分配担保(Handle Promotion),将多余存活对象直接存入老年代

4. 标记—整理(Mark- Compact)算法 (老年代)

  • 解决标记-复制算法,对象存活率高时的效率低问题

  • 标记阶段与标记-清除算法一样

    • 第二阶段不同:将所有存活对象移向内存空间一端,直接清理掉边界以外的内存
  • 吞吐量(Throughput):应用程序线程用时占程序总用时(线程加垃圾收集)的比例

  • 赋值器(Mutator):使用垃圾收集的用户程序,可称为用户程序/用户线程

  • 缺点:移动对象并更新引用,操作复杂
    移动操作需要全程暂停用户线程(Stop The World

  • 标记-清除算法(非移动式):不移动对象,停顿时间短,延迟低(CMS收集器:多次清除+1次整理)
    标记-整理算法(移动式):移动对象,程序吞吐量高(Parallel Old收集器)

  • 新生代使用:标记-复制算法
    老年代使用:标记-清除算法、标记-整理算法

四、HotSpot虚拟机的算法细节实现

1. 根结点枚举

  • 需要暂停用户线程(Stop The World),对栈进行扫描,找到对象的引用
  • OopMap数据结构:存储栈上对象的引用信息
    • 栈里和寄存器的引用(即时编译时在安全点记录,正常new出来的对象)
    • 对象内的引用(类加载动作完成时,HotSpot会计算出对象内什么偏移量上是什么类型的数据)
  • 通过遍历栈帧上的OopMap,实现GC Roots枚举

2. 安全点(Safepoint)

  • OopMap存在问题:导致OopMap内容变化(更新)的指令非常多,每一条指令都生成对应的OopMap,需要大量空间
  • 解决:只有在安全点才会生成(或更新)对应的OopMap
  • 定义:用户线程执行过程中的一些特殊位置
  • 特殊位置:具有让程序长时间执行的特征——指令序列的复用(方法调用、循环跳转、异常跳转)——所有非计数循环的末尾、所有方法返回之前、每条字节码的边界
  • 两种方案中断线程:
    • 抢先式中断:中断所有线程,检查线程位置,若有线程不在安全点则先恢复,直至安全点(不推荐)
    • 主动式中断:设置一个标志位,线程执行过程会主动去轮询这个标志,如果标志为真,就自己中断(轮询标志的地方与安全点重合,以及创建对象和在堆上分配内存的地方)
    • 大部分虚拟机使用主动式中断,HotSpot生成的轮询指令:test指令

3. 安全区域(Safe Region)

  • 解决程序线程不执行时(Sleep/Blocked状态),无法到达安全点中断自己的问题
  • 定义:确保某一段代码中,引用关系不会改变(拉伸的安全点)
  • 线程进入安全区域时,会先标识自己,当线程离开安全区域时,JVM会检查该线程是否完成根节点枚举,未完成则需要等待完成,才能离开
  • OopMap、安全点、安全区域相当于:商店的商品清单、在商店开门前关门后进行商品清点、商店放假的时候

4. 记忆集与卡表

  • 记忆集定义:记录从非收集区域(老年代)指向收集区域(新生代)的指针集合(抽象数据结构)
  • 跨代指针的记录精度:字长精度(32/64位)、对象精度(对象的字段)、卡精度(内存区域的对象)
  • 记忆集具体实现:通过卡表(Card Table)的方式实现记忆集(最常见)
  • HotSpot卡表:字节数组CARD_TABLE,数组元素(1个字节):对应内存区域中的一块内存块(卡页Card Page 512字节)
  • 卡页内一个或多个对象存在跨代指针,对应卡表的数组元素值为1(元素变Dirty),没有则为0

5. 写屏障(Write Barrier)

  • 通过写屏障更新维护卡表状态
  • 定义:“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作
  • 写前屏障(Pre-Write Barrier):在赋值前的部分的写屏障
    写后屏障(Post- Write Barrier):在赋值后的部分的写屏障(除了G1,其他收集器只用到了写后屏障

6. 并发的可达性分析

  • 解决或降低用户线程的停顿时间(延迟)———因为必须保障一致性的快照上进行对象遍历
  • 三色标级:白色(对象尚未被收集器访问)、黑色(已经被访问,且所有引用都被扫描)、灰色(已经被访问,但该对象上至少存在一个引用还未扫描)
  • 问题:用户线程与收集器并发工作时,引用关系发生变化
  • 对象消失问题:原本是黑色的对象被误标为白色
    • 线程插入了从黑色到白色的新引用(增量更新Incremental Update)
    • 线程删除了全部从灰色到白色的直接或间接引用(原始快照 Snapshot At The Beginning)
  • 增量更新:记录新插入的从黑色到白色的引用,之后以黑色为根重新扫描(黑色->灰色)
  • 原始快照(SATB):记录删除的从灰色到白色的引用,之后以灰色为根重新扫描(G1)
  • 记录都是通过写屏障实现,CMS通过增量更新实现并发标级

五、经典垃圾收集器

  • 七种垃圾收集器——连线:两种可以搭配使用
    • 单线程串行:一个GC线程与用户线程交替执行
    • 多线程并行(Parallel):多个GC线程与用户线程交替执行
    • 并发(Concurrent):GC线程与用户线程同时执行

1. Serial收集器

  • 单线程串行收集器,客户端(Client)模式下默认新生代收集器,适合用户桌面的应用场景
  • 优点:简单,高效(单线程),额外内存消耗最小(内存资源受限时),最高的单线程收集效率(单核处理器)

2. ParNew收集器

  • 多线程并行收集器,除了多线程其余与Serial一样,服务端(Server)模式的新生代收集器
  • 只有ParNew收集器可以和CMS收集器配合使用(并入CMS后,第一款退出历史舞台的收集器)

3. Parallel Scavenge收集器

  • 多线程并行收集器,吞吐量优先收集器,与ParNew不同(吞吐量,自适应调节策略),其它一样
    • 吞吐量 = 运行用户代码时间/(运行垃圾收集时间+运行用户代码时间)
  • CMS收集器关注缩短用户线程的停顿时间(延迟,用户交互,响应速度),Parallel Scavenge收集器关注吞吐量(后台运算)
  • -XX: MaxGCPauseMillis:最大垃圾收集停顿时间
  • -XX: GCTimeRatio :吞吐量大小(0,100) (用户线程时间与垃圾收集时间之比)
  • 自适应调节策略:-XX: +UseAdaptiveSizePolicy ,激活该参数后,不需要指定其它参数,虚拟机根据系统情况动态调整

4. Serial Old收集器(标记-整理算法)

  • Serial收集器的老年代版本,单线程串行
  • 主要是客户端模式
  • 服务端模式两种用途:
    • 与Parallel Scavenge配合使用
    • CMS收集器失败时的备选收集器(并发收集时发生Concurrent Mode Failure )

5. Parallel Old收集器(标记-整理算法)

Parallel Scavenge收集器的老年代版本,多线程并行,吞吐量优先

6. CMS(Concurrent Mark Sweep)收集器

  • 目标:获取最短回收停顿时间——>响应速度、用户交互(网站和服务端)
  • 过程:
    • 初始标记(CMS initial mark):标记GC Roots能够直接关联的对象,时间短,需要停顿
    • 并发标记(CMS concurrent mark):从直接关联对象开始遍历整个对象图,耗时最长,不需要停顿
    • 重新标记(CMS remark):通过增量更新修正变动的标记记录,耗时比初试稍长,远比并发短,需要停顿
    • 并发清除(CMS concurrent sweep):清理删除掉标记回收的对象,不需要停顿
  • 初始/重新标记:需要Stop The World,耗时短
    并发标记/并发清除:不需要停顿,与用户线程并发执行,耗时长
  • 优点:并发执行,低停顿
  • 缺点:
    • 吞吐量低(对处理器资源非常敏感,在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量)
    • 无法处理浮动垃圾(Floating Garbage),可能出现 Concurrent Mode Failure,导致Full GC(浮动垃圾:并发清除阶段用户线程运行而产生的垃圾。)因为浮动垃圾的存在,需要预留出一部分内存,所以 CMS 收集器不能像其它收集器那样等待老年代快满(阈值92%)的时候再回收。如果预留的内存不够存放新对象或浮动垃圾时,就会出现并发失败(Concurrent Mode Failure),这时虚拟机将冻结用户线程,临时启用 Serial Old 来回收老年代
    • 标记-清除算法导致大量空间碎片,当老年代空间剩余,无法找到足够大连续空间来分配大对象时,会导致提前触发一次 Full GC

7. G1(Garbage First)收集器

  • 面向服务端应用的垃圾收集器,将Region作为单次回收的最小单元,回收价值最大的Region
  • Region:将堆划分为多个大小相等的独立区域,每一个Region可以扮演不同的角色(Eden/Survivor/Old),还有特殊的Humongous区域(存储大对象),实现Mixed GC
  • 大对象:大小超过了Region容量一半,通过-XX:G1HeapRegionSize设置容量大小(1MB~32MB,2的N次幂),存放在N个连续的Humongous Region,相当于老年代
  • 价值:回收所获得的空间大小以及回收所需时间(通过之前回收经验获得)
  • 根据价值维护优先级列表,每次根据允许的收集时间(-XX:MaxGCPauseMillis),优先回收价值最大的 Region
  • 问题解决:
    • 解决跨Region引用对象问题:每个Region都有一个Remembered Set(双向卡表),记录其它Region对象指向自己的指针,本质是Hash table(Key:其它Region的起始地址,Value:存储卡表索引号的集合)
    • 解决并发阶段用户线程改变对象引用关系的问题:通过原始快照(SATB)
    • 解决并发阶段新创建对象内存分配问题:通过两个TAMS(Top at Mark Start)指针
  • 过程:
    • 初始标记(Initial Marking):标记GC Roots能够直接关联的对象,修改TAMS指针(下一阶段能分配新对象)时间短,需要停顿
    • 并发标记(Concurrent Marking):可达性分析,遍历整个对象图,扫描完成后,再次扫描SATB(变动引用),耗时较长,不需要停顿
      最终标记(Final Marking):处理并发阶段结束后遗留下来的少量SATB记录
    • 筛选回收:对每个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,复制需要回收的Region(回收集 CSet)中的存活对象到空Region中,再回收整个旧Region,需要停顿,多条GC线程并行
      除了并发标记,其余阶段都需要暂停用户线程
  • 优点:
    • 用户指定期望的停顿时间:-XX:MaxGCPauseMillis(默认200ms)
    • 分Region的内存布局,按价值动态确定回收集
    • 空间整合:整体(标记-整理算法),局部(两个Region之间)上看,基于标记-复制算法实现——G1运作期间不会产生内存空间碎片
  • 不足:
    • G1垃圾收集产生的内存占用(Footprint)比CMS高(卡表结构复杂且每个Region都要维护卡表)
    • G1程序运行时的额外执行负载(Overload)比CMS要高 (写屏障操作复杂,除了写后屏障维护卡表,原始快照实现需要写前屏障)
  • 适用:小内存:CMS;大内存:G1(Java堆容量平衡点通常在6GB至8GB之间 )

六、选择合适的垃圾收集器

衡量垃圾收集器的三项指标:内存占用(Footprint)、吞吐量(Throughput)、延迟(Latency)
前两个可以通过硬件性能提高,而延迟不行,所以延迟是垃圾收集器最重视的指标

1. 收集器的权衡

  • 关注指标:
    • 内存占用:客户端应用或嵌入式应用
    • 吞吐量:数据分析或科学计算,需要尽快算出结果
    • 延迟:SLA应用,停顿时间影响服务质量,用户交互体验
  • 硬件设施:硬件规格,处理器,操作系统
  • JDK发行商和版本号

2. 虚拟机及垃圾收集器日志(-Xlog)

  • JDK9以后,HotSpot所有功能的日志,汇总到**-Xlog**参数
  • -Xlog [ : [ selector ] [ : [ output ] [ : [ decorators ] [ : output-options ] ] ] ]
  • Selector(选择器):标签(Tag)、日志级别(Level
    • 标签:某个功能模块的名字(垃圾收集器:gc)
    • 日志级别:从低到高(Trace、Debug、Info、Warning、Error、Off),默认为Info
  • Decorator(修饰器):每行日志附加信息
    • time:当前日期和时间
    • uptime:虚拟机启动到现在经过的时间(s)——System.currentTimeMillis()
    • timemillis:当前时间毫秒数
    • uptimemillis:uptime的毫秒数
    • pid:进程ID
    • tid:线程ID
    • level:日志级别
    • tags:标签集
      默认是uptime、level、tags三种信息

七、内存分配与回收策略

1. 对象优先在Eden分配

  • 大多数情况下,对象在新生代Eden区分配
  • 当Eden区空间不够时,虚拟机将发起一次Minor GC

2. 大对象直接进入老年代

  • 大对象是指需要大量连续内存空间的对象
  • 最典型的大对象:很长的字符串,元素数量庞大的数组
  • 参数-XX:PretenureSizeThreshold(只支持Serial和ParNew收集器):指定大于此值的对象直接在老年代分配,避免在 Eden 区和两个Survivor 区之间的大量内存复制

3. 长期存活的对象进入老年代

  • 为每个对象定义一个对象年龄(Age)计数器(对象头)
  • 对象在Eden出生并经过 Minor GC 后依然存活,将移动到Survivor中,年龄就设为 1 岁,之后每熬过一次Minor GC,年龄就增加1岁,增加到一定年龄(默认15岁)后则移动到老年代中
  • 参数-XX:MaxTenuringThreshold 用来定义年龄的阈值

4. 动态对象年龄判定

  • 对象年龄不是必须达到阈值才能移动到老年代
  • 如果在Survivor中,低于或等于(<=)某个年龄的所有对象大小的总和大于 Survivor空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,不用等到 - XX:MaxTenuringThreshold 中设置的年龄阈值

5. 空间分配担保

  • 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
    • 大于:Minor GC 是安全的
    • 小于:虚拟机会查看 - XX:HandlePromotionFailure 设置值是否允许担保失败
      • 允许:继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小(经验值)
        • 大于:尝试进行一次 Minor GC
        • 小于或者 - XX:HandlePromotionFailure 设置不允许担保失败:进行一次 Full GC
  • JDK6后, - XX:HandlePromotionFailure参数不会影响空间分配担保策略:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则才Full GC
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值