在 《深入理解 Java 虚拟机》一书中,作者将运行时数据区和垃圾收集算法放在开头章节,说明了这两个知识点是进一步学习 JVM 的基础知识点,相比后续的 垃圾收集器和 JMM,它也更加的简单。
运行时数据区
运行时数据区是《Java 虚拟机规范》规定的,Java 虚拟机在运行 Java 程序的过程中划分的数据区域。分为程序计数器、虚拟机栈、本地方法栈、方法区和堆共五个区域。可以从三个方面来总结这些区域,即区域是用来存储什么数据的?区域是线程共享的还是私有的?区域会抛出哪些异常。
-
程序计数器
- 存储线程执行字节码的行号
- 线程私有
-
虚拟机栈
- 存储 Java 程序普通方法的调用栈
- 线程私有
- 可能抛出 StackOverFlowError 和 OutOfMemoryError
-
本地方法栈
- 存储 Java 程序 native 方法的调用栈
- 线程私有
- 可能抛出 StackOverFlowError 和 OutOfMemoryError
-
方法区
- 存储加载的类型信息、运行时常量池
- 线程共享
- 可能抛出 OutOfMemoryError
-
堆
- Java 虚拟机管理的最大的一块内存,用来存储对象实例
- 线程共享
- 可能抛出 OutOfMemoryError
垃圾收集算法
Java 语言和 C/C# 语言最大的区别就是 Java 语言会自动分配和回收内存,内存的分配暂且不聊,内存回收是由 JVM 中的垃圾收集器来提供支持的。在 JVM 中,内存分配的基本单位是对象,所以内存回收也是以对象为单位来回收的。
开发人员在设计垃圾收集器时,为了简化问题,将垃圾收集的过程分成了两步:
- 标记哪些对象需要回收;
- 怎么回收这些对象。
标记内存中哪些对象需要回收的算法有两种,分别是引用计数器法和可达性分析法。
引用计数器法
引用计数器法指的是在每个对象中维护一个计数器,当有一个地方引用它时计数器就加一,当一个引用失效时计数器就减一,当计数器为 0 的时候判定对象是垃圾内存,需要回收。
引用计数器法需要解决循环引用的问题,即两个对象互相引用,其他再没有地方引用它们,这时它们应该算是需要被回收对象才对,但是它们的引用计数器却不是 0。
可达性分析法
可达性分析法指通过一系列被称为 "GC Roots" 的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,在搜索过程中能够遍历到的对象就是存活的对象,没有遍历到的对象就是垃圾对象,需要回收。
值得注意的是,所有的垃圾收集器使用的判断对象是否可回收的算法都是可达性分析法。因为可达性分析法没有循环引用的问题,而要解决引用计数器法的循环引用问题,带来的复杂性和性能消耗可能会得不偿失。
标记-清理算法
确定了哪些内存可以回收之后我们需要确定怎么回收,在 Java 虚拟机发展过程中出现过许多垃圾收集算法。
标记-清理算法正如它的名字一样,它分为标记和清理两个步骤,其中标记是使用前面介绍的可达性分析法将可回收对象标记出来,标记结束后,统一回收掉所有的被标记对象。
标记-清理算法会造成大量不连续的内存碎片,因为给对象分配内存需要连续的内存空间,如果空间碎片太多的话会出现当前总的内存可用空间大于需要分配的空间,但是连续的内存可用空间都小于需要分配的内存,从而导致 JVM 需要提前触发垃圾收集动作。
标记-复制算法
标记-复制算法将内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了之后就将还存活的对象复制到另外一块内存中,然后把一块的内存一次清理掉。
因为每次垃圾收集之后都会将存活对象复制到另外一块内存区域,这里的复制和后续的新对象分配都是从内存区域的开头开始分配的,所以不会存在空间碎片的问题,但是这种算法的缺点也明显,只能使用可用内存空间的一半,空间浪费非常严重。
标记-整理算法
标记-整理算法的标记阶段和标记-清理算法一样,先标记出所有可回收对象,然后让所有存活对象向内存空间的一端移动,最后直接清理掉最后一个存活对象之后的所有内存空间,移动存活对象的时候就像整理一个个货物,所以它被称为标记-整理算法。
标记-整理算法看似比前两种算法都优秀,因为它既没有空间碎片的问题也没有空间浪费的问题,但是在整理过程中需要移动存活对象,移动之后需要更新所有引用这些对象的地方,这是一项非常中的操作。