目录
简介
Garbage Collection(GC),Java进程在启动后会创建垃圾回收线程,来对内存中无用的对象进行回收。
垃圾回收时机
1.显示的调用System.gc():此方法的调用是建议JVM进行 FGC(Full GC),虽然只是建议而非一定,但很多情况下它会触发 FGC,从而增加FGC的频率。一般不使用此方法,让虚拟机自己去管理它的内存。
2.JVM垃圾回收机制决定
创建对象时需要分配内存空间,如果空间不足,触发GC。
3.其他回收机制
java.lang.Object中有一个finalize() 方法,当JVM 确定不再有指向该对象的引用时,垃圾收集器在对象上调用该方法。finalize() 方法有点类似对象生命周期的临终方法,JVM 调用该方法,表示该对象即将“死亡”,之后就可以回收该对象了。
注意回收还是在JVM 中处理的,所以手动调用某个对象的 finalize() 方法,不会造成对象的“死亡”。
垃圾回收策略————如何判断对象已死?
判定对象是否存活都与“引用”有关。当对象失去所有引用时,我们就可以说对象生命周期结束了,即为死亡,就该回收它啦。
引用类型:
Java将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
例:在某块内存创建对象若该内存不足,则触发垃圾回收(不可达的强引用对象,所有的弱引用对象),如果还不够则回收软引用对象,如果还不够,则OOM内存溢出
1.引用计数算法
早期策略。在这种算法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
缺点:循环引用时无效
如:如父对象有一个对子对象的引用,子对象反过来引用父对象。
2.可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为GC Roots引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图,object5和object6虽然相互引用,但是由于他们到GC Roots都不可达,因此会被判定为可回收的对象。 Java、C#等语言都是使用可达性分析算法进行垃圾回收。
常见的 GC Roots:
1 虚拟机栈(栈帧中本地变量表)中引用的对象, 如每个线程被调用的方法堆栈中使用的参数,局部变量。
2 方法区中 静态属性 引用的对象,Java 类引用的静态变量。
3 方法区中 常量 引用的对象,字符串常量池中的引用。
4 Java 虚机内部的的引用,如基本数据类型对应的 Class 对象,系统类加载器。
5 所有被同步锁持有的对象,如: Synchronized 修饰的对象。
除了上面这些固定的 GC Roots 对象之外,根据用户所选择的垃圾器以及当前回收的内存区域的不同,其他对象可“临时性”的加入 GC Roots集合。比如在分代收集和局部回收时,如果只针对新生代进行垃圾回收时,必须考虑到新生代的对象很有可能被 Java 堆的其他区域的对象所引用,这时候就需要考虑将这些对象加入到 GC Root集合中去。
目前比较新的几款垃圾收集器都具备了局部回收的特征。
需要要垃圾回收的内存
1.方法区(jdk1.7)/元空间(jdk1.8)
这个区域在GC中一般称为永久代(Permanent Generation),永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。此区域进行垃圾收集的“性价比”一般比较低。
回收废弃常量:
和回收Java堆中的对象十分类似。以常量池中字面量(直接量)的回收为例,假如一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个String对象引用常量池的"abc"常量,也没有在其他地方引用这个字面量,如果此时发生GC并且有必要的话,这个"abc"常量会被系统清理出常量池。常量池中的其他类(接口)、方法、 字段的符号引用也与此类似。
判定一个类是否是"无用类"则相对复杂很多。类需要同时满足下面三个条件才会被算是"无用的类" :
- 该类所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
- 加载该类的ClassLoader已经被回收
- 该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法
JVM可以对同时满足上述3个条件的无用类进行回收,也仅仅是"可以"而不是必然。在大量使用反射、动态代理等场景都需要JVM具备类卸载的功能来防止永久代的溢出。
2.堆
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。
从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:
(1)新生代(Young Generation):又可以分为Eden空间、From Survivor空间、To Survivor 空间。新生代的垃圾回收又称为Young GC(YGC)、Minor GC。指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快。
(2) 老年代(Old Generation、Tenured Generation)。老年代垃圾回收又称为Full GC(FGC)、Major GC。指发生在老年代的GC。出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。 Major GC的速度一般会比Minor GC慢10倍以上。
垃圾回收算法
标记-清除算法(Mark-Sweep算法)
先暂停整个程序的全部运行线程,让回收线程以单线程进行扫描标记,并进行直接清除回收,然后回收完成后,恢复运行线程。标记清除后会产生大量不连续的内存碎片,造成空间浪费。
- 最基础的收集算法,老年代收集算法。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
适用场景:
1、只有小部分对象需要进行回收的,因为回收对象太多,其清除的时间就会越长。
2、关注停顿时间,对用户线程停顿时间有要求。
复制算法(Copying算法)
将可用内存分为大小相等的两份,在同一时刻只使用其中的一份。当这一份内存使用完了,就将还存活的对象复制到另一份上,然后将这一份上的内存清空。复制算法能有效避免内存碎片,但是算法需要将内存一分为二,导致内存使用率大大降低。新生代的收集算法
缺点:内存使用率不高----50%
优点:简单,高效。
适用场景:大部分对象都是需要进行回收只有少量对象存活的,因为需要把存活对象复制到新的内存区域,所需要复制的对象越多那么其复制的时间就会越长。
标记-整理算法(Mark-Compact算法)
标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
- 老年代收集算法
适用场景
1、只有小部分对象需要进行回收的,因为标记整理法也需要清除掉回收对象,回收对象多其清除的时间就会越长。
2、关注吞吐量
分代收集算法(Generational Collection算法)
当前JVM垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。 一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算 法。
垃圾回收的过程
-
新生代中98%的对象都是"朝生夕死"的,所以并不需要按照复制算法所要求1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden :Survivor From : Survivor To = 8 : 1 : 1。所以每次新生代可用内存空间为整个新生代容量的90%,只有10%的内存会被”浪费“。
-
Eden空间不足,触发Minor GC:用户线程创建的对象优先分配在Eden区,当Eden区空间不够时,会触发Minor GC:将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
-
垃圾回收结束后,用户线程又开始新创建对象并分配在Eden区,当Eden区空间不足时,重复上次的步骤进行Minor GC。
-
年老对象晋升到老年代。
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且把对象年龄设为1。对象在Survivor空间中每"熬过"一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将晋升到老年代中。
- Survivor空间不足,存活对象通过分配担保机制进入老年代
Survivor区只占新生代10%空间,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(HandlePromotion)。
内存的分配担保就好比我们去银行借款,如果我们信誉很好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。
内存的分配担保也一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
- 老年代空间不足,触发Major GC:因为大对象直接进入老年代、长期存活的对象也将进入老年代,当老年代空间不足时,也需要对老年代进行垃圾回收,也就是触发Major GC。
还有一些特殊情况会触发垃圾回收,在垃圾收集器中进一步讨论。
用户线程的暂停:Stop-The-World(STW)
垃圾回收工作是在垃圾回收线程中执行的,在很多情况下,执行垃圾回收工作,或是执行垃圾回收其中某一步骤时,需要暂停用户线程,也就是Stop-The-World(STW)。
- 垃圾回收首先是要经过标记的。对象被标记后就会根据不同的区域采用不同的收集方法。
- 垃圾回收线程标记好对象时,用户线程在并发执行时,可能会将该对象重新加入“引用链”,垃圾回收时就会回收这个不该回收的对象,出现问题。
- 在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得
知这些信息了。这些特定的位置称为安全点(Safepoint)。
程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。 - GC发生时让所有线程暂停,即让所有线程“跑”到最近的安全点上再停顿下来,有两种方案可供选
择:
- 抢先式中断(PreemptiveSuspension):不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
- 主动式中断(Voluntary Suspension):是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的
地方。
串行/并行(Parallel) : 指垃圾回收线程与用户线程是串行的,会造成用户线程暂停,也就是Stop The World(STW) 。
并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,而垃圾收集程序在另外一个CPU上。
吞吐量和停顿时间(用户体验)
评价垃圾回收器的指标:吞吐量和停顿时间(用户体验)
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那
吞吐量就是99%。
GC造成的用户线程单次停顿的时间越短,就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后
台运算而不需要太多交互的任务。
GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了(单次停顿时间减少了,但总的停顿时间变长了)。
一句话:吞吐量和用户体验关系可以看成反比关系,即用户体验越好,吞吐量越低。