1. 堆内存
Java 堆是垃圾收集器管理的主要区域,所以也叫做 GC 堆 Garbage Collected Heap
。堆的空间结构可以分为 Eden
、From Survivor
、To Survivor
、Old Memory
等
- 大部分的对象都会在
Eden
区域分配,特别大的对象会直接进入老年代 - 在一次垃圾回收后,如果对象还活着,则会进入
s0
或者s1
,并且对象的年龄还会+1
- 当年龄加到一定程度,默认 15 或者某个年龄大小超过
Survivor
区的一半,去这两个中的更小值,会晋升为老年代。-XX:MaxTenuringThreshild
2. 判断对象已死亡
引用计数法
- 给对象添加一个引用计数器,每当有一个地方引用它,计数器就
+1
;引用失效就-1
,0
就是不可能被使用 - 实现简单,效率高,但是目前主流虚拟机中并没有选择这个算法来管理内存,最主要的原因是它很难解决对象之间的相互循环引用
可达性分析
- 基本思想就是用过一系列称为
GC Roots
的对象作为起点,从这些节点开始向下搜索,节点走过的路径称为引用链,当一个对象到GC Roots
没有任何引用链相连的话,则证明此对象是不可用的。 - 对象不可达并不代表是非死不可的,真正宣告一个对象死亡,至少要经历两次标记过程
- 第一次标记筛选的条件是此对象是否有必要执行
finalize
方法 - 当对象没有覆盖
finalize
方法或者已经被调用过了,就视为没有必要执行 - 如果判定需要执行的对象被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被回收
废弃常量
- 假如常量池中存在字符串
"abc"
,如果当前没有任何String
对象引用该字符串常量,就说明"abc"
就是废弃常量,这个时候如果发生内存回收的话就会将其清理出常量池
无用的类
- 该类的所有实例已经被回收
- 该类的
ClassLoader
已经被回收 - 该类对应的
Class
对象没有在任何地方被引用,也就是无法通过反射访问该类的方法 - 满足这些条件也不是说一定会被回收
GC Roots
- 虚拟机栈(栈中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 Native 方法(JNI)引用的对象
3. 对象引用
强引用 Strong
- 最普遍的引用,使用的引用大部分都是强引用,如果一个对象有强引用,垃圾回收器就绝不会去回收它。
- 内存空间不足时,宁愿 OOM,也不会去回收强引用对象
软引用 Soft
- 如果内存空间够就不会回收它,内存不够就会回收
- 可以和引用队列联合使用,如果软引用被回收,虚拟机就会把软引用加入到与之无关的引用队列
弱引用 Weak
- 相比于软引用,弱引用生命周期更短
- 垃圾回收器扫描到了弱引用的对象,不管内存空间够不够,都会回收
- 不过垃圾回收器是一个优先级很低的线程,不一定会很快发现弱引用对象
- 也可以和引用队列联合使用
虚引用 Phantom
- 就和没有引用一样,随时都可能被垃圾回收
- 主要用来跟踪对象被垃圾回收的活动
程序设计中一般很少使用弱引用和虚引用,一般使用软引用,用来加速 JVM 对垃圾内存的回收速度,可以维护系统安全,防止 OOM 等。
4. 垃圾收集算法
标记清除
- 最基础的收集算法
- 效率也不够高
- 会产生大量的不连续碎片
标记整理
- 根据老年代的特点提出的标记算法
- 类似于标记清除,只不过不是直接回收可回收对象
- 是让所有存活对象向一端移动,直接清理掉端边界以外的内存
复制算法
- 解决效率问题
- 把内存分成大小相同的块,这一块内存使用完后,将还存活的对象复制到另一块去
- 再将使用的空间一次清理掉
- 这样每次内存回收都是堆内存区间的一半进行回收
分代收集算法
- 当前的虚拟机都采用分带收集算法,是根据对象存活周期将内存分为几块
- 然后不同的年代使用不同的垃圾收集算法
5. 垃圾收集器
- 新生代:
Serial
、ParNew
、ParallelScavenge
、G1
- 老年代:
CMS
、Serial Old
、Parallel Old
、G1
Serial
- 单线程的垃圾收集器
- 会 STW,只适用于小的
Client
应用 - 在
Client
模式下,新生代默认的收集器
ParNew
- 是
Serial
的多线程版本,其它基本一致 - 主要是使用在
Server
模式 - 多线程让垃圾回收更快,也就是减少了 STW 的时间
- 除了
Serial
,只有它能和CMS
配合
Parallel Scavenge
- 也是一个使用复制算法,多线程的收集器
- 和其他收集器的关注点不同,其他收集器关注的是停顿时间,
Parallel Scavenge
的目的是吞吐量 - 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
- 适合不需要太多用户交互的任务
- 参数:
-XX:MaxGCPauseMillis
最大垃圾收集时间-XX:GCTimeRatio
默认 99%,吞吐量大小-XX:UseAdaptiveSizePolicy
,自适应策略,开启后不需要手工指定新生代大小,比例之类的,只需要设置堆大小(-Xmx
),最大垃圾收集时间和吞吐量,虚拟机就会根据系统运行情况收集监控信息,去动态的调整这些参数
Serial Old
- 是工作于老年代的单线程收集器,也是主要给
Client
使用的 - 如果在
Server
模式下:- 在 JDK 1.5 及之前的版本中与 Parallel Scavenge 配合
- 作为 CMS 的后备预案,在并发收集发生
Coucurrent Mode Failure
时使用
Parallel Old
- 是
Parallel Scavenge
的老年版本,使用标记整理和多线程 - 以吞吐量为目的
CMS
- 以实现最短时间的
STW
为目的的收集器,希望给用户最好的体验 - 采用标记清除,主要步骤:初始标记 => 并发标记 => 重新标记 => 并发清除
- 初始标记和重新标记会触发
STW
,造成用户线程挂起,不过初始标记仅标记GC Roots
能关联的对象,速度很快 - 并发标记是进行
GC Roots Tracing
的过程 - 重新标记是为了修正并发标记期间因为用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,这一段比初始标记长,远比并发标记短
- 整个过程中耗时最长的是并发标记和标记清理,不过这两个阶段用户线程都可以工作,不影响应用的正常使用
- 缺点:
- CMS 对 CPU 资源很敏感,CMS 默认启动的回收线程数是
(CPU 数 + 3) / 4
,如果 CPU 数量只有一两个,吞吐量会直接下降很多 - CMS 无法处理浮动垃圾
Floating Garbage
,可能出现Concurrent Mode Failure
而导致另一次FULL GC
,由于并发清理阶段用户线程还在运行,所以清理的同时垃圾也在不断出现,这部分垃圾只能在下一次 GC 时处理;而且还需要预留足够多的空间要确保用户线程正常执行,这也就导致了 CMS 不能像其他收集器一样等老年代满了再使用,从 1.5 开始默认时老年代使用了 68%,可以使用-XX:CMSInitiatingOccupancyFraction
来设置,如果设置太高容易导致在 CMS 运行运行期间预留的内存无法满足内存需求,会导致Concurrent Mode Failure
失败,这时会启用Serial Old
收集器来重新进行老年代的收集,这样会导致STW
更长了 - 采用的是标记清除算法,会有内存碎片,如果无法找到足够大的连续空间来分配对象,会触发
Full GC
,会影响性能,可以开启-XX:+UseCMSCompactAtFullCollection
,默认是开启的用于在 CMS 收集器顶不住要Full GC
时开启内存碎片的合并整理,会导致STW
,停顿时间会边长,还有个参数-XX:CMSFullGCBeforeCompation
用来设置执行多少次不压缩的Full GC
后跟着带来一次压缩的
- CMS 对 CPU 资源很敏感,CMS 默认启动的回收线程数是
G1 (Garbage First)
- 面向服务端的收集器,被称为驾驭一切的垃圾回收器:
- 像 CMS 收集器一样,能与应用程序线程并发执行
- 整理空闲空间更快
- 需要 GC 停顿时间更好预测
- 不会像 CMS 那样牺牲大量的吞吐性能
- 不需要更大的 Java Heap
- 运行期间不会产生内存碎片,G1 从整体上看采用的是标记-整理,局部(两个 Region)上看是基于复制算法实现的,两个算法都不会产生内存碎片
- 在 STW 上建立了可预测的停顿时间模型,用户可以指定期望的停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内
G1 能建立可预测的停顿模型的原因:
- G1 对堆空间的分配与传统的垃圾收集器不一样,传统的是连续的新生代,老年代
- 而 G1 各代的存储地址不是连续的,每一代都使用了 n 个不连续的大小相同的
Region
,每个Region
占有一段连续的虚拟内存地址 - 除此之外
Region
还多了一个 H,代表Humongous
,这表示这些Regoin
存储的是巨大对象,即大小>=
Region
一半的对象,这样超大对象就直接分配到了老年代,防止了反复拷贝移动 - 传统的
Full GC
是对堆进行全区域的垃圾收集,而分配成各个Region
的话,方便G1
跟踪各个Region
里垃圾堆积的价值大小(回收空间大小及回收所需经验值),这样根据价值大小维护了一个优先列表,根据允许的收集时间,优先收集回收价值最大的Region
,就避免了整个老年代的回收,也就减少了STW
的停顿时间 - 由于可以只收集部分
Region
,就可以做到STW
时间可控 - 步骤:初始标记 => 并发标记 => 最终标记 => 筛选回收