垃圾回收机制
总体思路
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
判断对象存活的方法
- 引用计数法
- 思想:给对象设置引用计数器,每引用该对象一次,计数器就+1,引用失效时,计数器 -1。当任意时候,引用计数器的值为0时,则该对象可以回收
- java不适用原因:无法解决对象之间互相循环引用的问题
可达性分析法
- 以GC Roots为起点,从这些起点向下搜索,经过的路径称为引用链。若一个对象到GC Roots 之间没有任何引用链,则该对象是不可达的(该对象可以回收)
- 可以作为GC Roots的对象有
– 虚拟机栈(栈中的局部变量表)中引用的对象
– 方法区中静态属性引用的对象
– 方法区中常量引用的对象
– 本地方法栈中Native方法引用的对象
- 在可达性性分析过程中,对象引用类型会对对象的生命周期有影响,Java中有这几种类型的引用:
- 强引用:只要该引用还有效,GC就不会回收
- 软引用:内存空间足够时不进行回收,在内存溢出发生之前进行回收,用SoftReference类实现
- 弱引用:弱引用关联的对象只能活到下一次GC收集,用WeakReference类实现
- 虚引用:无法通过虚引用获得对象实例,也不会对对象的生存时间产生影响。唯一目的就是当该对象被GC收集时,收到一个系统通知。用PhantomReference类实现
判断对象存活的步骤
- 首先进行可达性分析,筛选出与GC Roots没有引用链的对象,进行第一次标记
- 第一次标记后,再进行一次筛选,筛选的条件是是否有必要执行finalize()方法。若对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机执行过一次了,则GC会回收该对象
- 若有必要执行,则该对象会被放入F-Queue中,由JVM开启一个低优先级的线程去执行它(但不一定等待finalize方法执行完毕)
- Finalize()方法是对象最后一次自救的机会,若对象在finalize()中重新加入到引用链,则它会被移出要回收的对象的集合。其他对象会被第二次标记,进行回收
Java中的垃圾回收算法:(内存回收的方法论)
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
- 标记 - 清除(Mark-Sweep)
- 两个阶段:标记、清除
- 缺点:两个阶段的效率都不高;容易产生大量的内存碎片
- 复制(Copying)
- 把内存分成大小相同的两块,当一块的内存用完了,就把可用的对象复制到另一块上,将使用过的那一块一次性清理掉
- 缺点:浪费了一半内存
- 标记 - 整理(Mark-Compact)
- 标记后,让所有存活的对象移动到一端,然后直接清理掉端边界以外的内存
- 分代收集
- 把对象分成新生代和老年代
- 新生代特点:每次垃圾收集的都有大批对象死去,只有少量存活
- 老年代特点:对象存活率高、没有额外空间对它进行分配担保
- 新生代使用复制算法
- 将新生代内存分为一块大的Eden区和两块小的Survivor;每次使用Eden和一个Survivor,回收时将Eden和Survivor存活的对象复制到另一个Survivor,回收时,将Eden和Survivor存活的对象复制到另一个Survivor(HotSpot的比例Eden:Survivor=8:1)
- 老年代使用标记 - 清除和标记 - 整理
垃圾收集器(内存回收的具体实现)
- Serial(串行收集器)
- 特性:单线程,stop the world ,采用复制算法
- 应用场景:JVM在Client模式下默认的新生代收集器,因为该模式下,新生代的内存较小,停顿时间相对较短
- 优点:简单高效
- ParNew
- 特点:是Serial的多线程版本,采用复制算法
- 应用场景:Server模式下常用对的新生代收集器,可与CMS配合工作
- Parallel Scavenge
- 特点:并行的多线程收集器,采用复制算法,吞吐量优先,有自适应调节策略
- 吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值
- 应用场景:需要吞吐量大的时候 (后台计算)
- SerialOld
- 特点:Serial的老年代版本,单线程,使用标记 - 整理算法
- Parallel Old
- 特点:Parallel Scavenge的老年代版本,多线程,标记 - 整理算法
- CMS
- 对CPU资源敏感
- 无法处理浮动垃圾(并发清除时,用户线程仍在运行,此时产生的垃圾为浮动垃圾)
- 产生大量的空间碎片
- 初始标记:stop the world;标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing
- 重新标记:stop the world;修正并发标记期间因用户程序继续运作而导致产生变动的那一部分对象的标记记录
- 并发清除:清除对象
- 特点:以最短回收停顿时间为目标,使用标记 - 清除算法
- 过程:
- 优点:并发收集,低停顿
- 缺点:对CPU资源非常敏感;无法处理浮动垃圾;会产生大量内存碎片
- G1
- 初始标记:stop the world;标记GC Roots能直接关联到的对象
- 并发标记:可达性分析
- 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
- 筛选回收:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所七万给的GC停顿时间来制定回收计划
- 并行与并发
- 分代收集
- 空间整合:从整体上来看,是基于标记 - 整理算法;从局部来看,是基于复制算法的。不会产生内存碎片
- 可预测的停顿:使用者可明确制定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
- 特点:面向服务端应用。将整个堆划分为大小相同的region
- 执行过程:
- GC自适应调节策略 Parallel Scavenge收集器有一个参数-XX:+UseAdapativeSizePolicy。当打开这个参数后,就不需要手动制定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)
内存分配规则
对象主要分配在Eden,若启动了本地线程分配缓冲,将优先在TLAB上分配
- 对象优先在Eden上分配
- 当Eden区没有足够的空间时就会发起一次Minor GC
- 大对象直接进入老年代
- 典型的大对象是很长的字符串和数组
- 长期存活的对象进入老年代
- 每个对象都有年龄计数器,每经过一次GC,计数器值加1,当到达一定的程度后(默认15),就会进入老年代
- 年龄的阈值可通过参数 -XX:MaxTenuringThreshold设置
- 对象年龄的判断
- Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于等于该年龄的对象就可直接进入老年代,无须等到MaxTenuringThreshold要求的年龄
- 空间分配担保
- 发生Minor GC钱,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若大于,则Minor GC是安全的
- 若不大于,JVM会查看HandlePromotionFailure是否允许担保失败,若不允许,则改成为一次Full GC
- 若允许担保失败,则检查老年代最大可用的连续空间是否大于每次晋升到老年代对象的平均大小,若大于,则尝试进行Minor GC;若小于,则要改为Full GC
内存分配并发问题
在创建对象的时候有个很重要的问题就是线程安全。因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机,必须保证线程是安全的,通常来讲,虚拟机采用两种方式来保证虚拟机线程安全:
- CAS+失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁即时,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,知道成功为止。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
- TLAB:为每一个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或者TLAB中的内存已经用尽时,采用上述的CAS进行内存分配
方法区回收
- 永久带中需要回收两部分内容:废弃常量和无用的类
- 废弃常量回收和对象的回收类似
- 无用的类需满足3个条件
- 该类的所有实例对象已被回收
- 加载该类的ClassLoder已被回收
- 该类的Class对象没有在任何地方被引用,无法在任何地方通过反射访问到该类的方法