前言
我们知道, 在java开发中, .java文件会被编译超成一个个.class文件, 最终被JVM加载和运行.
大致流程图如下
什么是类的加载
我们写的java文件保存着业务逻辑代码,
java编译器负责将 .java 文件编译成 .class 文件,
.class 文件中保存着java文件转换后虚拟机将要执行的指令.
当需要某个类的时候, java虚拟机会加载 .class 文件,并创建对应的class对象.
将class文件加载到虚拟机的内存, 这个过程被称为类的加载.
类加载的最终产品是位于堆区中的Class对象,
Class对象封装了类在方法区内的数据结构, 并且向Java程序员提供了访问方法区内的数据结构的接口.
类加载器并不需要等到某个类被“首次主动使用”时再加载它, JVM规范允许类加载器在预料某个类将要被使用时就预先加载它.
如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误).
如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误.
知道了什么类的加载, 下面我们就来了解下JAVA类的生命周期
JAVA类的生命周期
可以看出, JAVA类的生命周期如下:
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
类加载的过程包括了加载、验证、准备、解析、初始化五个阶段.
PS:
验证、准备、解析这三个阶段也被统称为连接阶段
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的;
而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定).
PS:
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段.
加载
加载阶段主要查找并加载类的二进制数据, 类加载器通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个class对象.
在加载阶段, 虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取其定义的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段, 因为开发人员既可以使用系统提供的类加载器来完成加载, 也可以使用自己定义的类加载器来完成加载.
JVM类加载器
类的加载由类加载器完成,类加载器通常由JVM提供.
JVM提供的这些类加载器通常被称为系统类加载器;除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器.
JVM预定义有三种类加载器:
- 启动类加载器(Bootstrap ClassLoader)
用来加载 Java 的核心类,是用原生代码来实现的,并不继承自java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类);
由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
//获得根类加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径
public class ClassLoaderTest {
public static void main(String[] args) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls){
System.out.println(url.toExternalForm());
}
}
}
结果
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/classes
扩展类加载器(Extension ClassLoader)
它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类, 由Java语言实现,父类加载器为null应用程序类加载器(Application ClassLoader)
被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径.
程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器;如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器,由Java语言实现,父类加载器为ExtClassLoader.
类加载器加载Class大致要经过如下8个步骤:
- 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步
- 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步
- 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步
- 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步
- 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步
- 从文件中载入Class,成功后跳至第8步
- 抛出ClassNotFountException异常
- 返回对应的java.lang.Class对象
JVM类加载机制
JVM的类加载机制主要有如下3种:
全盘负责
所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中.这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因双亲委派
双亲委派就是如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成, 若成功则直接返回, 否则继续向上,直到到达最顶层的类加载器;
因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器无法完成该加载请求时,子加载器才会尝试自己去加载该类
JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器.
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象
简而言之:
- 系统类防止内存中出现多份同样的字节码
- 保证Java程序安全稳定运行
JVM类加载方式
JVM有3种类加载方式:
命令行启动应用时候由JVM初始化加载
通过ClassLoader.loadClass()方法动态加载
将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块;
Classloader.loaderClass得到的class是还没有连接(验证、准备、解析)的通过Class.forName()方法动态加载
将类的.class文件加载到jvm中,还会对类进行解释,执行类中的static块; Class.forName()得到的class是已经初始化完成的
实例:
package com.test.classloader;
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()来加载类,不会执行初始化块
loader.loadClass("TestClass");
//使用Class.forName()来加载类,默认会执行初始化块
Class.forName("TestClass");
//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
Class.forName("TestClass", false, loader);
}
}
public class Test {
static {
System.out.println("静态初始化块执行了!");
}
}
验证
验证阶段是为了确保 Class 文件的字节流中包含的信息是符合当前虚拟机的要求,并且不会危害虚拟机自身的安全.
验证大致会完成4个检验动作:
文件格式验证
验证字节流是否符合Class文件格式的规范.
例如: 是否以0xCAFEBABE开头; 主次版本号是否在当前虚拟机的处理范围之内; 常量池中的常量是否有不被支持的类型;元数据验证
对字节码描述的信息进行语义分析(注意: 对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;
例如:这个类是否有父类, 除了java.lang.Object之外;
字节码验证
通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的.符号引用验证
确保解析动作能正确执行.
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响;
如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间.
准备
准备阶段是为static修饰的类变量分配内存, 并设置类变量初始值的阶段;
这些内存都将在方法区中分配; 不包含final修饰的静态变量, 因为final变量在编译时分配.需要注意的是:
- 此时进行内存分配的仅包括类变量(static),而不包括实例变量; 实例变量会在对象实例化时随着对象一块分配在Java堆中.
- 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等)而不是被在Java代码中被显式地赋予的值.
举个例子:
类中定义了变量public static int a =100;
实际上变量 a 在准备阶段过后的初始值为 0 而不是100;(对这句话有疑惑的可以去补充下JAVA数据类型初始值的知识
)
将 a 赋值为 100 的put static
指令是程序被编译后, 存放于类构造器<clinit>()方法之中的.不过注意,如果声明为:
public static final int a = 100;
在编译阶段会为 a 生成 ConstantValue 属性, 在准备阶段虚拟机会根据 ConstantValue 属性将 a 赋值为 100
解析
解析阶段是指虚拟机将常量池中的
符号引用
替换为直接引用
的过程,
主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行.
符号引用就是一组符号来描述目标,可以是任何字面量.直接引用
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄; 如果有了直接引用,那引用的目标必定已经在内存中存在.
符号引用
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中.
各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的; 因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中.
初始化
这里是类记载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量.(前面已经对static 初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化).
JVM负责对类进行初始化,主要对类变量进行初始化.
Java对类变量进行初始值设定的两种方式:
- 声明类变量是指定初始值
- 使用静态代码块为类变量指定初始值
JVM初始化步骤
- 假如这个类还没有被加载和连接,则程序先加载并连接该类
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如Class.forName(“com.test.Test”))
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
注意以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化
- 定义对象数组,不会触发该类的初始化
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类
- 通过类名获取 Class 对象,不会触发类的初始化
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化
- 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作