JVM--GC学习
一、为何需要学习GC?
很多人会问为何需要学习GC相关的知识,甚至会问为何学习JVM,最开始我也觉得学习JVM相关知识在工作并没有多大帮助,很多人包括自己学习的目的可能是为了应付面试。当然有的人说是因为兴趣,这个不排除这种可能,不过在中国绝大多数搞IT的仅仅是为了生存,并不是内心中喜欢编程,喜欢搞技术。不过当在工作中遇到内存回收相关的问题的时候,这部分知识如何没有了解过,就可能会手足无措了。今年4月份左右的时候,所在公司的一个web项目,出现了反应迟钝的问题,看到同项目组的两位老大哥通过工具分析tomcat的内存回收情况,诊断出问题到最后如何解决。当时看的时候,只觉得别人很牛逼,自己当时就想过,要是自己遇到这个问题,如果没了解过JVM内存回收相关的知识,那么当项目出现问题的,自己会想到往内存方面分析问题?以及自己能解决问题?答案很明显,是不能。所以我觉得学习JVM这方面的部分知识,有其必要性,当工作中遇到内存相关的问题时候,不说很快解决问题,这需要一个经验积累的过程,最起码不至于一脸懵逼。
二、哪些对象可以被回收?
如何确定对象的存活?主要两种方法:
1.引用计数算法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收,方法简单,缺无法解决对象相互循环引用的问题。
2.可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
在Java语言中,GC Roots包括:
- 虚拟机栈中引用的对象。
- 方法区中类静态属性实体引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象
三、如何回收垃圾对象?
1.标记-清除(Mark-Sweep)算法
标记:标记的过程其实就是,遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。
清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.复制算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。
3.标记-整理算法
标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。
整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
4.分代收集算法
Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。详细过程如下:
当new一个对象的时候,优先分配在Eden区,当Eden区满了的时候,触发一次minor GC,主要采用复制算法,将存活的对象复制到from Survivor区。
当再次new一个对象的时候,发现Eden区满了,再次触发Minor GC,注意这里稍微有点区别,会将Eden区与From Survivor区还在被使用的对象复制到To Survivor区。
再下一次Minor GC的时候,则是将Eden区与To Survivor区中的还在被使用的对象复制到From Survivor区。
经历多次Minor GC后,部分Survivor区的对象的年龄达到阀值。这部分对象复制进入老年代
当进行Minor GC的时候,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC。全局GC成本比较大,如果系统频繁Full GC,对系统的性能影响非常打。所以需要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。
四、何时进行GC?
1.了解GC时java堆区内存的细分
java堆细分为:年轻代、老年代。年轻代细分:Eden区,伊甸园的意思,对象的起源之地,对象基本上优先在这个区域分配内存。Survivor区,幸存区的意思,意味着当经历一次GC后,仍然存活的对象从Eden转移到Survivor区,Survivor区有两块:from Survivor和to Survivor。
GC分为:minor GC,普通的对象回收,主要是针对新生代区域的GC;major GC or Full GC,全局GC,主要针对老年代GC,偶尔伴随着新生代以及对永久代的GC。
2.对象的分配规则
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC(次要)。Eden区满足存活的对象转移到Survivor区.不满足存活的对象内存被回收释放。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Minor GC,如果false则进行Full GC。
3.GC的时机
当new一个对象,发现需要的内存空间,java堆的Eden区已经无法提供足够的内存空间的时候,即Eden区满的时候,触发Minor GC,通过引用计数或者可达性算法确定Eden区可被回收的对象后,通过复制算法将存活的对象复制到Survivor区,死亡的对象内存空间被释放。Survivor区存活的对象经历多次Minor GC后仍然存活的对象,这部分对象进入老年代。当进行Minor GC的时候,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC。