深入理解java垃圾回收机制——
一、垃圾回收机制的意义
Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。
ps:内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。
垃圾回收机制中的发现算法(发现垃圾)
Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。
1.引用计数法(Reference Counting Collector)
1.1算法分析
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
1.2优缺点
优点:
引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:
无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
如下图:
A,B,C三个对象都不可能计数器为1,但是它们三个连在一块确实是垃圾,所以引用计数法不能将它们回收。
根搜索算法(可达性分析算法)
由于引用计数法存在缺陷,所有现在一般使用根搜索算法。
根搜索算法图解
根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点
GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。 如上图中的ObjF、ObjD、ObjE通过GC Root是无法找到的,所以它们是无用节点。
Java 中可作为 GC Root 的对象:
- 虚拟机栈中引用的对象(本地变量表)
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
-
root可以简单的认为就是一个引用变量
垃圾回收算法
在确定了哪些垃圾可以被回收后,垃圾收集器要做的就是进行垃圾的回收,有下面的几中算法:
标记-清除(
Mark-Sweep)算法标记-清除算法分为两个阶段:
标记阶段:标记出需要被回收的对象。
- 清除阶段:回收被标记的可回收对象的内部空间。

标记-清除算法图
标记-清除算法实现较容易,不需要移动对象,但是存在较严重的问题:
- 算法过程需要暂停整个应用,效率不高。
标记清除后会产生大量不连续的内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
复制(
Copying)算法为了解决标志-清除算法的缺陷,由此有了复制算法。
复制算法将可用内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。
复制算法图
小结:优点:实现简单,不易产生内存碎片,每次只需要对半个区进行内存回收。
- 缺点:内存空间缩减为原来的一半;算法的效率和存活对象的数目有关,存活对象越多,效率越低。
标记-整理(
为了更充分利用内存空间,提出了标记-整理算法。Mark-Compact)算法
此算法结合了“标记-清除”和“复制”两个算法的优点。
该算法标记阶段和“标志-清除”算法一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
标志-整理算法图分代收集(
分代收集算法是目前大部分Generational Collection)算法JVM的垃圾收集器采用的算法。
核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
分代算法图
区域划分:年轻代(
Young Generation)- 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
- 新生代内存按照8:1:1的比例分为一个
eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。 - 当
survivor1区不足以存放eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
4.新生代发生的GC也叫做Minor GC,Minor GC发生频率比较高(不一定等Eden区满了才触发)。
年老代(
Old Generation)- 在年轻代中经历了
N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。 - 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发
Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代(
Permanent Generation) 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。
GC 类型:
Minor GC(新生代GC):
新生代GC,指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生熄灭的特点,所以Minor GC十分频繁,回收速度也较快。Major GC(老年代GC):
老年代GC,指发生在老年代的垃圾收集动作,当出现Major GC时,一般也会伴有至少一次的Minor GC(并非绝对,例如Parallel Scavenge收集器会单独直接触发Major GC的机制)。Major GC的速度一般会比Minor GC慢十倍以上。Full GC:
清理整个堆空间—包括年轻代和老年代。Major GC == Full GC。 参考:聊聊JVM(四)深入理解Major GC, Full GC, CMS
产生 Full GC 可能的原因:
- 年老代被写满。
- 持久代被写满。
System.gc()被显示调用。- 上一次
GC之后Heap的各域分配策略动态变化。
