文章目录
对象存活判断
算法
- 引用计数算法:一个地方引用对象,对象的引用计数器加1。这会产生循环引用问题。
- 可达性分析算法:从 GC Roots 开始搜索,所走过的路径称为引用链,如果对象不可达,则对象可回收。 GC Roots 包括以下
- 虚拟机栈中的引用对象。
- 本地方法栈中native方法引用对象。
- 方法区的静态属性引用对象。
- 方法区的常量引用对象。
引用强度
- 当内存空间还足够时,保留;当内存空间GC后任然紧张,抛弃。从而规定不同的引用级别。
- 强引用(Strong Reference):普遍存在,如 Object obj = new Object()。
- 软引用(Soft Reference):生存到下一次内存溢出之前,如果回收之后内存不够,才抛内存溢出异常。
- 弱引用(Weak Reference):生存到下一次GC之前。
- 虚引用(Phantom Reference):不影响生存时间,也无法取得对象实例,唯一作用是使得对象被回收是收到系统通知。
回收判断
- 回收需要经过两次标记过程。
- 第一次标记后,对象不可达时,需要进过筛选,是否需要执行finalize()方法:
- 没有覆盖finalize(),或者已经执行过一次finalize(),则无需执行,放入即将回收的集合。
- 需要执行finalize()方法,放入F-Queue队列,由Finalizer线程执行。对象可在方法中重新建立引用关联,避免被回收。
- 第二次标记,对F-Queue进行。
- 对象的finalize方法只能调用一次,由于运行代价高、不确定性大、无法保证对象的调用顺序,不建议使用。
回收方法区
- 废弃常量:没有其他地方引用这个字面量。
- 无用的类:
- 该类所有实例被回收。
- 加载该类的加载器被回收。
- 该类对应的Class对象没有被引用,无法通过反射访问该类的方法。
- 频繁自定义加载器和动态生成类的场景需要卸载类。
垃圾收集算法
标记-清除
- 最基础的算法。标记需要回收的对象,之后统一回收。
- 标记和清除效率不高,清除之后内存不连续。
复制
- 针对效率。将存活的对象复制到另一块内存上。
- 实现简单,运行高效,但是有效内存缩小,空间换时间。
- 将新生代划分为一块较大的Eden区和两块较小的Survivor区,HotSpot默认比例为8:1。每次使用一个Eden和一个Survivor,回收时将Eden和Survivor区中存活对象复制到另一个Survivor区中。如果另一个Survivor区没有足够的内存空间,这些对象将直接进入老年代,这是老年代的分配担保。
标记-整理
- 针对内存空间。先标记对象,然后将存活对象向一端移动,之后清理边界以外的内存。
分代收集
- 新生代:每次GC只有少量对象存活,使用复制算法。
- 老年代:每次GC对象存活率高,没有额外空间作为保证,使用标记-清除或标记整理算法。
HotSpot实现判断存活和垃圾回收
枚举根节点
- 枚举根节点是必须停顿所有的java执行线程,称为 Stop The World。
- 为了无需检查所有执行上下文和全局引用位置,提高效率,使用 OopMap 得知哪些地方存在对象的引用。
安全点
- 为每一条指令生成OopMap开销大。
- 程序在特定的位置记录OopMap,然后停顿开始GC,这个位置称为安全点。
- 停顿方式分为2种:
- 抢先式中断:中断所有线程,恢复不在安全点的线程,运行至安全点后中断。几乎不用。
- 主动式中断:线程轮询中断标志,如果为真,中断线程。
安全区域
- 线程没有分配CPU时间时,则无法在安全点挂起。
- 在某一区域内,引用关系不会发生变化,这一区域任意地方GC都是安全的,称为安全区域。
- 线程在安全区内进行GC,无需关心,离开安全区时,需要等待GC完成。
垃圾收集器
- 垃圾收集器是内存回收的具体实现。共7种。
Serial收集器
- 单线程收集器,需要将用户正常工作的线程全部停止。
- 简单高效,没有线程交互的开销。客户端新生代内存不大,停顿可接受。
- 虚拟机Client模式下默认的新生代收集器。
ParNew收集器
- Serial收集器的多线程版本。
- 由于线程交互的开销,单核性能不如Serial,多核性能较高。
- 虚拟机Server模式下默认的新生代收集器,匹配CMS收集器。
并行(Parallel):多条垃圾收集线程并行工作,用户线程等待。
并发(Concurrent):垃圾收集线程和用户线程交替执行,也可能在不同CPU上同时执行。
Parallel Scavenge收集器
- 使用停止复制算法的多线程新生代收集器,关注可控制的吞吐量(运行代码时间/(运行代码时间+垃圾收集时间))。
- 可直接控制最大垃圾收集停顿时间和吞吐量大小,也可自适应调节。
Serial Old收集器
- Serial收集器的老年代版本,使用标记-整理算法。
- 用途:
- jdk1.5之前和Parallel Scavenge收集器搭配。
- CMS收集器的后备,当并发收集遇到失败时使用。
Parallel Old收集器
- Parallel Scavenge收集器的老年代版本,注重吞吐量。
CMS收集器
- CMS(Concurrent Mark Sweep)收集器,获取最短的停顿时间,提高响应速度。
- 基于标记-清除,分为4个部分。其中并发标记和并发清理耗时较长,但是与用户进程并发,效率高。
- 初始标记: GC Roots 能直接关联到的对象。
- 并发标记:并发搜索可达对象。
- 重新标记:修正并发标记期间产生变动的对象的标记。
- 并发清理。
- 缺点:
- 对CPU资源敏感。并发过程占用CPU,使得用户线程变慢。
- 无法收集浮动垃圾。并发清理时会有新的垃圾产生,需要在老年代中预留一定比例空间存储清理时新产生的老年代对象。如果预留的内存无法满足要求,会 Concurrent Mode Failure,从而启动Serial Old收集器。
- 空间碎片。标记-清除内存空间不连续,可以在CMS收集器效果不佳时进行Full GC,整理内存碎片。
G1收集器
- 面向服务端应用的垃圾收集器。特点如下:
- 并行和并发。
- 分代收集:分为新生代和老年代。
- 空间整合:在两个Region之间使用复制算法,从整体上看,使用整理算法。
- 可预测停顿:规定垃圾收集的时间上限。
- 整个堆分为多个大小相等的Region,新生代和老年代不物理隔离,都是多个Region的集合。
- G1跟踪各个Region回收的价值和成本大小,可以按照优先级回收空间,并且限制回收时间。
- 使用 Remembered Set 管理Region之间的对象引用,从而避免每次回收进行全堆扫描。每一个 Region具有一个Remembered Set,每当对reference类型的数据进行写操作时,检查该对象是否处于不同的Region,如果不同,将相关引用信息记入被引用的Region对应的Remembered Set。进行内存回收时,GC Roots中加入Remembered Set从而避免全堆扫描。
- 类似CMS收集器,分为4个部分。
- 初始标记: GC Roots 能直接关联到的对象。
- 并发标记:并发搜索可达对象。
- 最终标记:修正并发标记期间产生变动的对象的标记。
- 筛选回收:对各个Region的回收价值和成本进行分析,根据期望的回收时间实行回收计划。
垃圾收集器参数
内存分配策略
- JVM-彻底搞懂 逃逸分析&标量替换
- 逃逸分析后,如果可标量替换,分配在栈上。
- 大对象直接进入老年代。典型的是很长的字符串或者大数组。
- 小对象优先分配在Eden。Eden没有足够空间,虚拟机进行一次Minor GC。
- 长期存活的对象将进入老年代。每个对象都有一个年龄计数器。
- 对象在Eden出生并经过一次Minor GC后存活,并且能被Survivor容纳,将进入Survivor。
- 在Survivor每经过一次Minor GC,年龄加1,年龄增加到阈值(默认15),则进入老年代。
- 年龄动态判定。如果Survivor中相同年龄的所有对象大小总和大于Survivor的一半,大于等于该年龄的对象直接进入老年区。
空间分配担保:
- 在Minor GC之前,老年代进行担保。
- 老年代最大可用的连续空间大于新生代所有对象的总空间,或者历次晋升到老年代对象的平均大小,则进行 Minor GC。否则Full GC。
逃逸分析
- 逃逸状态:
- 全局逃逸。对象的作用范围超过当前方法或线程,如静态变量、已逃逸对象、当前方法返回值。
- 参数逃逸。对象作为方法参数传递。
- 没有逃逸。
- 优点:
- 分配在栈上——标量替换
- 锁消除——同步消除
- 缺点:
- 必须在JIT中完成。需要收集足够多的数据才能判断是否逃逸。
标量替换
- 标量:基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等)
- 聚合量:可进一步分解的量,如Java对象
- 替换前提:逃逸分析后确定不会被外部访问,且可以进一步分解
- 替换步骤:JVM不创建该对象,而是将该对象成员变量分解成若干个被这个方法使用的成员变量,这些代替的成员变量在栈帧或寄存器上分配空间
- jdk7会后默认开启
同步消除
- 如果对象没有出现线程逃逸,那该对象的读写就不会存在资源的竞争,不存在资源的竞争,则可以消除对该对象的同步锁。