Java垃圾回收(一)—— 回收机制

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,外面的人想进去,墙里面的人却想出来。

垃圾回收

垃圾回收(Garbage Collection,GC),是一种自动的存储管理机制。当一些被占用的内存不再需要时,就应该予以释放,以腾出空间。

GC之What,When,How

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

哪些内存需要回收

我们在之前介绍了Java内存运行时区域,其中程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。因此这3个区域的内存分配和回收都具备确定性,便不需要多考虑回收的问题。

而Java堆和方法区则不一样,我们只有在程序处于运行期才能知道会创建哪些对象,(实现类的内存可能不一样,执行方法需要的内存也可能不一样)这部分内存的分配和回收都是动态的。垃圾收集所关注的就是这部分内存。


什么时候回收

总的来说,有两个条件会触发主GC : (除手动触发以外)Wing_Li的简书

  1. 当应用程序空闲或者转为后台应用时,GC会被调用。

  2. 堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若一次GC之后(minor GC)仍不能满足内存分配的要求,JVM会再进行一次GC(major GC)作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,应用将停止。


如何回收

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器会判定对象是否”存活”(与引用有关),不“存活”的对象便会在适当的时候通过垃圾收集器回收。


对象回收判定

对象的回收判定通常有两种算法 : 引用计数算法和可达性分析算法。

引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;
任何时刻计数器为0的对象就是不可能再被使用的,这时候变可通知GC收集器回收这些对象。

这里写图片描述
网络摘图

但是Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

优点:简单,高效,现在的objective-c用的就是这种算法。
缺点:很难处理循环引用,相互引用的两个对象则无法释放。(需要开发者自己处理)

ClassA a=new ClassA();
ClassB b=new ClassB();
a.setMethod(b);
b.setMethod(a);

可达性分析算法

Java虚拟机中,是通过可达性分析(Reachability Analysis)来判定对象是否存活的。
这个算法的基本思路是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链项链时,则证明此对象是不可用的。

(也可以说是可达GC Roots分析)

这里写图片描述

GC Roots的对象

  • 1.虚拟机栈(栈帧中的局部变量表)中的引用对象

  • 2.方法区中类静态属性(静态对象)引用的对象

  • 3.方法区中常量(final 修饰的成员对象)引用的对象。

  • 4.本地方法栈中JNI(Native)引用的对象。

PS : 成员对象(即存储在堆)的引用的对象为何不在GC Roots?



对象之引用

判定对象是否存活(需要回收)都与“引用”有关。Java对引用的概念扩充,将引用分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Refernce)。

  • 强引用 : 类似Object obj=new Object()这类的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

  • 软引用 : 软引用是用来描述一些有用但非必须的对象,在系统将要发生内存溢出(OOM)异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出(OOM)异常。JDK1.2之后提供了SoftReference类来实现软引用。

  • 弱引用 : 弱引用也是用来描述非必需对象的,但是它的引用比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。JDK1.2之后提供了WeakReference类来实现软引用。

  • 虚引用 : 虚引用也成为幽灵引用或者幻影引用,它是最弱的一种引用关系。无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用的唯一目的就是能够在这个对象被回收的时候收到一个系统通知。

android 常见内存泄漏原因及解决办法




垃圾回收算法

  1. 标记 - 清除算法
  2. 复制算法
  3. 标记 - 整理算法
  4. 分代收集算法

标记 - 清除算法

标记清除算法是最基础的收集算法,后面的几种算法都是基于这种思路并对其不足进行改进而得到的。
算法分为“标记”和“清除”两个阶段。

  • 标记 : 首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。标记的判定就是通过可达性分析算法分析出可回收对象。

  • 清除 : 标记完成后会回收对象。但是这其中存在着两个不足问题 : 第一是标记和清除这两个过程效率不高。其次是清除之后会产生大量不连续的内存碎片

空间碎片太多可能会导致以后在程序运行过程中需要分配比较大对象时,无法找到足够连续内存。从而提前触发依次GC。

这里写图片描述

相当于生活中标记不需要的物品,然后找几个人把这些东西丢了。
但是由于房间的空间是有限的,如果一直没有整理房间,如果有大件家具搬进来时,没有足够的空间可能要再标记清除一下不需要的物品,如果还是没有办法放下这件大家具,抛出OOM异常。丢(Out)到(Of)门外(Memory)。



复制算法

为了解决效率问题,复制收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。

当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后把已使用过的那一块内存空间全部清理。

这样便不用考虑内存碎片问题,只要移动堆顶指针,重新按顺序分配内存即可,实现简单,运行高效。

只是这种算法的代价是将活动使用的内存缩小为原来的一半。

这里写图片描述

复制算法当存活对象越少(或者垃圾很容易产生)时,它的效率越高。
也就是你只要将一些少量需要用到的对象丢到另一片干净的内存中,就可以轻松的大扫除。
换句话说,如果你需要保留的对象很多,那么便需要又费力又频繁的搬动到另一块内存中。

复制算法特别适用于存活对象少,垃圾对象多的情况。



标记 - 整理算法

复制收集算法在对象存活率较高时就要进行较多的操作,效率就会变得很低。并且只能使用50%的空间。

那么针对存活对象多,垃圾对象少的情况,就可以用“标记 - 整理”(Mark-Compat)算法。

  • 标记 : 因为存活的对象比较多,我们将这些对象标记起来。然后接下来整理到一块。

  • 整理 : 让所有标记过的对象都向一端移动,然后直接清理掉端边界以外的内存。(按内存地址一次排列)

这里写图片描述



分代收集算法

当前很多商业虚拟机都采用分代收集(Generational Collection)算法作为垃圾收集器。这种算法只是根据对象存活周期的不同将内存划分为几块,进而采用不同的回收算法的策略。

一般是把Java堆分成新生代和老年代。

  • 新生代 : 对象朝生暮死,存活的对象少,可回收对象多。
    选用复制算法,只要付出少量存活对象的复制成本就可以完成收集。

  • 老年代 : 对象存活率高,回收的对象很少。
    选用标记 - 清理算法,或者标记 - 整理算法来进行回收。


新生代

对于新生代采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,采用复制算法效率最高。

但实际中并不是按照上面算法中说的1:1的比例来划分新生代的空间的,而是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,比例为8:1:1(比例可以调整)。

每次使用Eden和其中一块Survivor,当回收时,将Eden和刚才用过的Survivor空间中还存活的对象一次性的复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。(后面会继续研究,如果这次复制的对象大小超过Survivor空间,将会有分配担保机制。)

老年代

由于老年代的特点是每次回收都只回收少量对象,使用的是标记 - 清理算法,或者标记 - 整理算法来进行回收。它与新生代的比例为 2(老年代) : 1(新生代)。当然这个比例也是可以调整的。

这里写图片描述


分代收集算法工作流程

分代收集算法工作流程摘自杰风居(写的特别好。)

  • 1 . 分配了一个又一个对象 : 放到Eden区。

  • 2 . 不好,Eden区满了,只能GC(新生代GC:Minor GC)了 : 把Eden区的存活对象copy到Survivor A区,然后清空Eden区(本来Survivor B区也需要清空的,不过本来就是空的)


  • 3 . 又分配了一个又一个对象 : 放到Eden区。

  • 4 . 不好,Eden区满了,只能GC(新生代GC:Minor GC)了 : 把Eden区和Survivor A区的存活对象copy到Survivor B区,然后清空Eden区和Survivor A区。


  • 5 . 又分配了一个又一个对象 : 放到Eden区。

  • 6 . 不好,Eden区满了,只能GC(新生代GC:Minor GC)了 : 把Eden区和Survivor B区的存活对象copy到Survivor A区,然后清空Eden区和Survivor B区

// ...
// ...
// 有的对象来回在Survivor A区或者B区呆了比如15次,就被分配到老年代Old区
// 有的对象太大,超过了Eden区,直接被分配在Old区
// 有的存活对象,放不下Survivor区,也被分配到Old区
// ...
// 在某次Minor GC的过程中突然发现:
// 不好,老年代Old区也满了,这是一次大GC(老年代GC:Major GC)
   Old区慢慢的整理一番,空间又够了
// 继续Minor GC
// ...
// ...


为什么不是一块Survivor空间而是两块?

这里涉及到一个新生代和老年代的存活周期的问题,比如一个对象在新生代经历15次(仅供参考)GC,就可以移到老年代了。问题来了,当我们第一次GC的时候,我们可以把Eden区的存活对象放到Survivor A空间,但是第二次GC的时候,Survivor A空间的存活对象也需要再次用Copying算法,放到Survivor B空间上,而把刚刚的Survivor A空间和Eden空间清除。第三次GC时,又把Survivor B空间的存活对象复制到Survivor A空间,如此反复。
所以,这里就需要两块Survivor空间来回倒腾。


为什么Eden空间这么大而Survivor空间要分的少一点?

新创建的对象都是放在Eden空间,这是很频繁的,尤其是大量的局部变量产生的临时对象,这些对象绝大部分都应该马上被回收,能存活下来被转移到survivor空间的往往不多。所以,设置较大的Eden空间和较小的Survivor空间是合理的,大大提高了内存的使用率,缓解了Copying算法的缺点。
我看8:1:1就挺好的,当然这个比例是可以调整的,包括上面的新生代和老年代的1:2的比例也是可以调整的。
新的问题又来了,从Eden空间往Survivor空间转移的时候Survivor空间不够了怎么办?直接放到老年代去。

猜你喜欢

转载自blog.csdn.net/qian520ao/article/details/79024835