虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。
前面的定义已经讲了是加载描述类的数据,也就是Class文件,关于Class文件,我在《深入解析Class类文件的结构》一文中进行了分析。
加载描述类的类文件的二进制流是由类加载器完成的,已有的三种类加载和自定义的类加载器组成了类加载器子系统,关于类加载器,下文会详细讲述。
这就是本文的重点,类加载机制中的类加载流程。
可以通过下图整体上看一下类加载在JVM体系中的位置
类的生命周期共有7个阶段,分别如下图:
前5个阶段属于类加载流程的范围,其中验证、准备、解析又被称为连接,类加载的5个阶段并不是按照顺序依次完成的,除了解析可能会在初始化之后开始,其他的几个阶段的开始顺序是确定的,但结束顺序不一定,可能会交叉着进行,加载还没完成,连接可能已经开始。
类加载分为5个过程,分别是加载、验证、准备、解析、初始化,下面分别对这几个过程进行讲述,尽量简短明了。
"加载"是"类加载"流程的一个阶段
加载阶段主要干的3件事:
在这三件事里,开发人员能干预的是第一件事,我们可以使用系统的三个类加载器去加载我们想要加载的类文件,也可以自定义类加载器去获取二进制字节流。
定义类的二进制字节流不一定是经过编译后存储在磁盘上的.class文件,有可能是以下来源:
Hotspot虚拟机中,Class实例不是在堆上分配空间,而是存放在方法区中,这个实例在代码中可以轻松的获取到,并通过它可以获取代表某个类的各种数据结构。
验证是对输入的字节流进行检查的过程
为什么要有验证这个过程呢?就是因为加载的对象:描述类的二进制字节流,来源广泛,不得不防止它被小人利用,损害虚拟机的正常运行,导致崩溃。所以总共有四个验证过程,分别如下图:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
注意这里是为类变量分配内存,而且是分配在方法区中,实例变量是后面随着实例一起分配在堆上的。
设置初始值也不是代码里赋的值,而是各个数据类型规定的零值,比如基础类型是相应类型不同字节长度的0,引用类型是null。
不是每个类变量都是设置为零值,被final修饰的常量,因为在编译期带有一个ConstantValue属性,属性值则是该常量在代码里赋的值,这个值在准备阶段前就已经确定了,所以在准备阶段设置值的时候,直接取的ConstantValue给类常量。
下面的例子可以很好的了解准备阶段,准备阶段过后,a、b、c分别是多少?
public class Test {
public static int a;
public static int b = 1;
public static final int c = 2;
public void say(){
System.out.println("Hello");
}
}
答案揭晓:0, 0, 2 原因上文里写的很明白
解析是将常量池内的符号引用替换为直接引用的过程
那什么是符号引用和直接引用呢?
符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
解析的时机根据虚拟机实现不同而不同,可以是类加载器加载时解析,也可以是符号引用使用前解析
解析主要是对7类符号引用进行:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符
初始化是执行类构造器
()方法的过程
类初始化阶段是类加载流程的最后一个阶段,是执行
对一个类进行主动引用的时候必须初始化,主动引用的场景如下:
对一个类进行被动引用的时候不初始化,被动引用的场景有下面一些:
实现“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块就叫做类加载器
类加载不仅仅是加载二进制字节码的作用,还起着独立的类名称空间的作用,确定一个类的唯一性由三个因素决定:
下图中各个加载器之间的层次关系被称为类加载器的双亲委派模型
图中可以看到,系统提供了三个类加载器:启动类加载器、扩展类加载器和应用程序类加载器,java程序启动的时候,三个类加载器分别从各自指定的路径中加载所需的类。最下面是开发人员自定义的类加载器,继承自ClassLoader,重写findClass()方法。
一般我们自己写的类是默认由应用程程序加载器加载的,自定义的类加载器的父类加载器默认是应用程序加载器,应用程序加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器,这种父子关系不是一般的继承或实现关系,而是子加载器持有父加载器的引用,是一种组合关系。自定义类加载器时,可以在构造函数中传入指定的父类加载器。
一个类加载器收到了类加载的请求时,它首先会先检查自身有没有加载过这个类,实质就是在JVM的常量池中查找该类的符号引用是否存在,如果有就直接返回,否则把这个请求委派给父类加载器,直至委派给启动类加载器,只有当父类加载器加载失败,子类加载器才会尝试自己去加载。
下面是实现双亲委派模型的主要代码,代码简单易懂:
//ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//加锁,整个类加载期间都持有锁
synchronized (getClassLoadingLock(name)) {
// 首先,检查此类是否已被加载过,是的话直接返回
Class<?> c = findLoadedClass(name);
if (c == null) { //如果没有加载过,则继续
long t0 = System.nanoTime();
try {
if (parent != null) { //有父类加载器,则交给父类加载器加载,递归执行loadClass方法
c = parent.loadClass(name, false);
} else { //没有父类加载器,交给启动类加载器加载,执行一个本地方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 除了启动类加载器之外的类加载器加载类失败抛异常,此处不进行任何处理
}
if (c == null) {
// 父类加载器未成功加载到类,则调用本加载器的findClass方法
long t1 = System.nanoTime();
c = findClass(name);
// 记录一些状态
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//验证解析
if (resolve) {
resolveClass(c);
}
return c;
}
}
虽然易懂,但配合下面的图更容易加深理解,下面是这段代码的数据流程图:
下面按照一般的双亲委派模型来分析,假设是自定义的类加载器调用了loadClass方法,触发了类加载的过程,则下面的过程会依次执行:
下面是自定义类加载器的示例代码:
public class MyClassLoader extends ClassLoader{
private String classpath;
//指定父类加载器的构造函数
public MyClassLoader(String classpath,ClassLoader classLoader) {
super(classLoader);
this.classpath = classpath;
}
//默认父类加载器为应用程序加载器的构造函数
public MyClassLoader(String classpath) {
this.classpath = classpath;
}
//重写findClass,加载类文件,返回类
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classFilePath = null;
String finalName = name.replace(".", "/");
classFilePath = classpath + "/" + finalName + ".class";
Path path = Paths.get(classFilePath);
if (!Files.exists(path)) {
return null;
}
try {
byte[] classData = Files.readAllBytes(path);
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new RuntimeException("Can not read class file into byte array");
}
}
}
最后来讲讲为什么要使用这个模型?用这个模型有什么好处?
采用双亲委派模式的好处之一是类和它对应的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类加载器已经加载了该类时,子类加载器就没有必要再加载一次。
其次是考虑到安全因素,保证java核心api中定义的类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
最新2021整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等
Java中级资料提升以上福利教程领取方式:
1、点赞+评论(勾选“同时转发”)
2、关注小编。并私信回复关键字【19】
(一定要私信哦~点击我的头像就能看到私信按钮了)
页面更新:2024-05-09
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号