JVM(Java 虚拟机)
1.简介
1.1 概述
JVM 是运行操作系统之上,没有和硬件有直接交互,但有可以通过接口调用来与底层操作系统交互。
Java 能够实现一次编译,全平台运行,就是通过 JVM 来实现。通过 JVM 可以让相同的数据类型在不同的系统上运行得到统一的结果。
1.2 组成
- 程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号器,用于记录当前虚拟机正在执行的线程指令地址
- 虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出 StackOverFlowError
- 本地方法栈:线程私有的,保存的是 native 方法的信息,当一个 jvm 创建的线程调用 native 方法
后,jvm 不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法 - 堆:java 堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该
区域经常发生垃圾回收的操作 - 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据
程序计数器、虚拟机栈、本地方法栈为线程私有,堆、方法区为线程共享。
2.ClassLoder 类加载器
2.1 概述
负责将接收到的 class 文件到方法区去找相应的模版(类)进行实例化,有三种 JVM 自带的类加载器和用户自定义加载器。
加载的类信息存放于称为方法区的内存空间,常量池运行时加载到内存中,即运行时常量池(1)。
(1):在 java 1.8 以后方法区通过元空间实现,方法区存放类的信息存放到了元空间、运行时常量池存放到堆中。
运行时常量池在 Java 程序运行期间是可以动态放入新的值到常量池中,典型的方式就是 String 中的 intern() 方法。
常量池无法申请到内存空间后,会抛出 OutOfMemoryError(OOM)
2.2 类加载过程
类的整个生命周期包括:加载(Loading)、验证(Verification)、准备 (Preparation)、解析 (Resolution)、初始化 (Initialization)、使用(Using) 和 卸载(Unloading) 7个阶段。
其中准备、验证、解析3个部分统称为连接(Linking)。如图所示:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)。
2.2.1 加载(Loading)
-
通过一个类的全限定名获取定义此类的二进制字节流
JVM 虚拟机并没有指明二进制字节流获取途径,因此许多如动态代理、JSP 等就可以从这里生成 class 文件,让虚拟机运行。
-
将这个字节流所代表的的静态存储结果转化为方法区的运行时数据结构
-
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
首先加载该类的父类
加载阶段中的加载和链接可能是交替运行
2.2.2 验证 (Verification)
-
验证
验证类是否符合jvm规范,安全性检查
-
准备
为成员(static)变量分配空间,并设置默认值(默认值不是用户指定的值,而是系统对于该变量的默认值,如:int类型的默认值为0)
- 空间分配和赋值两个阶段
在后面的初始化阶段(即cinit阶段,调用类的构造方法)才会进行赋值,如果加上了 final 修饰符,如:static final int a = 10;会在编译阶段就会进行赋值(基本数据类型和字符串常量适用这个规则)
注意:在 jdk6 之前是将静态变量存储在方法区,jdk7 以后是和类一起存储在堆空间中
-
解析
将常量池中的符号引用解析为直接引用
将代码中的符号解析的引用类型执行具体的内存地址
2.2.3 初始化 (Initialization)
初始化阶段是执行类构造器方法()的过程。此方法不需要定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来,自动生成。
如果没有类变量和静态代码块,也不会有clinit
1.导致初始化的情况
-
main 方法所在的类,总会被首先初始化
-
首次使用这个类的静态方法或静态变量时会初始化
-
子类初始化,会一同进行父类的初始化
-
子类访问父类的静态变量,只会触发父类的初始化
-
Calss.forName
如:驱动加载
-
new 实例会导致类的初始化
2.不会导致初始化的情况
- 访问类的 static final 静态常量(上面的加载过程中的准备阶段加载的常量,基本数据类型和字符串常量)不会触发初始化
- 调用类对象 .class 属性不会触发初始化
- 创建该类的数组不会发生初始化
- 调用类的 loadClass 方法不会初始化
- Class.forName 的设置第二个参数为 false 时不会初始化
最后就是使用和卸载阶段。
2.3 类加载器分类
2.3.1 系统自带
-
Bootstrap:启动类加载器
加载 Java 的基本的运行环境,Java 诞生时自带的类(基础类库)通过这个加载,如:object 类
-
Extension:扩展类加载器
除去基本的运行环境中含有的方法以外,在后续的更新中添加的一些额外扩展的类库,通过这个方式加载
-
AppClassLoader:应用程序类加载器
Java 也叫系统类加载器,加载当前应用的 classpath 的所有类,用于加载用户自定义的类
2.3.2 自定义加载器
概念上,将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义加载器。用户可以自己定义加载器的加载顺序和使用。(1)
(1):关于顺序,后面会讲解双亲委派机制,就可以了解到 JVM 内部的加载器的顺序是固定的且有要求的。
2.3.3 加载器顺序实例
public class