文章目录
如何判断对象可以被回收?
1.引用计数
-
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。
-
引用计数法虽然看似合理,但在实际情况下难以稳定工作,需要配合大量的额外处理。比如两个对象相互引用,但是这两个对象在程序中已经失效,使用引用计数法来判断对象是否存活的话,这两个无用对象永远无法被回收。
-
在主流的Java虚拟机中,从来没有使用这种方式
2. 可达性分析(Reachability Analysis)
-
是指从一些名为
GC Roots
的对象(GC Root Set)开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,是不可达对象 -
那么,不难发现,我们又引入了一个问题:哪些对象才能被定义为GC Roots?这些内容暂时只要了解一下就好,固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,比如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,比如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
-
除了这些固定的可作为GC Roots的对象之外,也会临时性地加入一些对象。所以GC Roots的数目并没有想象的那么少,后面也会提到,如何去优化对这些对象的枚举。
-
目前主流的Java垃圾回收器,就是通过可达性分析来进行对象存活判断的。
GC算法和GC收集器
- GC 算法是内存回收的方法论,垃圾收集器就是算法的落地实现。
- 目前为止还没有完美的收集器的出现,更加没有万能的收集器,只是针对具体应用最适合的收集器,进行分代收集。
Serial(串行)收集器
- 作用范围:新生代
- 采用复制算法
- 单线程
- 是JVM在clien模式下默认的新生代垃圾收集器
- 缺点:必须暂停其他所有的工作线程(“Stop The World”),可能会产生较长的停顿
适用场景:弱交互,科学计算/大数据处理场景,不适合服务器环境
ParNew(Parallel New Generation)收集器
- Java8默认的收集器
- 作用范围:新生代
- 采用复制算法
- 多线程,其实就是Serial收集器的多线程版本。
- -XX:ParallelGCThreads,配置GC线程数量,默认线程数=cpu数量
- 一般和CMS收集器配合工作
Parallel Scavenge收集器
- 类似ParNew收集器
- 也叫PSYoungGen
- 采用复制算法
- 作用范围:新生代
- Parallel收集器更关注系统的吞吐量,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务
- 和ParNew的一个重要区别:自适应调节策略,根据当前系统运行情况,动态调整停顿时间或吞吐量
- 在Java8中和Parallel Old收集器配合使用
- 配置:-XX:UseParallelGC或-XX:UseParallelOldGC
- 适用场景:主要适合在后台运算而不需要太多交互的任务。
Serial Old收集器
- Serial收集器的老年代版本
- 单线程收集器
- 使用“标记-整理”算法。
- Java 8 以后基本不使用 Serial Old
- 是JVM在clien模式下默认的老年代垃圾收集器
是CMS的备用收集器
Parallel Old 收集器
- 也叫ParOldGen
- 是Parallel Scavenge收集器的老年代版本
- 多线程
- 使用标记-整理算法
并发垃圾回收器(CMS)
- 英文:Concurrent Mark Sweep
- 以获取最短回收停顿时间为目标。
- 适用场景:对响应速度有较高要求的场景,堆内存大,cpu核数多的服务端应用
- 是G1出现前大型应用的首选收集器
- 基于“标记-清除”算法
- 开启参数:-XX:+UseConcMarkSweepGC,开启该参数后会自动将-XX:+UseParNewGC打开,同时,Serial Old收集器将作为CMS出错后的备用收集器
- 优点:
- 并发收集、低停顿
- 缺点:
- 对CPU资源非常敏感。当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大
- 由于CMS和应用进程并发进行,会增加堆内存的占用,CMS必须在老年代堆内存用尽之前完成GC,否则会失败,触发备用的Serial Old,以STW的方式进行GC,造成较长 的停顿时间
- CMS是基于“标记-清除”算法,这意味着收集结束时会有大量空间碎片产生
- 标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐渐耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数 -XX:CMSFullGCsBeForeCompaction (默认0,即每次都进行内存整理) 来指定多少次 CMS 收集之后,进行一次压缩
- 无法处理浮动垃圾(Floating Garbage)。可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。
CMS 处理过程有4个步骤:
- 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
- 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。与用户线程同时运行
- 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。需要“Stop The World”。
- 并发清除(CMS concurrent sweep):与用户线程同时运行
查看服务器默认垃圾收集器
- Java -XX:+PrintCommandLineFlags
参数说明
- DefNew : Default New Generation
- Tenured : Old
- ParNew : Parallel New Generation
- PSYoungGen : Parallel Scavenge
- ParOldGen : Parallel Old Generation
JVM的Server/Client 模式
- JVM Server模式与client模式启动,最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。 JVM工作在Server模式下可以大大提高性能,Server模式下应用的启动速度会比client模式慢大概10%,但运行速度比Client VM要快至少有10倍
如何选择垃圾收集器
- 单 CPU 或者小内存,单机程序:-XX:UseSerialGC (基本已过时)
- 多 CPU 需要最大吞吐量,如后台计算型应用:-XX:UseParallelGC 或者 -XX:UseParallelOldGC
- 多 CPU 追求低停顿时间,需要快速响应,如互联网应用:-XX:+UseConcMarkSweepGC
G1 垃圾收集器
以前收集器的特点
- 年轻代和老年代是各自独立且连续的内存块
- 年轻代收集器使用 eden + S0 + S1 进行复制算法
- 老年代收集必须扫描整个老年代区域
- 都是以尽可能的少而快速地执行 GC 为设计原则
G1 是什么
- 英文:Garbage-First
- 是一款面向服务器的垃圾收集器, 主要针对配备多颗处理器及大容量内存的机器. 满足GC停顿时间要求的同时,还具备高吞吐量
- G1 是在 2012 年才在 jdk.1.7u4 中可以呀用,在 jdk9 中将 G1 变成默认垃圾收集器来代替 CMS。它是以款面向服务应用的收集器。
- 主要改变是 Eden、Survivor 和 Tenured 等内存区域不再是连续的,而是变成了一个个大小一样的 region(默认是512K),每个 region 从 1M 到 32M 不等,一个 region 有可能属于 Eden、Survivor 或者 Tenured 内存区域。逻辑上保留新生代和老年代,但不再物理隔离,而都是一部分Region(不需要连续)的集合。每个分区都可能随 G1 的运行在不同代之间前后切换。
- Region逻辑上连续,物理内存地址不连续。同时每个Region被标记成E、S、O、H,分别表示Eden、Survivor、Old、Humongous。其中E、S属于年轻代,O与H属于老年代。
- H表示Humongous。从字面上就可以理解表示大的对象(下面简称H对象)。当分配的对象大于等于Region大小的一半的时候就会被认为是巨型对象。H对象默认分配在老年代,可以防止GC的时候大对象的内存拷贝。通过如果发现堆内存容不下H对象的时候,会触发一次GC操作。
使用
- 配置开启:-XX:+UseG1GC
- 设置最大GC停顿时间:-XX:MaxGCPauseMillis=n 这是一个软指标(soft goal), JVM 会尽量去达成这个目标
- 设置region大小:-XX:G1HeapRegionSize=n
- 参考
优点
- G1可以独立管理整个GC堆
- 避免全内存扫描,只需要按照区域来进行扫描即可。
- G1从整体来看是基于"标记-整理"算法,从局部(两个Region之间)上来看是基于"复制"算法。这意味着G1运行期间不会产生内存空间碎片
- 建立可预测的停顿时间模型 。这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型。用户可以指定期望的停顿时间
避免全堆扫描的原理
- Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。
- 为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。
- 虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。
- 当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏
G1收集器步骤(不计算维护Remembered Set的操作)
- 初始标记(Initial Marking): 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
- 并发标记(Concurrent Marking): 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
- 最终标记(Final Marking) :为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
- 筛选回收(Live Data Counting and Evacuation): 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。