java 的垃圾回收也就是我们常说的 gc ( garbage collector )。这是一个老生常谈的问题。之所以这么经常被提及,是因为它对 java 性能有很大影响。
在 c/c++ 中考虑到效率,内存的分配和销毁是由程序员来控制的。程序员使用内存必须手动分配,在使用结束后,在手动释放掉已经不用的内存空间。如果忘记释放内存空间,那么这块内存区域就是一直被占用的状态。其他急需内存的程序将无法使用这块内存,导致内存泄漏。
java 中为了减轻程序员的负担,同时为了防止程序员忘记释放内存导致内存泄漏,内存的回收交由垃圾回收器(gc)管理。程序员可以专注于核心业务的编写,不受繁琐的手动分配和释放的打扰。
gc 所做的工作就是,将不再被使用的对象或者不再被引用的对象,所占用的内存空间进行回收。
为了理解 java 的垃圾回收,先来了解一下 java 内存区域
java 的内存区域粗略包括有5个部分
- 方法区(Method Area)
- 虚拟机栈(VM Stack)
- 本地方法栈(Native Method Stack)
- 堆(Heap)
- 程序计数器(Program Counter Register)
程序计数器
程序计数器(pc),是 jvm 的一个抽象概念,和 cpu 中的 pc 概念相似。用来记录当前执行的字节码文件的所在行。用来控制程序的执行顺序,进行流程控制。jvm 中的每个线程都拥有一个 pc ,来记录各自执行到的指令位置。
方法区
方法区在 Hotspot JVM 中也称为 permanent generation。与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。 对于习惯在HotSpot虚拟机上开发、部署程序的开发者来说,很多人都更愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。
虚拟机栈
和 pc 一样,虚拟栈也是线程私有的。生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈
本地方法栈和虚拟机栈相似,但是它是用来记录 Native 方法的。也就是 java 调用的非 java 方法,比如 c/c++
堆
堆是 jvm 内存类中的主要部分,被所有线程共享。所有对象和数组都在这个堆上分配,因此堆是 gc 管理的主要区域。为了有效地实现垃圾回收,堆又被细分为新生代(yong generation)和老年代(old generation),新生代又可以再细致地分为 Eden Space 和 Survivor Space。在 Oracle 的 Hopspot JVM 的描述中,也将方法区称为永久代(permanent generation)。
Hotspot 中 gc
JVM 只是 java 虚拟机概念或者称它为标准。就像 rfc 文档和具体网络协议实现的之间关系。因此,JVM 有很多的实现,比如我们最熟悉的两种实现 OpenJDK 和 HotSpot。
本文讨论的垃圾回收机制以 HotSpot 为例
HotSpot 中的垃圾回收,主要涉及到 heap ,也就是堆区。
HotsSpot 使用一种叫世代回收(Generational Garbage Collection)的方法来实现垃圾回收,本文的后面将介绍为什么要分代
为了实现世代回收,HotSpot 把 heap 分为多个区域
分别为新生代和老年代,对应于垃圾回收的不同过程,使用不同的回收算法
注:
1、有两个垃圾收集过程,minor garbage collection、marjor garbage collection。
2、在进行垃圾回收的时候,会停止所有在运行的线程被称作 " stop world event "
Hotspot 中的垃圾回收有下面几个步骤:
1、新的对象被分配到 eden,两个 survivor 分区在开始状态都是空的
2、eden 空间分配满时,就开始执行第一次的 minor garbage collection。将还在被使用的对象移动到 S0,同时删除 eden 中,不再被使用的对象。标记对象 age 为 1。
3、发生下一次的 minor GC 时,将 eden 区中未被使用的对象删除,并将正在使用的对象移动到 survivor 区。需要注意,这个时候把 eden 对象放到 S1,还把 S0 中还被使用的对象移动到 S1。然后,把 age 加 1。
4、再进行下一次 minor GC 时,将 eden 区中未被使用的对象删除,并移动到 S0 区,同时 S1 移动到 S0, age 加 1
5、当对象的 age 达到某个阈值(通常情况为 8),就进行 marjor GC 。把 age 为 8 且还在使用的对象,从 yong generation 移动到 old generation
6、过程总览
为什么要分代?
根据经验分析得到对象特性:
- 刚创建的对象容易被回收
- 经过几次回收后还保留的对象,被回收的几率会降低
基于这种分析,可以将堆分为新生代和老生代,针对不同的阶段的特点,采用不同的回收算法来提高回收效率。这就时Hotspot JVM GC 的思想
当一个对象经过几次回收后依然存活,对象就会被放入称为老生代的内存空间。在老生代中,几乎所有的对象都是经过几次垃圾回收后依然得以幸存的。因此,可以认为这些对象在一段时期内,甚至在应用程序的整个生命周期中,将是常驻内存的。如果依然使用复制算法回收老生代,将需要复制大量对象。再加上老生代的回收性价比也要低于新生代,因此这种做法也是不可取的。根据分代的思想,可以对老年代的回收使用与新生代不同的标记-压缩算法,以提高垃圾回收效率。
(后续讨论算法)
总结
上面简述了 Hotspot JVM 的 GC,但是没有涉及具体的垃圾回收算法。大概的垃圾回收算法有,引用计数法、标记-清除算法、复制算法、标记-压缩法、增量算法*。浅析 java 垃圾回收(二)—— 回收算法 继续回收算法
参考
[1] Java Garbage Collection Basics
[2] Java深度历险(四)——Java垃圾回收机制与引用类型