程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。因此这几个区域的内存分配和回收都具备确定性,这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而 Java 堆和方法区则不一样,一个接口中多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器关注的是这部分内存。
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。
难以解决对象之间循环引用的问题。
通过一系列的称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
在 Java 语言中,可作为 GC Roots 的对象包括下面几种:
-
虚拟机栈
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中 JNI (即一般说的 Native 方法)引用的对象
-
强引用就是在程序代码之中普遍存在的,类似
Object obj = new Object()
这类的引用。 -
软引用,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。(SoftReference 可以实现软引用)
-
弱引用,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。(WeakReference 可以实现弱引用)
-
虚引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。(PhantomReference 可以实现虚引用)
即使在可达性算法中不可达的对象,也“并不是非死不可”的,这时候它们是处于“缓刑”阶段的,要真正宣告一个对象死亡,至少要经过两次标记过程。
如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()
方法。当对象没有覆盖 finalize()
方法,或着 finalize()
方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定有必要执行 finalize()
方法,那么这个对象将会被放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程区执行它。
一个对象的 finalize()
方法只会被自动自动调用一次。
被称为永久代,垃圾收集效率很低
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类
类需要同时满足下面 3 个条件才能算是 “无用的类”:
-
该类所有的实例已经被回收,也就是 Java 堆中不存在该类的任何实现
-
加载该类的 ClassLoader 已经被回收
-
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。
主要不足有两个地方:
-
效率问题,标记和清除两个过程效率都不高
-
空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多会导致以后在程序运行过程中需要分配较大对象时,无法找到连续内存而不得不提前触发另一次垃圾收集动作。
将内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存使用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
因为新生代中的对象 98% 都是“朝生夕死”,所以将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性地复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才使用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 是 8 : 1。
如果另一块 Survivor 空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
标记过程仍与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。
当前商业虚拟机的垃圾收集机制都采用“分代收集”算法,这种算法只是根据对象存货周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根绝各个年代的特点采用最适合的收集算法。
在新生代使用复制算法,在老年代使用“标记-清理”或者“标记-整理”算法进行回收。
一个单线程的新生代收集器,但它的“单线程”的意义不仅仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾回收的时候,必须暂停其他所有的工作线程。
Serial 收集器的多线程版本,新生代收集器
新生代收集器。
Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量,所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 消耗时间的比值,即吞吐量等 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。
Serial 收集器的老年代版本,它同样是以一个单线程收集器,使用“标记-整理”算法。
Parallel Scavenge 收集器的老年代版本。
CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。
CMS 收集器是基于“标记-清除”算法实现的。
- 并发与并行
- 分代收集
- 空间整合
- 可预测的停顿
-
对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将会发起一次 Minor GC。
-
大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。
-
长期存活的对象将进入老年代
虚拟机给每个对象定义一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1。对象在 Survivor 区中每“熬过”一次 Minor GC,年龄就增加了一岁,当它的年龄增加到一定程度(默认为 15 岁),将会被晋升到老年代中。
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄的对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。