- 哪些内存需要回收
- 什么时候回收
- 如何回收
Java堆和方法区的垃圾回收
对象是否存活
引用计数算法
给对象添加一个引用计数器,当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为的对象就是不可能再被使用的
- 很难解决对象之间相互循环引用的问题;
可达性分析算法
通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链,当一个对象对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
可作为GC Roots的对象
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
四种引用
- 强引用:只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
- 软引用(SoftReference)
- 还有用但并非必须的对象
- 被软引用关联的对象,将在内存发生溢出前,把对象列进回收范围中进行二次回收。如果这次回收还没有足够的内存,将抛出内存溢出异常
- 弱引用(WeakReference)
- 只能生存到下一次垃圾收集发生前,无论内存是否足够
- 虚引用(PhantomReference)
- 为对象设置虚引用关联是能在这个对象回收时收到系统通知
回收方法区
- 回收废弃常量
- 系统没有任何一个地方引用到的常量
- 无用类:同时满足以下条件(可以被回收,需要虚拟机具备卸载功能)
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类任何实例
- 加载该类的ClassLoader已被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾收集算法
标记-清除算法
先标记所有需要回收的对方,标记完后同一回收被标记对象
- 不足
- 效率不高
- 空间问题:碎片化
复制算法
解决效率问题
- 将可用内存按容量分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,再清理已使用过的空间
- 好处:内存分配不需要考虑内存碎片
- 弊端:内存缩小为原来的一半
- 通常分为Eden和两个Survivor;Survivor不够分配担保
标记-整理算法
解决复制算法在对象存活率较高时要进行较多复制,效率会变低;不想浪费50%控件需要有额外的控件进行分配担保,不适合老年代
- 先标记,让所有存活的对象向一端移动,然后清理端边界外的内存
分代收集算法
- 根据各个年代特点采用适当的收集算法
- 新生代:每次垃圾收集都有大批对象死去,选用复制算法
- 老年代:对象存活率高、没有额外空间进行分配担保,使用“标记-清除”、“标记-整理”
HotSpot算法实现
枚举根节点
- GC Roots弊端
- 消耗大量时间
- GC停顿
- 当系统停顿后,不需要一个不漏的检查所有执行上下文和全局的引用位置,虚拟机应知道哪些地方存放着对象引用
- HotSpot使用一组OopMap的数据结构实现;当类加载完,把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。GC在扫描时可直接获得这些信息
安全点
- OopMap弊端
- 引用关系变化、OopMap内容变化的指令非常多时,如果为每条指令都生成对应的OopMap,需大量的额外空间,GC空间成本升高
- 安全点
- 生成OopMap的点;
- 程序执行时并非在所有地方都能停顿下载开始GC,只有到达安全点才能暂停
- Safepoint太少GC等待时间长;太多增大运行负荷
- 选定是以程序“是否具有让程序长时间执行的特征”
- “长时间执行” 指令序列复用,方法调用、循环跳转、异常跳转等
- 如何在GC发生时让所有线程(除JNI调用的线程)都“跑”到最近安全点停顿
- 抢先式中断
- 不需要线程的执行代码配合,GC时把所有线程全部中断
- 发生GC时,先中断全部线程,有线程中断的地方不在安全点就恢复线程,让它“跑”到安全点
- 主动式中断
- GC需要中断线程时不直接对线程操作,先设置标志,各线程执行时轮询这个标志,中断标志为真就自己中断挂起
- 轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方
- 抢先式中断
安全区域
解决程序没有分配CPU时间(Sleep状态或Blocked状态),程序无法响应JVM的中断请求,JVM不会登台线程重新分配CPU时间
- 指在一段代码片段中,引用关系不会发生变化
- 执行执行到Safe Region中的代码时,先标识,GC不用管标识Safe Region状态的线程。
- 线程要离开Safe Region先检查是否完成根节点枚举(或整个GC),完成了继续执行,未完成等待可以离开Safe Region的信号
垃圾收集器
- Young generation
- Serial
- ParNew
- Parallel Scavenge
- Tenured generation
- CMS
- Serial Old(MSC)
- Parallel Old
- G1
Serial收集器
- “单线程”:必须停止其他所有的工作线程知道收集结束
- 简单高效(与其他收集器的单线程比),对于限定单个CPU的环境,Serial收集器没有线程交互的开销
- Client
ParNew收集器
- Serial多线程版
- Server
- 能与CMS配合
并发和并行
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU上
Parallel Scavenge收集器
- 使用复制算法,多线程
- 达到一个可控制的吞吐量
- 吞吐量:CPU运行用户代码时间与CPU总消耗时间的比;吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
- 高吞吐量适合在后台运算不需要太多交互的任务
Serial Old收集器
- Serial老年代版
- 标记-整理算法
- 用途
- 主要用于Client
- Server
- JDK 1.5前与Parallel Scavenge配合
- CMS后备预案,并发收集发生Concurrent Mode Failure时使用
Parallel Old收集器
- Parallel Scavenge老年版
- 标记-整理算法
- 注重吞吐量及CPU资源敏感的场合有限考虑 Parallel Scavenge + Parallel Old
CMS收集器
- 以获取最短回收停顿时间为目标的收集器
- 标记-清除算法
- 四个步骤
- 初始标记:标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程
- 重新标记:修正并发标记旗舰因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
- 并发清除
- 初始标记和并发标记需要“Stop The World”。
- 由于并发标记和并发清除耗时最长,与用户线程并发执行
- 缺点
- 对CPU资源非常敏感
- 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”导致Full GC
- 浮动垃圾:出现在标记过程之后,CMS无法档次收集中处理,留到下一次GC
- 由于用户线程和GC同事运行,需预留足够内存给用户线程使用
- 由于基于“标记-清除”算法实现,GC后会有大量碎片
G1收集器
- 并行与并发
- 分代收集
- 整合空间:基于“标记-整理”算法
- 可预测的停顿
G1将整个Java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物流隔离的了,他们都是一部分Region(不需要连续)的集合。 G1收集器之所以能简历可预测的停顿时间模型,是因为它可以有计划的避免在这个Java堆中进行安全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 弊端
- Region不可能是孤立的,一个对象分配在某个Region中,它并非只能被本Region中的其他对象引用,而可以与整个Java堆任意的对象发生引用关系。
- G1收集器中,REgion之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描。
- G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,后产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否与老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏
- 如果不计算维护Remembered Set的操作,G1收集器运作分以下几步
- 初始标记:标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但好使很短。
- 并发标记:从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
- 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在该线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
- 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
内存分配与回收策略
对象优先在Eden分配
- 大多数情况,对象在新生代Eden中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
Minor GC和Full GC区别
- 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上
大对象直接进入老年代
- 大对象:需要连续空间的Java对象,(很长的字符串或数组)
- 经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。
- 设置参数令大于设置值的对象直接在老年代分配。避免在Eden区及两个Survivor区之间发生大量的内存复制
长期存活的对象将进入老年代
- 年龄计数器:如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当它的年龄增加到一定成都(默认15),将被晋升到老年代中。
动态对象年龄判定
- 为了能更好适应不同程序的内存状况,虚拟机并不是用于地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor空间中的相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄
空间分配担保
- 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否是大于新生代所有对象总空间,如果条件成立,MinorPro GC可以确保是安全的。不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,将进行Full GC
- 冒险:新生代采用复制收集算法,但为了内存利用率,只使用那个其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间