这里介绍Java虚拟机JVM的基础知识
内存区域划分
在Java程序运行过程中,JVM(Java虚拟机)会将其管理的内存划分为若干个数据区域。根据Java虚拟机规范,其内存区域划分如下所示。为了方便记忆,可以利用口诀——“两栈一PC,堆加方法区” ,前半句均为线程私有,后半句均为线程共享
Heap 堆
该内存区域用于存放对象实例、数组。该区域随着JVM的启动而建立,通常情况下该内存区域是JVM所管理的内存中最大的一块。显然该内存区域是被所有线程所共享的
Method Area 方法区
其区域用于存放已经被虚拟机加载的类信息等数据。同样该区域也是线程共享的。对于HotSpot虚拟机而言,在JDK 1.8之前,很多人习惯把方法区称呼为永久代。但是要注意的是二者在本质上并不等价,仅仅是因为当时是通过永久代(Permanent Generation)来实现方法区的。而在JDK 1.8之后,永久代这一说法就成为了历史,转而通过使用本地内存的Metaspace元空间来实现方法区
Program Counter Register 程序计数器
程序计数器又被称作PC,其是线程私有的一块很小的内存区域,用于指示当前线程执行的Java方法字节码的地址信息。如果当前线程执行的为Native方法,则其为空
VM Stack 虚拟机栈
该内存区域同样为线程私有,其生命周期与该线程生命周期一致。用于存放该线程中Java方法执行过程的信息。具体地,Java方法的调用、结束即对应着一个Stack Frame栈帧(其包含了局部变量表、操作数栈、动态链接、方法出口等信息)在虚拟机栈的入栈、出栈过程
Native Method Stack 本地方法栈
在一个Java程序中通常还会包含一些Native方法的调用,则Native Method Stack 本地方法栈就是为其服务的。可以看到,VM Stack与Native Method Stack作用是类似的,只是服务的对象不同,前者为线程的Java方法,后者则为线程的Native方法。故其同样也是线程私有的
至此,我们就将JVM中的大致内存划分介绍完毕了。为了方便记忆,可以利用口诀——“两栈一PC,堆加方法区” ,前半句均为线程私有,后半句均为线程共享
垃圾判定算法
垃圾回收机制的第一步,就是需要判断对象是否可以被回收,即垃圾判定
Reference Counting 引用计数算法
引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。具体地,通过给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;当计数器为0时,则该对象将不可能再被使用,故可判定为垃圾。可以看到该算法较为简单,故在很多语言中得到了的广泛使用,比如Python、PHP、C++11的智能指针std::shared_ptr等。但是该算法的弊端同样明显突出,其很难解决对象间的循环引用问题,让我们来看下面的一个例子
1 | public class RefCountGc { |
在Step 1中new了两个对象并赋值给相应的引用变量,此时两个对象的计数分别为1;然后通过Step 2让两个对象之间进行循环引用,此时引用计数均为2,最后Step 3进行显式地置null,此时两个对象的引用计数均减为1,因为两个对象彼此之间依然还存在相互引用。虽然这两个对象现在已经无法被外部访问获取到了,但是由于各自的引用计数均不为0。故导致其无法被判定为垃圾,更别说进行后续的回收工作了。故在主流的Java虚拟机中不使用该算法进行垃圾的判定
Reachability Analysis 可达性分析算法
通过一系列的称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,即从GC Roots到该对象不可达,此时则说明该对象不可用、可被回收。在Java、C#等语言中均使用该算法来进行判定
具体地,在Java中可作为GC Roots的对象,常见的有:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
- 类中静态属性引用的对象,如Java类的引用类型静态变量
- 常量引用的对象,如字符串常量池
- Native方法引用的对象
- JVM内部的引用,如基本数据类型对应的Class对象、常驻的异常对象(如NullPointExcepiton、OutOfMemoryError等)、系统类加载器
- 被synchronized持有的锁对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
Garbage Collection 垃圾回收机制
分代收集思想
使用过C++和Java的开发者,最大的感触就在于前者经常需要开发者手动释放内存,而后者则依托GC(Garbage Collection)垃圾回收机制实现内存管理。大多数JVM垃圾收集器的设计均采用了分代收集的思想。在分代收集的思想中其依据如下两个假设:
- 弱分代假说(Weak Generational Hypothesis) : 绝大多数对象都是朝生夕灭的
- 强分代假说(Strong Generational Hypothesis) : 熬过垃圾收集过程次数越多的对象越难以消亡
故在此思想的指导下,垃圾收集器对Heap堆内存进行区域细分,然后根据对象的年龄(即对象熬过垃圾收集过程的次数)分配到合适的内存区域中。具体地,将Heap堆分为Young Generation新生代、Old Generation老年代,二者内存大小比例默认为1:2。至于它们的用途简单来说,前者是通常用于存放刚刚new出来的对象,而后者通常用于存放年龄较大的对象
而对于Young Generation新生代而言,又继续细分为三部分:Eden区、Survivor 0区(或称作From区)、Survivor 1区(或称作To区),三者之间内存大小比例默认为8:1:1。
Mark-Sweep 标记-清除算法
该垃圾收集算法很简单,其分为两个阶段
- 标记:先通过垃圾判定算法标记需要回收的对象
- 清除:再统一回收所有被标记对象的内存
该算法简单易实现,但是缺点也很明显
- 执行效率不稳定:标记、清除两个过程的执行效率会随着垃圾数量的增长而降低
- 内存空间的碎片化:该算法会导致大量不连续的内存碎片。一旦需要为大对象分配内存空间而又找不到足够的连续内存时,则不得不提前触发一次垃圾收集过程
标记-复制算法
该算法通常被简称为复制算法。其思想是将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完时,将还存活着的对象复制到另一块上,然后再把此块内存空间一次性清理掉。在复制过程中通过顺序分配来避免出现内存碎片
在需要回收大量对象的条件下,该方案清理效率较高,因为只需要执行少量的对象复制操作。但是该方案的代价同样较为高昂,其使得实际可用的内存空间缩小了一半
而实际上,大部分采用分代收集思想的Java虚拟机均是采用该算法来对新生代进行收集的。前面我们提到弱分代假说,而事实上经过IBM公司的研究也确实佐证了这一点——即新生代中的对象有98%熬不过第一轮收集过程,所以对于新生代空间而言,没有必要按照上面的方案进行1:1的分配,而是将新生代分为一块较大的Eden空间和两块较小的Survivor空间(Survivor 0、Survivor 1)。其垃圾收集过程基本流程如下
- 每次构造对象、分配内存时只使用Eden和其中一块Survivor(记为A),另一块Survivor(记为B)作为保留区域
- 当进行垃圾收集时,先将Eden和刚刚用过的Survivor区(即A)中仍然存活的对象复制到另外一块Survivor(即B)当中
- 然后直接清理掉Eden和刚刚已用过的那块Survivor区(即A)
- 此时构造对象、分配内存时,将只使用Eden和上一次存放存活对象的Survivor区(即B),而将刚刚清理掉的Survivor区(即A)作为本轮的保留区域。即两个Survivor区轮流改变角色(使用区域、保留区域)
对于JVM Heap堆区域的新生代而言,比较适合采用该算法进行垃圾收集。即使在某次收集过程中发现存活对象数量过多,导致作为保留区域的Survivor区无法全部放下也不必担心。因为堆中还有老年代的内存空间可以进行兜底
标记-整理算法
该垃圾收集算法和标记-清除算法在有点相似,二者在标记阶段的操作是一样的。不同的地方在于标记阶段完成之后,该算法是将所有存活的对象都向内存空间的一端移动,然后对于边界之外的内存直接清除。相比较于标记-清除算法而言,其可以避免出现内存空间的碎片化问题
分代收集算法
在分代收集的思想指导下,通常会把Heap分为新生代、老生代。而所谓分代收集算法并不是某种具体的垃圾收集算法,其指的是根据各年代区域中对象的特点选择最适当的垃圾收集策略。比如在新生代中一般使用标记-复制算法,而对于老年代由于没有其他内存空间可以为其兜底,故只能选择标记-清理算法或标记-整理算法
类加载机制
类的生命周期
Java类从被加载到JVM到卸载出内存的生命周期如下,其中连接具体包含:验证、准备、解析。
加载 → 连接 → 初始化 → 使用 → 卸载
Parents Delegation Model 双亲委派模型
类加载器用于进行Class类文件的加载,为此系统提供了三个默认的类加载器。与此同时,用户还可以进行定义自己的类加载器,即自定义类加载器
- BootStrapClassLoader(启动类加载器) : 其是唯一一个由JVM提供的基于C++实现的类加载器。其主要负责加载%JAVA_HOME%/lib目录中、-Xbootclasspath参数所指定路径下的类库。当然其有一定识别校验机制,对于文件名不符合要求的类库即使放到lib目录下也不会被加载
- ExtClassLoader(扩展类加载器) : 该加载器负责负责加载%JAVA_HOME%/lib/ext目录中、系统变量java.ext.dirs 所指定路径下的类库。该加载器是基于Java实现的,其目的在于允许用户将通用性的类库放在ext目录中以拓展JavaSE的功能
- AppClassLoader(应用程序类加载器) : 其又被称作为系统类加载器,该加载器负责加载用户classpath类路径中所有的类库。通常其也是我们应用程序中默认的类加载器
各类加载器之间的层次关系被称为类加载器的Parents Delegation Model双亲委派模型。在双亲委派模型中,当一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。通过双亲委托模型,可以避免出现类的重复加载
Note:
- 类加载器之间的父子关系一般不是通过继承的方式来实现的,通常是通过组合的方式来实现的
- 双亲委派模型中的”双亲”一词,是由于parents翻译的缘故,并不是指一个类加载器有两个父类加载器
参考文献
- 深入理解Java虚拟机·第2版 周志明著
- 深入理解Java虚拟机·第3版 周志明著