GC算法一般来说分为: 引用计数法(过时) 、标记清除、 标记压缩、 复制算法
GC的对象是堆空间(新生代、老年代)和永久区(永久代)
1.引用计数法:
目前使用者比如python
引用计数器的实现很简单,对于一个对象A(每个对象都有一个引用计数器),只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
根对象:方法区中的静态引用,native方法引用的对象,栈帧局部变量表(执行上下文)
总的来说就是一系列的名为“GCRoots”的对象
-----:代表引用
--/--:代表不再引用
根对象X--------->A(1)----------->B(1)--------C(1),A,B,C都为X的可达有效对象。
当X----/---->A(0)时。A为不可触及状态,A被回收,接受B没有被A引用,跟着回收。
缺点:
@1 引用和不再引用伴随加法和减法,影响性能
@2 很难处理循环引用(某个对象是垃圾,但是它的的引用计数器不为0)
X--------->A(2)<----------->B(1)
比如跟对象X引用A(1),A引用B(1),B再次引用A(2),那么当X不再需要A了,A应当被清理,但是A(1)<-->B(1)
2.标记-清除:
标记-清除算法是现代垃圾回收算法的思想基础,Full GC 是发生在老年代的垃圾收集动作,所采用的就是标记-清除算法。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。
eg:X--------->A(1)----------->B(0)--------C(0),其中A为可达对象,被标记
因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象(会产生不连续的内存空间,也就是内存碎片),此后如果需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
3.标记-压缩:
标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法首先进入标记阶段,需要从根节点开始,对所有可达对象做一次标记。然后压缩阶段:将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
4.复制算法:
与标记-清除算法相比,复制算法是一种相对高效的回收方法 适用于存活对象较少的场合 如新生代 (minor gc垃圾收集动作)将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象(包括eden,from两处gc(算是一次minor gc,同时进行)后的存活对象的总和)复制到未使用的内存块(to)中,之后,清除正在使用的内存块中的所有对象
交换两个内存的角色(from->to),完成垃圾回收。
左边比较大的是eden区,两块小的是survivor区
复制算法流程(新生代->老年代):
eden饱和后,开始minor gc(每一次gc,都会伴随对from区域gc,然后清空eden+from),存活对象(对from区域的对象gae+1,判断年龄,满足old年龄【默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 】,则会移动到老年代)复制一份到to区域,如果对象过大,survivor区域无法接收,那么直接加入老年代,如果老年代中没有足够空间(一般来说,与新生代1:2)容纳这个(些)对象,那么会触发一次Full GC(设置参数HandlerPromotionFailure【允许担保失败】为true时,那么也会先尝试进行一次Minor GC,Minor GC无法执行时再进行Full GC),Full GC会对整个Heap进行一次GC,如果Full GC后还有无法给新创建的对象分配内存,或者无法移动那些需要进入老年代中的对象,那么JVM抛出OutOfMemoryError。之后如果eden再次饱和,那么开始minor gc,包含了eden+from,将存活对象复制到to区域,然后清空eden+from,此时,身份转变,即原来的from变为了to,to变为了from。//包换数据的那一块,称之为from,是动态的。
tip:每次其实都是向to区存放数据,之后这个to转成from,所以,from就是最终存放数据的那一块。
tip:fullgc补充如下:
更详细的图解:https://www.jianshu.com/p/314272e6d35b
对象进入老年代的4种情况:
(1) 假如进行Minor GC时发现,存活的对象(from或eden)在To区(To区的阈值)中存不下,那么把存活的对象存入老年代
(2) 大对象直接进入老年代(eden的阈值)
假设新创建的对象很大,比如为5M(这个值可以通过PretenureSizeThreshold这个参数进行设置,默认3M),那么即使Eden区有足够的空间来存放,也不会存放在Eden区,而是直接存入老年代
(3) 长期存活的对象将进入老年代
此外,如果对象在Eden出生并且经过1次Minor GC后仍然存活,并且能被To区容纳(TO区也会饱和),那么将被移动到To区,并且把对象的年龄设置为1,对象每"熬过"一次Minor GC(没有被回收,也没有因为To区没有空间而被移动到老年代中,第一种),年龄就增加一岁,当它的年龄增加到一定程度(默认15岁,配置参数-XX:MaxTenuringThreshold),就会被晋升到老年代中
(4) 动态对象年龄判定
虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor区中相同年龄(设年龄为age)的对象的所有大小之和超过总Survivor剩余空间的一半,年龄大于或等于该年龄(age)的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
eg:在from区,大于等于5岁的对象总和1.5M>=1.2M ------- (4M(S1+S2)-1.6M)/2
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlerPromotionFailure这个参数设置的值(true或flase)是否允许担保失败(如果这个值为true,代表着JVM说,我允许在这种条件下尝试执行Minor GC,出了事我负责)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,如果Minor GC还是无法执行,此时还得改为Full GC。;如果小于,或者HandlerPromotionFailure为false,那么这次Minor GC将升级为Full GC
空间分配担保机制说白了,就是新生代放不下就会借用老年代的空间,老年代没有充足空间,则会进行mnior gc或者full gc,如果Full GC后还有无法给新创建的对象分配内存,或者无法移动那些需要进入老年代中的对象,那么JVM抛出OutOfMemoryError ,也就是OOM。
Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长,因为我们要尽量避免Full GC。
分代思想:
第一步:根据对象存活的周期进行分类,短命的为新生代,长的为老年代。一般的,大对象存活时间较长,直接加入老年代
第二部:根据不同代的特点,选取收集算法,新生代使用复制算法(少量存活对象),老年代(大量存活对象)使用标记-清理/标记-压缩
可触及性:
根据对象是否可触及,来判断是否是垃圾
对象周期:可触及的(从根节点可以触及到这个对象)->可复活的(xx = null,引用释放)->不可触及的(调用了finalize方法之后,第一次调用finalize,方法中可以写复活代码,比如xx = new XX(),之后不可能在复活,不可触及的对象不可能复活但可以回收)
eg:重写finalize方法,并使用System.gc()去尝试触发finalize方法,但是只能复活一次:
Stop-The-World:
Java中一种全局暂停的现象 ,多半由于GC引起,所有Java代码停止,native代码可以执行,但不能和JVM交互 。
GC时为什么会有全局停顿? 比如在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。 危害 长时间服务停止,没有响应。
遇到HA系统,可能引起主备切换,严重危害生产环境。