垃圾收集(Garbage Collection,GC),哪些内存需要回收?什么时候回收?如何回收?
一,对象在内存中的状态
当一个对象在堆内存中运行时,可分为三种状态:
1.可达状态
当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法
2.可恢复状态
程序中某个对象不再有任何引用变量引用它,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的finalize方法进行资源清理,如果系统在调用finalize方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态
3.不可达状态
当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize方法后依然没有使该对象变为可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源
二,回收前确定哪些内存“活着”哪些已经“死去”
1.引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器值增加1,当引用失效,计数器值减1,任何时刻计数器值为0的对象就是不可能再被使用的。
- 优点:实现简单,判定效率高
- 缺点:很难解决对象之间相互循环引用的问题,so主流的java虚拟机里面没有使用引用计数算法来管理内存
2.可达性分析算法
通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(图论,就是从GC Roots到这个对象不可达),证明此对象是不可用的
3.引用
- 强引用:只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
- 软引用:有用但并非必须的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收
- 弱引用:比软引用更弱一些,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
- 虚引用:无法通过虚引用获得一个对象的实例,为一个对象设置虚引用的目的是能在这个对象被收集器回收时收到一个系统通知
4.被GC时自我拯救
finalize()方法何时被调用,是否被调用具有不确定性
JVM执行可恢复对象的finalize()时,可能使该对象或系统中其他对象重新变成可达状态
任何一个对象的finalize()方法都只会被系统自动调用一次
三,垃圾收集算法
1.标记-清除算法(Mark-Sweep)
首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象
- 缺点:会产生大量不连续的内存碎片
2.复制算法(Copying)
将可用内存按容量划分为大小相等两块,每次只使用其中的一块,当这一块内存用完,就将还存活的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理
新生代中的对象98%都是朝生夕死的,所以并不需要按照1:1来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor,当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理Eden和刚才用过的Survivor空间。HotSpot默认Eden和Survivor的大小比例为8:1,即参数设置-XX:SurvivorRatio=8,即每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被浪费
- 优点:按顺序分配内存即可,实现简单,运行高效
3.标记-整理(Mark-Compact)
首先标记所有需要回收的对象,然后让所有存活的对象都向一端移动,直接清理掉端边界以外的内存
4.分代收集算法
即对上述进行总结:在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,使用复制算法,只需要付出少量存活对象的复制成本;老年代中对象存活率高、没有额外空间对他进行分配担保,使用标记-清除或标记-整理
四,垃圾收集器
引用《深入java虚拟机》上的图,如果两个收集器之间存在连线,说明它们可以搭配使用。
1.Serial
单线程收集器,即:在它进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集结束。(Stop The World)
- 缺点:工作线程因内存回收而导致停顿
- 优势:简单高效
- 适合:运行在client模式下,新生代
2.ParNew
其实就是Serial的对线程版本
是许多运行在Server模式下的虚拟机中首选的新生代收集器
3.Parallel Scavenge
新生代收集器,也是使用复制算法
目标是达到一个可控制的吞吐量(其他收集器的关注点可能是竟可能地缩短垃圾收集时用户线程的停顿时间)
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
GC自适应调节策略,即当打开开关参数-XX:+UseAdaptiveSizePolicy,就不需要手工指定新生代的大小、Eden与Survivor比例、晋升老年代对象的年龄等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量
4.Serial Old
是Serial收集器的老年代版本,同样是一个单线程收集器
5.Parallel Old
是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
6.CMS
以获取最短回收停顿时间为目标,基于“标记-清除”算法
- 场景:目前很大一部分java应用集中在互联网站或者B/S系统的服务器上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短以给用户带来较好体验
- 优点:并发收集、低停顿
- 有三个明显的缺点:
- CMS收集器对CPU资源非常敏感,虚拟机提供了一种称为“增量式并发收集器(i-CMS)”的CMS收集器变种,所做的事情和单CPU年代pc机操作系统采用抢占式来模拟多任务机制的思想一样,就是在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会少一些,也就是速度下降的没那么明显
- CMS收集器无法处理浮动垃圾(Floating Garbage)
- CMS是一款基于“标记-清除”算法实现的收集器,会产生大量的空间碎片
7.G1(Garbage-First)
面向服务端应用,特点:
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
五,内存分配与回收策略
- 新生代GC(Minor GC):发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也很快
- 老年代GC(Major GC/Full GC):指发生在老年代的GC,Major GC的速度一般会比Minor GC慢10倍以上
- VM参数:-Xms堆最小值,-Xmx堆最大值,-Xmn堆中新生代大小
将堆最大值和最小值设置为一样即可避免java堆的自动扩展,eg:-Xms20M -Xmx20M -Xmn10M,限制堆大小为20M,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代
1.对象优先在Eden分配
2.大对象直接进入老年代
虚拟机提供了参数-XX:PretenureSizeThreshold,大于这个设置值的对象直接在老年代分配
3.长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄Age计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor中,并对象年龄设为1,对象在Survivor中每熬过一次Minor GC,年龄就增加1,当年龄增加到一定程度(默认15),就会被晋升到老年代中,可通过-XX:MaxTenuringThreshold设置
4.对象动态年龄判定
虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升为老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄
5.对象分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlepromotionFailure设置是否允许担保失败,如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次GC是有风险的;如果小于,或者HandlepromotionFailure设置不允许冒险,这时改为一次Full GC。
感谢你看到这里,我是程序员麦冬,一个java开发从业者,深耕行业六年了,每天都会分享java相关技术文章或行业资讯