JVM 初步理解


原文地址 [www.zhihu.com\](https://www.zhihu.com/question/20097631/answer/817071740)

如果 JAVA 开发是降龙十八掌,JVM 就是九阳神功,具备九阳神功的内力基础降龙十八掌的威力会被发挥的淋漓尽致。

JVM 的内存结构和类加载机制是玩转 JVM 的入口,如果弄通了接下来的路该怎么走你自然就知道了

下面就从这两个方面来深入解析一下 JVM

1.JVM 内存结构

jvm 内存分为五大块:

标灰的是线程公有的内存区域,没有标灰的是线程私有。

1.1 程序计数器

程序计数器是用来指示当前线程要执行哪条指令,并且在执行完该条指令后让程序计数器指向下一条指令,直到将程序执行完毕。指令需要靠 cpu 来执行,在多线程中,多个线程是通过轮流切换分配 cpu 的时间片而执行的,在切换时需要记录当前执行到了哪条指令以便将来继续执行,每一个线程都需要有自己的程序计数器,所以程序计数器是线程私有的内存。

1.2 虚拟机栈

通常我们把 jvm 的内存粗略的分为堆和栈,其中的栈指的就是虚拟机栈, 虚拟机栈也是线程私有的

虚拟机栈对应的是方法的内存区域,每个方法执行时都会创建一个栈帧,用来存储该方法的局部变量表,操作数栈,动态链接,方法返回地址:

1.2.1 局部变量表

局部变量表中存储的是方法的参数和方法中定义的局部变量,在编译期间就为局部变量表分配好了内存空间。局部变量表中存储三种类型的数据:

(1) 基本数据类型

(2) 引用类型:指向一个对象在内存中的地址

(3) returnAddress 类型:指向指令的地址(已经很少见了,指向异常处理的指令,现在已经由异常表代替)

1.2.2 操作数栈

当虚拟机执行一些指令的时候会对操作数栈进行入栈或出栈的操作,比如 iadd 指令将两个数相加,会先将操作数栈中的两个数弹出来(出栈),相加后再压入栈(入栈)中。

1.2.3 动态链接

在运行时常量池中存储了诸如类名,方法名,我们要找到目标类,执行相应的方法就需要用到动态链接,栈帧中有一个指向运行时常量池的引用,通过这个引用可以找到相应的类名和方法名,但是光知道名称是没法执行方法的,需要通过名称找到相应的类和方法在内存中的地址,这个过程就是动态链接。

1.2.4 方法返回地址

当方法执行完以后如果有返回值,就会把这个返回值返回给该方法的调用者,方法的返回就是我们 java 中用到的 return 命令。方法返回之后调用者需要继续往下执行就需要知道要执行的地址,该地址就是方法返回地址,它被记录在了栈帧中,当然在发生异常的情况下不会有返回值,要继续执行的地址可以通过异常处理器表来确定。

虚拟机栈可能出现两种类型的异常:

1. 线程请求的栈深度大于虚拟机允许的栈深度会抛出 StackOverflowError,(虚拟机栈空间不能动态扩展的情况下)

2. 如果虚拟机栈空间可以动态扩展(目前多数的虚拟机都可以),当动态扩展无法申请到足够的空间时会抛出 OutOfMemory 异常。

1.3 本地方法栈

本地方法栈与虚拟机栈的作用是一样的,区别在于虚拟机栈为虚拟机执行 java 方法服务,而本地方法栈为虚拟机执行 native 方法服务,native 方法为本地方法,不是用 java 语言写的有可能是 c 或者 c++ 写的,在 jdk 中就有很多 c 的代码,就是提供给本地方法来调用的。

1.4 堆

通常我们把 jvm 的内存粗略的分为堆和栈,其中的堆就是指它,它是虚拟机中占用内存最大的一块,是被所有线程共享的一块区域,它是用来存放对象实例的。是垃圾收集器管理的主要区域。

1.5 方法区

方法区也是被所有线程共享的一块区域,它存储的是类信息,常量,静态变量,编译后的字节码等信息。方法区中还有一块区域 “运行时常量池 “:运行时常量池中存储的是编译期生成的各种字面量和符号引用。字面量相当于 Java 里常量的概念,比如字符串,声明为 final 的常量值等,符号引用包括了:类和接口名,字段名,方法名。

2.类加载机制

2.1 java 类的加载过程

编译后的 Java 类是以字节码的形式存在的,它只有被加载到虚拟机内存中才能被使用,它是如何被加载到内存中的呢?

下图为类加载到内存的机制:

2.1.1 加载

在加载(注意和类加载是不同的概念)阶段虚拟机需要完成三件事

(1). 通过一个类的全限定名(类名全称,带包路径的用点隔开,例如: java.lang.String)来获取其定义的二进制字节流(被编译以后的字节码文件就是二进制的)。

(2). 将这个字节流所代表的静态存储结构(字节码文件就是其中一种)转化为方法区的运行时数据结构(能够在虚拟机中存储的结构)。

(3). 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象(用于表示这个类的信息),作为对方法区中这些数据的访问入口。

2.1.2 验证

验证,准备,解析统称为连接,作为连接阶段的第一步,验证的主要作用是保证加载进来的二进制流中的信息是符合当前虚拟机要求的,并且不会对虚拟机的安全造成危害。主要包括:

(1)文件格式验证:主要是验证二进制流是否符合 class 文件格式的规范,并且能被当前的虚拟机处理,例如:主次版本号是否在当前虚拟机处理范围之内,常量池的常量中是否有不被支持的常量类型。只有通过了这个阶段的验证后字节流才会进入内存的方法区中进行存储,从这里我们可以看出加载和验证阶段是交叉进行的,加载还未完成,文件格式验证就已经开始了。

(2)元数据验证:对字节码描述的信息进行语义分析以保证其描述的信息符合 java 语言规范的要求:例如:这个类是否有父类(所有类除了 Object 都应该有父类)

(3)字节码验证:确定程序语义是合法的符合逻辑的,如将子类对象赋给父类类型是符合逻辑的,反之,将父类对象赋给子类类型则是不合法的。

(4)符号引用验证:可以对常量池中各种符号引用的信息进行匹配性校验。例如:符号引用中通过字符串描述的全限定名是否能找到对应的类。

2.1.3 准备

准备阶段会为静态变量分配内存并设置初始值,注意:该初始值为数据类型的零值例如:

Public static int num = 3; 在准备阶段会将 num 值设置为 0 而不是 3. 只有在初始化阶段才会赋值为 3.

2.1.4 解析

解析阶段是把类中的符号引用转换为直接引用的过程:

符号引用:在编译的时候是不知道类所引用的类的具体地址,因此只能使用符号引用来代替,比如:com.Student 类引用了 com.Grade 类,编译时 Student 类并不知道 Grade 类在内存中的实际地址,只能用符号 com.Grade。

直接引用; 引用的实际内存地址。

2.1.5 初始化

初始化阶段是类加载过程的最后一步,在这个阶段会根据程序中的赋值语句给变量赋值,当有继承关系时先初始化父类,再初始化子类。如果该类还没有被加载和连接,那么初始化之前先加加载和连接。

什么时候会进行初始化呢?

1. 使用 new 关键字实例化对象的时候

2. 读取或设置一个类的静态字段的时候

3. 调用一个类的静态方法的时候

4. 对类进行反射调用的时候

5. 当虚拟机启动时执行一个类的 main 方法,会先初始化这个类

2.2 类加载器

加载阶段中的第一步:“通过一个类的全限定名来获取其定义的二进制字节流” 是通过类加载器来完成的,类加载器分为三种:

2.2.1 启动类加载器(BootStrap ClassLoader)

这个类加载器负责将 jdk\jre\lib 下的类库加载到内存中,启动类加载器无法被应用程序直接使用。

2.2.2 扩展类加载器(Extension ClassLoader)

它负责加载 jdk\jre\lib\ext 中的类库。开发者可以直接使用扩展类加载器。

2.2.3 应用程序类加载器 (Application ClassLoader)

它用来加载 classpath 路径(src 路径下的文件在编译后会放到 WEB-INF/classes 路径下。默认的 classpath 是在这里)指定的类。开发者可以直接使用这个类加载器,如果如果应用程序中没有定义自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这三种类加载器互相配合进行加载的,如果有必要还可以加入自己定义的类加载器。

2.3 双亲委派模型

下图为双亲委派模型的类加载器的层次关系:

双亲委派模型的的工作过程是:如果一个类加载器收到了类加载的请求,它会把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时(在自己的加载范围内没有搜索到该类), 子加载器才会尝试自己去加载。例如:

1. 当应用程序类加载器加载一个类时,它会把类加载请求委派给扩展类加载器。

2. 扩展类加载器又把这个类加载请求委派给启动类加载器。

3. 启动类加载器如果加载失败,在 (jdk/jre/lib) 里没有找到这个类,会使用扩展类加载器进行加载。

4. 扩展类加载器如果加载失败,在(jdk/jre/lib/ext)里没有找到这个类,会使用应用程序类加载器来加载

5. 应用程序加载器加载失败则会报:ClassNotFoundException 异常。

使用双亲委派模型的意义:

例如类 java.lang.Object 存放在 rt.jar 中,无论哪个类加载器要加载这个类,最终都会委派启动类加载器来加载,如果没有双亲委派模型用户自己编写了一个 java.lang.Object 类,放到 ClassPath 中,系统中将会出现多个不同的 Object 类,应用程序也会变得混乱。


  TOC