目录
GC(Varbage Collection:垃圾回收):将堆内存中不在被使用的对象进行回收,GC中用于回收的方法称之为收集器。GC是需要消耗一些资源和时间,对Java堆对象的生命周期特征进行分析,按照新生代、老年代的方式对对象进行回收,以尽可能虽短GC对应用造成的暂停。
-
对于新生代的对象的收集称之为minor GC/Young GC
-
对老年代的对象的收集称之为Full GC
-
程序中主动调用System.gc(),强制执行GC为Full GC
我们知道在Java中有不同的对象引用类型,而GC会对不同的引用类型采用不同的方法进行回收,Java中引用分为4种类型
-
强引用:默认情况,New 的对象都是强引用所作用的对象,任何情况下都不会被回收
-
软引用(SoftReference):软引用所引用的对象在内存不足时发生GC操作会被回收
-
弱引用(WeakReference):只要发生GC操作,弱引用所作用的对象会被回收
-
虚引用(PhantomReference):虚引用不会影响对象的回收,作用用于通知对象是否被回收
1、对象被标记为垃圾的方法
JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分
引用计数法
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
对以上代码通过引用计数法分析:
可达性分析
可达性算法是目前主流的虚拟机都采用的算法,程序把所有的引用关系看作一张图,从一个节点GC Roots开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
-
虚拟机栈中引用的对象(栈帧中的本地变量表);
-
方法区中类静态属性引用的对象;
-
方法区中常量引用的对象;
-
本地方法栈中JNI(Native方法)引用的对象。
可以得出对象实例1、2、4、6都具有对象可达性,也就是存活对象,不能被GC回收的对象。而随想实例3、5虽然直接相连,但并没有任何一个GC Roots与之相连,即GC Roots不可达对象,就会被GC回收的对象
2、垃圾回收算法
标记-清除算法
标记/清除算法的基本思想就跟它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
标记阶段:
标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的 GC Roots 对象,对从 GCRoots 对象可达的对象都打上一个标识,一般是在对象的 header 中,将其记录为可达对象;
清除阶段:
清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header 信息),则将其回收
上图是标记/清除算法的示意图,在标记阶段,从对象 GC Root 1 可以访问到 B 对象,从 B 对象又可以访问到 E 对象,因此从 GC Root 1 到 B、E 都是可达的,同理,对象 F、G、J、K 都是可达对象;到了清除阶段,所有不可达对象都会被回收。
在垃圾收集器进行 GC 时,必须停止所有 Java 执行线程(也称"Stop The World"),原因是在标记阶段进行可达性分析时,不可以出现分析过程中对象引用关系还在不断变化的情况,否则的话可达性分析结果的准确性就无法得到保证。在等待标记清除结束后,应用线程才会恢复运行。
标记清除算法缺点:
-
效率问题
标记和清除两个阶段的效率都不高,因为这两个阶段都需要遍历内存中的对象,很多时候内存中的对象实例数量是非常庞大的,这无疑很耗费时间,而且 GC 时需要停止应用程序,这会导致非常差的用户体验。
-
空间问题
标记清除之后会产生大量不连续的内存碎片(从上图可以看出),内存空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
复制算法
复制算法是将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块内存上,然后把这一块内存所有的对象一次性清理掉
复制算法每次都是对整个半区进行内存回收,这样就减少了标记对象遍历的时间,在清除使用区域对象时,不用进行遍历,直接清空整个区域内存,而且在将存活对象复制到保留区域时也是按地址顺序存储的,这样就解决了内存碎片的问题,在分配对象内存时不用考虑内存碎片等复杂问题,只需要按顺序分配内存即可。
复制算法缺点:
复制算法简单高效,优化了标记清除算法的效率低、内存碎片多问题,存在缺点:
-
将内存缩小为原来的一半,浪费了一半的内存空间,代价太高;
-
如果对象的存活率很高,极端一点的情况假设对象存活率为 100%,那么我们需要将所有存活的对象复制一遍,耗费的时间代价也是不可忽视的。
标记-整理算法
标记-整理算法算法与标记/清除算法很像,事实上,标记/整理算法的标记过程任然与标记/清除算法一样,但后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边线以外的内存。
可以看到,回收后可回收对象被清理掉了,存活的对象按规则排列存放在内存中。这样一来,当我们给新对象分配内存时,jvm 只需要持有内存的起始地址即可。标记/整理算法弥补了标记/清除算法存在内存碎片的问题消除了复制算法内存减半的高额代价,可谓一举两得。
标记整理算法的缺点:
-
效率不高:不仅要标记存活对象,还要整理所有存活对象的引用地址,在效率上不如复制算法。
分代回收算法
分代收集算法的思想是按对象的存活周期不同将内存划分为几块一般是把 Java 堆分为新生代和老年代(还有一个永久代,是 HotSpot 特有的实现,其他的虚拟机实现没有这一概念,永久代的收集效果很差,一般很少对永久代进行垃圾回收),这样就可以根据各个年代的特点采用最合适的收集算法。
特点:
新生代:朝生夕灭,存活时间很短。采用复制算法来收集
老年代:经过多次 Minor GC 而存活下来,存活周期长。采用标记/清除算法或者标记/整理算法收集老年代
新生代中每次垃圾回收都发现有大量的对象死去,只有少量存活,因此采用复制算法回收新生代,只需要付出少量对象的复制成本就可以完成收集;
老年代中对象的存活率高,不适合采用复制算法,而且如果老年代采用复制算法,它是没有额外的空间进行分配担保的,因此必须使用标记/清理算法或者标记/整理算法来进行回收。
新生代中的对象“朝生夕死”,每次GC时都会有大量对象死去,少量存活,使用复制算法。新生代又分为Eden区和Survivor区(Survivor from、Survivor to),大小比例默认为8:1:1。
老年代中的对象因为对象存活率高、没有额外空间进行分配担保,就使用标记-清除或标记-整理算法。
这里说一下堆中的数据存储情况:
新产生的对象优先进去Eden区,当Eden区满了之后再使用Survivor from,当Survivor from 也满了之后就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,然后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。复制的时候,如果Survivor to 无法容纳全部存活的对象,则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进去老年代,如果老年代也无法容纳,则进行Full GC(老年代GC)。