类加载、连接和初始化
类的生命周期
- 类加载:查找并加载类文件的二进制数据
- 类连接:将读入内存的类合并到JVM运行时环境中,包含如下几个步骤:
- 验证:确保被加载类的正确性
- 准备:为类的静态变量分配内存,并初始化他们
- 解析:将常量池中的符号引用转化成直接引用
- 类初始化:为类的静态变量赋初始化值
类加载
类加载概述
类加载步骤
- 通过类的全限定名来获取该类的二进制字节流
- 将二进制字节流转化为方法区运行时数据结构
- 在堆上创建一个
java.lang.Class
对象,用来封装类在方法区内的数据结构,并想外提供访问方法区内数据结构的接口
类加载方式
- 静态加载:从本地文件系统,jar等归档文件中加载
- 动态加载:将java源文件动态编译成class
- 网络加载:从网络下载、转悠数据库中加载
类加载器
类加载器概述
Java虚拟机自带的类加载器包括如下几种:
平台类加载器在JDK8叫扩展类加载器(ExtensionClassLoader),原因是JDK8中该加载器主要是去加载jre环境下的
lib/ext/*.jar
,当想扩展Java功能时,将jar包放到该文件夹下,这种方式并不安全;扩展功能由JDK9之后模块化功能取代
- 启动类加载器(BootstrapClassLoader):根加载器,用于加载基础模块,比如:
java.base
、java.management
、java.xml
模块等,JKD8用于加载jre环境下的lib/*.jar
或-Xbootclasspath
参数指定的路径中且JVM能识别的类库(按照名字识别) - 平台类加载器(PlatformClassLoader):用于加载一些平台相关的模块,比如:
java.scripting
、java.compiler*
、java.corba*
模块等,JKD8用于加载jre环境下的lib/ext/*.jar
或java.ext.dirs
系统变量所指定的类路径中所有类库 - 应用程序类加载器(AppClassLoader):用于加载应用级的模块,以及
classpath
路径下的所有类库,比如:jdk.compiler
、jdk.jartool
、jdk.jshell
等,JKD8用于加载classpath
路径中所有类库
用户可以自定义类加载器,即java.lang.ClassLoader
的子类,用户可定制类加载方式,自定义加载器执行顺序在所有系统加载器之后执行
Java程序不能直接引用启动类加载器,直接设置classLoader
为null,默认就使用启动类加载器;类加载器无需等到某个类首次使用时才加载,JVM规范允许预测需要使用的某个类时可预先加载;若加载Class文件缺失时,会在该类首次主动使用时报LinkageError
错,若为使用该缺失的Class文件则不会报错
自定义类加载器
继承ClassLoader
复写findClass()
方法
import java.io.*;
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) {
byte[] data = new byte[0];
InputStream inputStream = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
name = name.replace(".", "/"); //将路径分隔符替换为斜杠
try {
inputStream = new FileInputStream(new File("target/classes/" + name + ".class")); //读取编译后的class文件
int i = 0;
while ((i = inputStream.read()) != -1) {
outputStream.write(i);
}
data = outputStream.toByteArray(); //将读入的内容转化成byte数组
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return this.defineClass(name, data, 0, data.length); //将读入的二进制数组转化成Class
}
}
使用下面代码进行测试
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader myClassLoader = new MyClassLoader(); //创建ClassLoader对象
Class service = myClassLoader.findClass("Service"); //调用复写的findClass()方法,传入要加载的全类名
System.out.println(service.getClassLoader()); //查看类加载器
System.out.println(service.getClassLoader().getParent()); //查看类加载器的父加载器
}
}
双亲委派模型
双亲委派模型概述
JDK8由于无模块化,双亲委派模型少了第一步,而是直接委派给父类加载器
JVM中的ClassLoader通常采用双亲委派模型,要求除BootstrapClassLoader
外,其余的类加载器都应该有自己的父级加载器,这里的父子关系是组合而不是继承,工作流程如下:
- 一个类加载器接收到类加载请求之后,首先搜索内建加载器定义的所有具名模块
- 若找到合适的模块定义,将会使用该类加载器来加载
- 若未找到,将会委托给父级加载器,直到根加载器
- 若父级加载器反馈不能完成类加载请求,子类加载器才会自己加载,即在父级搜索路径下也找不到要加载的类,子类加载器才从
classpath
中进行加载 - 在类路径下找到类将成为这些加载器的无名模块
简单的说,双亲委派模型就是从子级类加载器开始,先在自己的模块化中找,若找不到委派给父级类加载器,直到根加载器,若根加载器都没找到,在给子类加载器反馈,子类加载器再从自己的classpath
中找找,一路下来都无法找到就会抛出ClassNotFoundException
异常
双亲委派模型说明
- 双亲委派模型保障了Java程序的稳定运作
- 公用且具有一致性,也就是说同样的类、公用的类有且只会加载一次
- 安全,对于已经加载的系统级别的类,无法在加载同包同名的类,保证了系统类不会被恶意修改
- 由于实现双亲委派的代码在
java.lang.ClassLoader
的loadClass()
方法中,若自定义类加载器的话,建议复写findClass()
方法 - 若一个类加载器能加载某个类,对于加载的这个类来说,该类加载器就是定义类加载器,所有能成功返回该类的Class对象的类加载器都能被称为初始化类加载器
- 若为指定父加载器,默认是根加载器
- 每个类加载器都有自己的命名空间,命名空间由该类加载器及其所有父加载器所加载的类构成,不同的命名空间,可出现类全限定名相同的情况(是对于同时启动两遍程序的情况,这两套程序运行时类加载器的类限定名是相同的)
- 运行时包由同一个类加载器的类构成,决定两个类是否属于同一个运行时包,不仅要看全限定类名是否一致,还要看定义类加载器是否相同,只有属于同一个运行时包的类才能实现相互包内可见
破坏双亲委派模型
双亲委派模型问题:都是子委派給父,导致父加载器无法向下识别子加载器加载的资源
- 对于实现热替换,对于OSGI的模块化热部署,他的类加载器不再严格按照双亲委派模型,很多可能就在平级的类加载器中执行
- 对于SPI(供应商扩展接口,只定义接口未定义实现,比如说JDBC加载驱动)来说,只能在运行期间由子加载器加载资源
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class aClass;
aClass = Class.forName("java.sql.DriverManager");
System.out.println(aClass.getClassLoader()); //由ClassLoaders$PlatformClassLoader加载
aClass = Class.forName("com.mysql.cj.jdbc.Driver");
System.out.println(aClass.getClassLoader()); //由ClassLoaders$AppClassLoader加载
}
}
从DriverManager
的源码中可以看到,使用的是线程上下文类加载器解决了该问题,使用实际装在驱动类的ClassLoader加载
//...
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) { //当类夹杂去是平台加载器或根加载器时
callerCL = Thread.currentThread().getContextClassLoader(); //使用从线程中传上来的类加载器
}
//...
类连接
验证
- 类文件结构检查:按照JVM规范规定类的文件结构进行
- 元数据验证:对字节码描述的信息进行语义分析,保证符合Java语言规范要求,比如类是否允许被继承,接口方法是否实现等
- 字节码验证:通过对数据流和控制流进行分析,确保程序语义是合法和符合逻辑的,主要对方法体进行校验,比如数据类型是否匹配,逻辑控制是否合法等
- 符号引用验证:对类自身以外的信息,即常量池中各种符号引用,进行匹配校验,比如检查该类是否存在,是否有访问权限等
准备
就是为类的静态变量分配内存,并初始化他们
解析
- 将常量池中的符号引用转化成直接引用的过程,主要针对类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符
- 符号引用:用一组无歧义的符号来描述所引用的目标,与JVM的实现无关
- 直接引用:直接指向目标的指针、相对偏移量、或是能间接定位到目标的句柄,是和虚拟机实现相关的
类初始化
就是为类的静态变量赋初始值,或者说是执行类构造器<clinit>
方法的过程
- 若类还没有加载和连接,就先加载和连接
- 若类存在父类,且父类没有初始化,就先初始化父类
- 若类存在初始化语句(静态代码块、静态变量),就依次执行这些初始化语句
- 对于接口来说,初始化类时,并不会先初始化他实现的接口;初始化接口时,并不会初始化父接口;只有当程序首次使用接口里面的变量或调用接口方法时才会导致接口初始化
- 调用ClassLoader类的
loadClass()
方法赖加载一个类,并不会初始化这个类,因为不是对类的主动使用
类初始化时机
Java程序对类的使用方式分成主动使用和被动使用,JVM必须在每个类或接口首次主动使用时才会初始化他们,被动使用类不会导致类的初始化
主动使用有如下几种情况:
- 创建类实例
- 访问某个类或接口的静态变量
- 调用某个类的静态方法
- 反射某个类
- 初始化某个类的子类,而父类还没被初始化
- JVM启动时运行的主类
- 定义了default方法的接口,当接口实现类初始化时
被动使用有如下几种情况:
- 通过子类引用父类的静态字段,不会导致子类初始化
- 通过数组定义来引用类,不会触发该类的初始化
- 访问常量不会导致类初始化,因为在编译阶段就会放入常量池中
类初始化顺序
public class ClassA {
private static ClassA classA = new ClassA(); //此时测试输出结果为 a=0,b=1
private static int a = 0;
private static int b;
//private static ClassA classA = new ClassA(); //此时测试输出结果为 a=1,b=1
private ClassA() {
a++;
b++;
System.out.println("构造方法中:a=" + a + ",b=" + b);
}
public static ClassA getInstance() {
return classA;
}
public void print() {
System.out.println("a=" + a + ",b=" + b);
}
}
出现上述情况的原因是,进行类初始化时是依次向下执行,先调用的构造方法,在构造方法中值确实发生了改变,但是在下一步初始化时又重新赋值了,若不重新赋值则不会改变,要是调换顺序就不会出现这种情况
类卸载
JVM自带的类加载器加载的类是不会被卸载的,由用户自定义类加载器加载的类是可以被卸载的,当一个类的Class对象不再被引用,则该Class对象的生命周期就结束,对应的方法区中的数据也会被卸载,该情况的判断和操作都是有JVM自动执行
Comments NOTHING