Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来
垃圾回收
Java GC(Garbage Collection,垃圾回收)机制,顾名思义,就是Java将程序中不再需要使用的对象占用的内存进行回收。虽说Java在垃圾回收上可以做到“自动化”,但是了解垃圾回收机制后,我们将不会困惑于怎样进行内存泄漏、内存溢出等问题的排查,或者是垃圾回收成为系统瓶颈时的处理。
说到内存的占用,在Java运行区域中,程序计数器、虚拟机栈和本地方法栈这个三个区域的每一个栈帧分配多少内存,在程序启动时即已经知道了,无需关心,唯独Java堆和方法区则不一样,只有在程序运行期间才知道会创建哪些对象,需要多少内存,所以垃圾回收基本上是讨论这两个区域的内存回收。
怎样定义垃圾
要做到垃圾回收,首先就要确认哪些是垃圾,即——哪些对象是不再需要使用或者是长时间不再使用的,他们占着内存是对内存的浪费。所以需要通过一系列算法来确定这些对象,进行回收。
引用计数算法
引用计数算法的思想是在对象的头里分配一个空间来存储该对象被引用的次数,每被引用一次就+1,失去一个引用就-1,当该对象被引用次数为0的时候,就是该对象不可能再被使用的时候,就是需要被kill的时候了。
引用算法的实现简单,判定效率也高。但在主流的Java虚拟机里并没有使用引用计数算法来管理内存,其主要原因是很难解决对象之间相互引用的问题。就像是以下情况:
从运行结果可知,Java在进行GC时,即使是循环引用的对象,也会进行回收。
可达性分析算法
在主流的编程语言里,都使用可达性分析算法来判断对象是否存活,可达性分析算法的基本思路是,通过一些被称为 GC Roots 的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。
垃圾回收算法
标记-清除算法
标记/清除算法如题,分为标记和清除两个阶段进行。
标记:即使用可达性分析算法,遍历GC Roots对象,将所有可达对象都打上标记
清除:对堆内存进行遍历,将所有没有被标记的对象全部回收
标记/清除算法存在问题:
1、效率低下。因为要遍历内存中的对象,所以标记和清除两个阶段的效率都不高,而且GC时需要停止应用程序,虽然不是连续的,但是当对象极多时,也并不好用
2、空间问题。标记清除是对没有标记的对象进行回收,所以回收后之后会产生大量不连续的内存碎片,内存空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作
复制算法
复制算法的原理是:将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块内存上,然后把这一块内存所有的对象一次性清理掉。
复制算法每次都是对整个半区进行内存回收,这样就减少了标记对象遍历的时间,在清除使用区域对象时,不用进行遍历,直接清空整个区域内存,而且在将存活对象复制到保留区域时也是按地址顺序存储的,这样就解决了内存碎片的问题,在分配对象内存时不用考虑内存碎片等复杂问题,只需要按顺序分配内存即可。
复制算法简单高效,优化了标记/清除算法的效率低、内存碎片多的问题。但是它的缺点也很明显:
1、将内存缩小为原来的一半,浪费了一半的内存空间,代价太高;
2、如果对象的存活率很高,极端一点的情况假设对象存活率为100%,那么我们需要将所有存活的对象复制一遍,耗费的时间代价也是不可忽视的
标记-整理算法
标记/整理算法从名字上看,与标记/清除算法很像,事实上,标记/整理算法的标记过程与标记/清除算法几乎一样,但标记后不是直接对可回收对象进行回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边线以外的内存
这样,就可以避免清除时需要再次遍历对象的问题,并且将分散的内存片段也置为了连续的内存片段