垃圾回收机制,是java对于内存自动回收的机制。但是呢,这不是java独创的。
GC回收的是内存,更准确的来说是对象,回收的是堆上的内存。
那么释放哪几块内存区呢?
1)程序计数器(不需要进行额外的回收,线程销毁,自然就回收了)。
2)栈(不需要进行额外的回收 ,线程销毁,自然就回收了)。
3)元数据区(一般也不需要,都是加载类,很少卸载类)。
4)堆(GC回收的主力部分)(这里我们主要回收的就是对象,new出来的都是在堆中的)。
GC的流程主要有两个步骤:
1. 找到谁是垃圾。
2. 释放所对应的内存。
GC机制的两种实现方法
引用计数法
这里的引用计数法并不用jvm采取的方案,但这里的引用计数法非常的简单。
例如,当我们创建对象是如:Test a = new Test() , 这里对象本身的计数器就会加一。
当我们再写一个 Test b = a 时,这里我们的计数器就会再加一,变成2。
如图解:
当我们都将a 和 b 置为空是,这里的计数器就减一减一,变成零。
如图:
引用计数变成零,这里就可以视作为垃圾了。
但是这个方法有两个缺陷:
(1)消耗额外的存储空间(如果你的对象比较大还好,但如果你的对象比较小,且数目比较多的话,那么消耗的额外空间就比较多了)
(2)循环引用的问题(比较重要)
循环引用问题解释:
如代码:
class Test{Test t;
}Test a = new Test();Test b = new Test();a.t = b;b.t = a;a = null;b = null;
如上图代码,a和b的引用都被引用了两次,计数器上的数字为2。
但当我们,对a和b赋值为空的时候,计数器都减一,变成一,但这里就可以视作为垃圾释放了,但计数器为一,释放不掉,就会出现问题了。
如图:
所以两个引用相互指向对方,外面的代码也无法访问到,就会出现这种问题。
可达性分析(JVM采取的方案)
可达性分析呢,解决了存储空间的问题,也解决了循环引用的问题
JVM把对象之间的引用关系,理解成了树形的结构。
JVM就会不停的遍历这样的结构,能遍历到的标记为可达,不能被标记到的标记为不可达。
这里就像是数据结构中的,从根节点触发,所有的叶子节点都是可达的。
如果后续叶子节点没有遍历到,那么就是不可达的了。
如果根节点为空了,那么都是不可达的了,那么全都视为垃圾了。
还有这里的树型结构是自己创建的,本身就存在的,不是虚构的。
如图:
释放垃圾的策略
(1)标记清除
标记清除就是把对应垃圾的对象所对应的内存释放,简单粗暴。
如图:
这样的有什么后果呢,从图中可以分析,释放垃圾后,会存在大量的内存碎片。
当我们后面创建对象时,需要申请一大块空间时,就会影响到。
所以这种算法并不实用。
(2)复制算法
这里的复制算法,比如,我们的一段内存空间,有的是垃圾,那么我们会把不是垃圾的对象所对应的内存,复制到另一块的内存空间上,然后对原来的一块内存,全部释放掉。
如图:
释放后:
这里最大的问题就是,空间浪费的太多了。
(3)标记整理
这里的标记清理,类似于搬运,就是一块空间,垃圾释放掉之后,会把不是垃圾的放到一块。
如图:
这样的处理方法解决了时间的问题,但是时间开销更大了。
基于上述的这三种策略,又有了一个更复杂的策略。分代回收。
分代回收
分代算法和上面讲的3种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策 略,从而实现更好的垃圾回收。这就好比中国的⼀国两制方针⼀样,对于不同的情况和地域设置更符合当地的规则,从而实现更好的管理,这就时分代算法的设计思想。
当前JVM垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想, 只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代 中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
如图:
(1)伊甸区 ->生存区 通过复制算法。(由于存活对象很少, 复制开销也很低, 生存区空间也不必很大)。
(2)生存区 ->另一个生存区 通过复制算法,没经过一轮 GC,生存区中都会淘汰掉一批对象,剩下的通过复制算法,进入到另一个生存区(进入另一个生存区的还有从伊甸区里进来的对象)。
存活下来的对象,年龄 +1
(3)生存区 -> 老年代 某些对象, 经历了很多轮 GC, 都没有成为垃圾,就会复制到 老年代 ~~
老年代的对象,也是需要进行 GC 的,但是老年代的对象生命周期都比较常,就可以降低 GC扫描的频率。上述的过程就是分代回收的基本算法逻辑。