一 前言
对于从事C、C++程序开发的开发人员来说,在内存管理领域,他们既是拥有最高权力的“皇帝”又是从事最基础工作的“劳动人民”——既拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任。
对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题,由虚拟机管理内存这一切看起来都很美好。不过,也正是因为Java程序员把内存控制的权力交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会成为一项异常艰难的工作。
本篇将从概念上介绍Java虚拟机内存的各个区域,讲解这些区域的作用、服务对象以及其中可能产生的问题,然后介绍Java中GC机制。
二 运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图所示
2.1 程序计数器(Program Counter Register)
是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。该区域是整个内存中较小的一块。
当前线程私有的,每个线程都有自己计数器,从而线程切换后能恢复到正确的执行位置。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.2 Java虚拟机栈(JVM Stack)
- 线程私有,生命周期与线程相同;
- 是Java方法执行的内存模型,方法在执行的时候会创建一个栈帧存储方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。
- 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程;
- 当线程请求的栈深度大于虚拟机所允许的深度,则StackOverflowError异常;
- 如果栈的扩展时无法申请到足够的内存,则OutOfMemoryError异常。
2.3 本地方法栈(Native Method Stack)
- 线程私有,生命周期与线程相同;
- 本地方法栈作用与虚拟机栈非常相似的,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务;
- 也会抛出StackOverflowError和OutOfMemoryError。
2.4 Java堆(Java Heap)
- 被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存放对象实例;
- Java堆是垃圾收集器管理的主要区域;
- 可以通过-Xmx和-Xms控制堆的大小;
- 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等,Java堆中的上述各个区域的分配、回收等细节下节再介绍。
2.5 方法区(Method Area)
- 线程共享
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 这个区域的内存回收目标主要针对常量池的回收和对类型的卸载。
- 方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
2.6 运行时常量池(Runtime Constant Pool)
- 是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
- 当常量池无法再申请到内存时,则抛出OutOfMemoryError异常。
三 GC机制
上节介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;而Java堆和方法区则不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。后面讨论的GC机制也是针对Java堆和方法区这一部分内存区域。
3.1 内存中的哪些对象可以回收?
3.1.1 堆内存
在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,要确定这些对象之中哪些还“存活”着,哪些已经“死去”。
(1)引用计数算法
就是给每个对象加一个计数器,如果有一个地方引用就加1,当引用失效就减1;当计数器为0,则认为对象是无用的。这种算法实现简单,判定效率也很高,但缺点在于很难解决对象之间相互循环引用的问题。
(2)可达性分析算法
基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
obj8、obj9、obj10都没有到GCRoots对象的引用链,即便obj9和obj10之间有引用链,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
判定对象是否存活都与“引用”有关,Java引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次逐渐减弱。
- 强引用:类似Object obj=new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:描述一些还有用但并非必需的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。提供了SoftReference类来实现软引用。
- 弱引用:也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。提供了WeakReference类来实现弱引用。
- 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。提供了PhantomReference类来实现虚引用。
3.1.2 方法区
主要有两部分:废弃的常量和无用的类。
废弃的常量判断方法和堆中的对象类似,只要判断没有地方引用就可以回收。相比之下,判断一个类是否无用,条件就比较苛刻,需要同事满足下面3个条件才能算是“无用的类”:
- 该类的所有实例都已经被回收,也就是java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对于满足上面三个条件的无用类进行回收,仅仅是可以回收,具体能否回收,JVM提供了-Xnoclassgc参数进行控制。
3.2 垃圾收集算法
GC有多种算法,不同的算法实现了不同的垃圾回收器。
3.2.1 标记-清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
特点:
1.是效率问题,标记和清除两个过程的效率都不高;
2.是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3.2.2 复制算法
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
特点:1.每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,2.只要移动堆顶指针,按顺序分配内存即可,3.实现简单,运行高效。
3.2.3 标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
特点:不会产生空间碎片。
3.2.4 分代收集算法
分代收集算法根据对象存活周期的不同将内存划为几块,一般把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最合适的收集算法。在新生代中,每次垃圾回收时都发现大批对象死去,只有少量存活,那就选用复制算法,付出少量复制成本就可以完成收集。而老年代中对象存活率较高且没有空间进行担保(后面讲新生代的担保分配),就必须使用“标记-清除”或者“标记-整理”算法。
3.3 分代垃圾回收
3.3.1 Young(年轻代)复制算法
年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。
3.3.2Tenured(年老代)标记清除|标记整理
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
3.3.3 Perm(持久代)很难发生GC
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。