程序计数器、虚拟机栈、本地方法栈这三个区域的内存,当方法结束,或线程结束时,内存自然就跟随着回收了。
而 Java 堆区和方法区有显著的不确定性,垃圾回收器所关注的正是这部分内存的管理。
判定对象是否存活
引用计数算法
Java 虚拟机没有选用引用计数算法进行内存管理,该算法需要配合大量额外处理才能保证正确工作,比如单纯的引用计数很难解决对象循环引用的问题。
Python 语言使用的时引用计数算法进行内存管理。
可达性分析算法
当前主流的商用程序语言(Java、C#、Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活。
思路:
通过一系列称为『GC Roots』的根对象作为起始结点集,从这些结点开始,根据引用关系向下搜索,搜索过程走过的路径称为『引用链』(Reference Chain),如果某个对象到 GCRoots 间没有任何引用链相连,或者用图论的话就是从 GC Roots 到这个对象不可达时,则证明此对象时不可能再被使用的。
GC Roots
在 Java 技术体系里面,固定可作为 GC Roots的对象包括:
- 虚拟机栈中引用的对象,比如各个线程使用到的参数、局部变量、临时变量
- 方法区中静态属性引用的对象,比如 Java 类引用类型静态变量
- 方法区中常量引用的对象,比如字符串常量池里的引用
- 本地方法栈中本地方法引用的对象
- 虚拟机内部的引用,比如基本数据类型对应的 Class 对象,一些常驻的异常对象等,还有系统类加载器
- 所有被同步锁(synchronized 关键字)持有的对象
- 反应虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
引用
在 JDK1.2之前,Java 里面的引用是传统的定义:
- 如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该 reference 数据是代表某块内存、某个对象的引用。
这种定义下只有『被引用』或者『未被引用』两种状态。
JDK1.2版之后,Java 对引用的概念进行了扩充,将引用分为以下四种
强引用
强引用是最传统的引用的定义,指代码中的引用赋值的引用关系。任何情况下,只要强引用关系存在,垃圾收集器就不会回收被引用的对象
软引用
用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发声内存溢出异常前,回吧这些对象列进回收范围中进行二次回收,如果这次回收后还没有足够内存,才会抛内存溢出异常。
弱引用
也是用来描述非必须对象,但是强度比软引用更弱,只被弱引用关联的对象只能生存到下一次收集为止,无论内存是否够,都会被回收。
虚引用
也称为『幽灵引用』或『幻象引用』,它是最弱的引用关系。一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
设置虚引用的唯一目的是为了能在这个对象被回收时收到一个系统通知。
垃圾收集算法
垃圾回收算法从如何判定对象消亡的角度出发,垃圾回收器算法可以划分为『引用计数式垃圾收集』和『追踪式垃圾收集』。
Java 各个平台的虚拟机操作内存的方法都有差异,绝大部分都采用追踪式垃圾收集。
HotSpot 虚拟机提供了种类繁多的垃圾回收器。
分代收集理论
基于两个假说:
- 绝大多数对象在短时间内会不可达
- 熬过越多次垃圾收集过程的对象越难消亡
新生代和老年代
新生代:新建的对象放在这里。因为大多数对象很快变得不可达,所以大多数对象在新生代中创建,然后消失。当对象国在这块内存区域消失时,就是发生了一次”minor GC”。
老年代:没有变得不可达,存活下来的新生代对象被复制到这里。这块区域一般大于新生代。GC 发生的次数比新生代少。对象从老年代消失时,就是发生了一次”major GC”。
新生代有 3 块空间,1 块为 Eden 区,2 块为 Survivor 区。各个空间的执行顺序:
- 绝大多数新创建的对象分配在 Eden 区。
- 在 Eden 区发生一次 GC 后,存活的对象移动到其中一个 Survivor 区。
- 一旦一个 Survivor 区已满,存活的对象移动到另外一个 Survivor 区。然后之前已满的 Survivor 区将被清空。
-
Mark-Sweep(标记-清除)算法
分为两个阶段:
标记阶段:标记出所有需要被回收的对象
- 清除阶段:回收被标记对象所占用的空间
Copying(复制)算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活着的对象复制到另外一块,然后再把已使用的内存空间一次清理掉,这样就不容易出现内存碎片问题。
缺点:能够使用的内存缩减到原来的一半。如果存活数量多,效率会大大降低。
Mark-Compact(标记-整理)算法
该算法在标记阶段和 Mark-Sweep 一样,但是在完成标记后,不是直接清除可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
内存利用率比复制算法更高。
Generational Collection(分代收集)算法
分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。
根据对象存活的生命周期划分为若干个不同的区域。
一般情况下将堆分为老年代和新生代,
老年代的特点是每次回收只有少量对象被回收,
新生代每次都有大量对象需要被回收,
根据不同代的特点采取最合适的收集算法。
- 目前大部分垃圾收集器对于新生代都采取 Copying 算法,因为新生代每次垃圾回收都要回收大部分对象,也就是需要复制的操作次数较少,但实际并不是按照1:1来划分新生代对象的:
一般来说是将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,进行回收时,将 Eden 和 Survivor中还存活的对象复制到另一块 Survivor 空间中,然后清理掉 Eden 和刚才使用过的 Survivor 空间。