【Java】Garbage Collection-垃圾回收

概念

  垃圾回收(Garbage Collection)是一种自动的存储器管理机制,可以减轻程序员管理内存的负担,减少程序员犯错的机会。垃圾回收最早源于LISP语言,而非Java的伴生产物。Java与C/C++语言对于内存的管理,用钱钟书先生的《围城》描述非常形象:外面的人想进来,里面的人想出去。


image.png | center | 380x351

  垃圾回收主要需要解决三个问题:

  • 什么地方的垃圾需要回收
  • 什么时间回收
  • 使用什么方法回收

位置

  在运行时数据区域(Runtime Data Area),内存被分为了5个区域。其中程序计数器、JVM栈、本地方法栈为线程所独有,与线程的生命周期一直,也就是说线程结束后它们也随之消失,因此这3个区域不需要进行垃圾回收。剩下的方法区是进行垃圾回收的区域。

  方法区用于存储已被虚拟机加载的类信息、常量、静态变量等数据,对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,在HotSpot 虚拟机中把它当成永久代来进行垃圾回收。
  主要存放对象实例,是垃圾收集(Garbage Collection)的主要目标,很多时候也被叫做“GC堆”。

垃圾对象

  现在明确了垃圾回收的区域,还需要清楚哪些是垃圾,哪些数据可以被清除。判断一个对象是否可以被回收有两种方式:

  • 引用计数算法(Reference Counting):给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减1。引用计数为 0 的对象可被回收。引用计数算法实现简单、判定效率也很高,但是当两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
objA.instance = objB;
objB.instance = objA;
  • 可达性分析算法:基于图论的理论,找一组对象作为GC Root(根结点),从根节点开始搜索,不可达的对象(根节点与此对象直接没有通路)是可以被回收的。如下图,灰色的对象是不可达的,会被回收。目前主流的商用虚拟机均采用此算法。


image.png | center | 580x208

引用类型

  无论是引用计数算法还是可达性分析算法,判断对象是否存活都与“引用”有关。在JDK1.2之后,Java对于引用的概念进行了扩充,可以分为4种类型的引用。

  1. 强引用(StrongReference):最为普遍的引用类型,只要存在强引用,GC就不会回收。比如使用new关键字创建对象。object obj = new object();
  2. 软引用(SoftReference):描述有用但非必须的对象。如果内存空间足够,GC不会回收,可以继续使用;如果内存空间不足,GC会回收此类对象。JDK1.2后可以使用 SoftReference来实现软引用。
  3. 弱引用(WeakReference):生命周期比软引用更短暂,只要开始GC,使用软引用的对象就会被回收。
  4. 虚引用(PhantomReference):又称为幽灵引用或者幻影引用。不同于其余三种引用,虚引用不会影响对象的生命周期,也无法通过虚引用获得对象的一个实例,主要用来追踪对象被垃圾回收期回收的活动,对象被回收时会收到系统通知。

算法

  算法部分主要解决如何回收的问题,由于不同虚拟机的垃圾回收算法实现各不相同,这里不深入程序实现的细节,介绍几种常用回收算法思想。

  • 标记-清除(Mark-Sweep):基础的GC实现方式,标记需要回收的垃圾对象(上文有介绍,使用可达性分析算法)然后统一清除。

    image.png | center | 580x96

  缺点:标记和清理的效率都不高;会产生大量的内存碎片,可能无法为大对象直接分配内存,从而再触发一次GC。

  • 标记-整理(Mark-Compact):可以解决内存碎片问题,标记过程与“标记-清楚”一样,但是不是直接对可回收对象进行清理,而是将存活对象向一端移动,然后直接清理掉边界以外的内存。
    image.png | center | 580x149

  • 复制(Copy):基本思路是把内存空间分为两部分,每次只使用其中一部分。当内存空间不足,进行GC时,先把存活的对象移动到另一块儿内存中,然后对上一块使用过的内存进行整体上的清理。
    image.png | center | 580x131

  这种方式的回收效率很高,并且可以解决内存碎片的问题,但是它的代价是把内存的可用部分缩小至原来的50%,用空间换取时间。

分代收集

  分代收集没有使用什么新的回收算法,只是根据对象的存活时间划分了不同的垃圾回收区域。当前商用的JVM一般都是采用分代收集与上述垃圾回收算法组合的解决方案。


image.png | center | 568x372

  一般JVM把Java堆分为:新生代和老年代,HotSpot虚拟机把方法区称为永久代(Permanent Generation)。

新生代(Young Generation)

  大部分新创建的对象都分配到此区域,使用“复制”算法进行垃圾回收,在新生代进行的GC称为:Minor GC。新生代大部分的对象存活时间不长,因此使用复制(Copy)算法只需要复制少量的存活对象,无需按照1:1的比例来分配新生代内存。为了更高效的进行GC,新生代中又可以分为三部分:

  • Eden
  • From Survivor
  • To Survivor

  新生代将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和From Survivor,当回收时,将Eden和From Survivor中还存活着的对象都一次性地复制到另外一块To Survivor空间上,然后清理掉Eden和刚使用的From Survivor空间。
  一次Minor GC结束的时候,Eden空间和From Survivor空间都是空的,而To Survivor空间里面存储着存活的对象。在下次MinorGC的时候,两个Survivor空间交换他们的标签,现在是空的“From” Survivor标记成为“To”,“To” Survivor标记为“From”。因此,在MinorGC结束的时候,Eden空间是空的,两个Survivor空间中的一个是空的,而另一个存储着存活的对象。

  HotSpot虚拟机默认的Eden : Survivor的比例是8 : 1,由于一共有两块Survivor,所以每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的容量会被“浪费”。

老年代(Old Generation)

  多次Minor GC后仍然存活的新生代对象会被移动到老年代空间,在老年代进行的GC称为:Major GC或Full GC。老年代通常比新生代空间要大,进行GC的频率较低,可以使用“标记-清理”或者“标记-整理”算法。

时机

  根据分代收集技术,GC可以分为:Minor GC(新生代)和Full GC(老年代),两者回收的触发条件不同。Minor GC的触发条件非常简单,当新生代Eden区的空间满时,就将触发一次Minor GC。Full GC相对复杂,条件如下:

  1. 调用System.gc():此方法的调用会建议JVM进行Full GC,会增加Full GC的频率,从而增加”stop-the-world”(应用程序会停顿,等待GC的完成)的次数。生产环境不建议使用此方法。
  2. 老年代空间不足:

参考资料

  1. 垃圾回收-wiki
  2. GC Algorithms: Basics
  3. Garbage Collection in Java
  4. Understanding Java Garbage Collection
  5. 深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)-周志明
  6. What is Java Garbage Collection? How It Works, Best Practices, Tutorials, and More

猜你喜欢

转载自blog.csdn.net/u013201439/article/details/80300047