1. 了解下 Java 中内存区域的划分
Java 虚拟机在执行 Java 程序的过程中,会把它所管理的内存划分为若干个不同的数据区域。如图所示:
-
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的信号指示器。
每条线程都需要一个独立的程序计数器,是为了线程切换后能恢复到正确的位置。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
-
Java 虚拟机栈
Java 虚拟机栈是线程私有的,生命周期与线程相同。虚拟栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型、对象引用和 returnAddress 类型。
局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的的大小。
-
本地方法栈
本地方法栈为虚拟机使用到的 Native 方法服务。
-
Java 堆
Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
Java 堆是垃圾收集器管理的主要区域。
-
方法区
方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
内存回收目标主要是针对常量池的回收和对类型的卸载
-
运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭。
栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作。每一个栈帧中分配多少内存是在类结构确定下来时就已知的,这几个区域的内存分配和回收多具备确定性。这几个区域不需要过多考虑回收的问题,方法结束或者线程结束时。内存就自然跟随着回收了。
Java 堆和方法区,只有在程序运行期间才能知道会创建哪些对象,内存的分配和回收都是动态的。
2. JVM 在进行垃圾回收之前,需要判断哪些对象是需要回收的
-
引用计数算法
给对象中添加一个的引用计数器, 每当有一个地方 引用它时, 计数器值就加 1; 当引用失效时, 计数器值就减 1; 任何时刻计数器为 0 的 对象 就是不可能再被使用的。
public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024*1024; private byte[] bigSize = new byte[2 * _1MB]; public static void main(String[] args) { testGc(); } private static void testGc() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objA.instance = objA; objA = null; objB = null; System.gc(); } }
弊端:很难解决对象之间相互循环引用的问题。
-
可达性分析算法
通过一系列的称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索。搜索所有走过的路径称为引用链,当一个对象到 GC Roots 对象没有任何引用链相连,则证明此对象是不可用的。
可作为 GC Roots 的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法中 JNI 引用的对象
回收方法区
方法区(永久代)的垃圾收集主要回收两部分:废弃常量和无用的类。
无用的类判定条件:
该类所有的实例都已被回收。
加载该类的 ClassLoader 被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3. 通过垃圾收集算法进行垃圾回收
-
标记清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
标记清除算法不足:
-
效率问题
标记和清除两个过程的效率都不高。
-
空间问题
标记清除之后会产生大量不连续的内存碎片, 空间碎片太多可能会导致以后在程序运行 过程中 需要分配较大对象时,无法找到足够的连续内存 而不得不提前触发另一次垃圾收集动作。
-
-
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还活着的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉。
-
标记整理算法
标记过程仍然与标记清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
-
分代收集算法
根据对象生活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或标记整理算法来 实现。
参考资料「 深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)」