GC算法
一、什么是GC
GC是垃圾收集的意思(Gabage Collection),在程序运行过程中会产生一部分不再使用的对象占用着内存空间,如果这些对象长期占用内存而不被清除,就会使得内存不足,而导致内存溢出。内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃。我们知道 C++ 程序员在编码过程中,都需要自己去管理内存,一旦内存没有回收就会导致程序或系统的不稳定甚至崩溃。JAVA为了解决这个问题就推出了这个自动清除无用对象的功能,或者叫机制,这就是GC,它可以自动监测对象是否超过作用域从而达到自动回收内存的目的,使得Java 程序员不用担心内存管理。
System.gc() 和 Runtime.getRuntime().gc()在 java 中可以提醒GC做垃圾回收,但是仅仅是提醒,具体GC做不做垃圾回收不是人为可以控制的,因为 垃圾回收时不定时的。
在 Java 中 GC 主要进行垃圾回收的位置是,堆和永久区。
二、可触及性
在垃圾回收的过程中,需要去识别什么是垃圾,这里存在一个可触及性的概念。
- 可触及的
从根结点开始可以触及到这个对象,那么这个对象就具有可触及性。
- 可复活的
一些对象在当前状态不可被触及,但是过一段时间之后就会再次被触及,所以这种对象也是不可以被GC回收。一旦所有引用被释放就是可复活状态,因为在finalize()中可以复活。所以一般要避免使用finalize()方法,如果操作不当,有可能会导致对象复活而导致一些不可预见的错误,而且它的优先级比较低,什么时候会被调用也时一件不确定的事情,如果有相对应的操作可以使用try-catch-finally来代替。
- 不可触及的
在finalize()之后,可能会进入不触及状态,不可触及的的对象不能被复活,可以直接被GC回收
哪些元素可以作为根元素呢?
①. 本地方法栈中的JNI(Native方法)引用对象。
②. 栈中的本地变量表
③. 方法区中的常量
④. 类的静态变量
三、GC算法
- 引用计数法
在堆中存储对象时,每个对象都会存在一个计数器,如果此对象被其他对象或者栈中局部变量等引用,则引用计数 +1 ,反之,如果被放弃引用则引用计数 -1.当一个对象的引用计数为 0 时,则说明没有被其他对象所引用,于是它就属于垃圾。但是,引用计数法存在一个问题,如果A对象引用B对象,B对象引用A对象,那么测试的计数器的值都会大于0,所以使得A对象和B对象不可以被回收。在java中没有使用,而是被python所使用。
我们看下面例子:
此时 A,C,E 对象的引用计数为 1 ,而B,D 则为 引用计数为2 ,F的引用计数为 0,所以,F对象则为即将被回收的对象,但是像 G 和 H 对象则互相引用,他们的引用计数均为1,所以此时 G,H两个对象将不会被回收,这也是引用计数法存在的一个最根本的问题。
- 可达性分析算法
可达性分析算法时用来判断对象是否存活的。这个算法主要时以从根元素(GC Roots)出发,向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,则判定这个对象为可回收对象。
我们看下面例子:
此时,A,B,D是可达的,而C,D则时不可达的,无论C,D是否相互引用,都会被回收。
- 标记清除法
标记清除法时现代垃圾回收算法的思想基础。它主要分两个阶段,标记阶段,和清除阶段,其中标记阶段就是使用上面的可达性算法去判断,此对象是否被引用,将未被引用和被引用的对象进行区分,将被引用的对象进行标记。在清除阶段清除所有没有被标记的对象。
我们看下面例子:
其中灰色部分表示被标记的对象,黑色部分表示未被标记对象,白色部分表示被空闲空间,在标记阶段,通过根元素开始标记被引用的对象,标记结束之后回收未被标记的对象。
- 标记压缩法
标记压缩法是在标记清除法的基础上做了一定的优化,它适用与存活对象较多的内存空间,比如老年代。标记压缩法跟标记清除法一样首先需要标记被引用的对象,但是在清除阶段有一定的优化,它将被标记的对象放在内存的一端,然后将内存边界之外的内存全部清除。
我们看下面例子:
首先,将被引用的对象标记出来,然后将被引用的对象全部按照地址进行存放到内存的一端,然后清除边界外的所有内存。
- 复制算法
复制算法,也是对标记清除法的优化,而且它的效率更加高效,但是它不适合于老年代这种存活对象较多的内存,应为它将内存划分为大小完全相同的两块区域,而每次只使用一块,另一块则完全处于空闲状态,当进行回收对象时,它首先需要做的也是将被引用的对象进行标记,然后将这些被引用的对象复制到另一块内存中,将这块内存进行清理回收,然后将两块内存的职责进行调换,完成垃圾回收。
我们看下面例子:
首先,我们有两块完全一样大小的内存,一块不进行使用,完全空闲,另一块使用之后,在进行垃圾回收时进行标记被引用对象,然后将被引用对象复制到另一块内存中,将第一块内存进行回收,然后将两块内存职责交换。
四、现代JMV中GC的算法使用
在现代JVM中GC垃圾清理时不止使用一种算法,而是将不同的算法运用与不同的内存空间进行垃圾回收。
现在,GC在进行垃圾回收时,老年代将使用标记清除法进行回收,而新生代则使用了标记压缩法和复制算法,首先第一块比较大的区域为 eden区,下面两块比较小的区域为from区和to区也叫做s0和s1区,最下面为老年代。eden区在进行垃圾回收时首先将大对象直接进入老年代,然后将其他的被标记的对象放入没有使用的这块空间中也就是to区,然后将eden区全部清空。from区进行回收时,首先将被标记,但是比较年轻(经历过垃圾回收次数少)的对象放入to区,然后将年龄比较大(经历过垃圾回收次数多)的对象放入老年代,然后将from区全部清空,最后将from区和to区进行交换,结束回收。
五、分代思想
根据对象存活的周期进行分类,短命的对象归为新生代,长命的对象归为老年代。
新生代:刚刚被创建出来的对象就是新生代对象。比较适合复制算法,因为新生代对象相对较少。
老年代:在新生代中的大对象无法进入from区的对象会直接进入老年代,多次垃圾回收都没有被回收的对象会进入老年代。比较适合标记清除法和标记压缩算法,因为在老年代的对象要么是生命周期比较长的对象,要么是系统级的对象,它们多数都处于存活的状态,如果使用复制算法,一方面比较浪费空间,一方面存活对象较多,复制速度较慢。
六、Stop-The-World
在java程序中,有极小的可能会出现全局停顿的情况,所以代码都会被挂起,只有native代码可以执行,但不能和JVM进行交互,此时多半是由GC引起的,因为在程序在运行的过程中会不断的产生垃圾对象,但是GC在进行垃圾操作时,为了可以将垃圾回收进行彻底,于是将java程序挂起,等到GC结束之后再继续运行java程序。此时就会导致服务长时间没有响应。一般GC速度是比较快的,但是如果内存比较大,垃圾比较多的时候就会使用比较长的时间进行垃圾回收。
这时如果遇到HA系统的话就会出现一些不必要的问题。因为主机正在进行GC时服务器没有了响应,此时备机就会自动启动去代替主机,但是当主机GC结束之后,程序会继续运行,此时主机和备机就会一起使用,就有可能导致数据不一致。
-------------------- END ---------------------
最后附上作者的微信公众号地址和博客地址
Herrt灬凌夜:https://www.cnblogs.com/wuyx/