JVM类加载
本文基于OpenJDK8,OpenJDK默认使用的是HotSpot虚拟机(JVM),而HotSpot是基于c++实现的
Klass模型
一个java的class在jvm中是怎么存储的?用的就是Klass模型。
java中的对象都是由jvm进行管理的,而HotSpot是基于c++实现的,c++本身也是一个面向对象编程语言,所以每一个class都有一个Klass模型(是一个c++的类,和spring中的beanDefinition类似)来存放类的元数据信息:属性信息,常量池,方法信息 等。
klass模型类的继承结构
InstanceKlass表示一个普通的java类(Object)。他有三个子类实现
1.instanceMirrorKlass 用于表述特殊的java.lang.Class 类(存储在堆区)
2.instanceRefKlass 表示引用
3.InstanceClassLoaderKlass 表示类加载器
ArrayKlass java的数组是动态数据类型,即运行时生成的。java数据的元信息由其子类表示
1.TypeArrayKlass 表示基本类型的数组
2.ObjectArrayKlass 表示引用类型的数组
类加载的过程
类加载由7个步骤完成
加载
主要做三件事:
1.jvm通过类的全限定名获取class(二进制文件)文件。
2.将class解析成InstaceKlass实例对象,存放到方法区
3.在堆区生成类的Class对象,即instanceMirrorKlass实例
无论是java还是其他类型的语言,只要完成这三步就可以完成加载步骤。
何时加载:
主动使用时:
1.new、getStatic、putstatic、invokestatic
2.初始化子类会加载父类
3.反射(class.forName)
4.启动类(main函数)
预加载:
Thread、String、包装类等
验证
由于在加载阶段的过程当中,并没有严格规定二进制字节流需要通过Java源码编译而来,所以验证阶段的主要目的是在加载阶段获取得到的二进制字节流中包含的信息符合当前虚拟机的要求,并且不会导致虚拟机收到危害。主要分为了以下几种形式的验证:
1、文件格式验证
2、元数据验证
3、字节码验证
4、符号引用验证
准备
为静态变量分配内存,赋初值。变量存放在方法区
实例变量是在创建对象的时候完成赋值的,没有赋初值一说
如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步
解析
将常量池中的符号引用转换成直接引用
符号引用: 用一串符号描述引用的目标,符号可以是任何形式的字面量(目标还未被加载到内存)
直接引用: 直接指向目标的指针(目标已经加载到内存)
初始化
执行静态代码块,给静态变量赋值
方法中语句的先后顺序与代码的编写顺序相关
案例分析
Test1
public class Test_1 {
public static void main(String[] args) {
System.out.printf(Test_1_B.str);
while (true);
}
}
class Test_1_A {
public static String str = "A str";
static {
System.out.println("A Static Block");
}
}
class Test_1_B extends Test_1_A {
static {
System.out.println("B Static Block");
}
}
输出:
A Static Block
A str
理由:
Test_1_B.str使用到的是父类的str属性,并没有加载子类。 但是只要使用继承就会加载父类,所以A的静态代码块会执行
Test2
public class Test_2 {
public static void main(String[] args) {
System.out.printf(Test_2_B.str);
}
}
class Test_2_A {
static {
System.out.println("A Static Block");
}
}
class Test_2_B extends Test_2_A {
public static String str = "B str";
static {
System.out.println("B Static Block");
}
}
输出:
A Static Block
B Static Block
B str
理由:
先加载父类,执行父类的静态代码块,然后加载子类,执行子类的静态代码块。 最后调用输出子类的str
Test3
public class Test_3 {
public static void main(String[] args) {
System.out.printf(Test_3_B.str);
while (true);
}
}
class Test_3_A {
public static String str = "A str";
static {
System.out.println("A Static Block");
}
}
class Test_3_B extends Test_3_A {
public static String str = "B str";
static {
System.out.println("B Static Block");
}
}
输出:
A Static Block
B Static Block
B str
理由:
先加载父类,执行父类的静态代码块,然后加载子类,执行子类的静态代码块。 最后调用输出子类的str
Test4
public class Test_4 {
public static void main(String[] args) {
Test_4_A arrs[] = new Test_4_A[1];
}
}
class Test_4_A {
static {
System.out.println("Test_4_A Static Block");
}
}
输出:
不会输出
理由:
定义数组是不会分配空间,只有在填充对象的时候才会加载
Test5
public class Test_5 {
public static void main(String[] args) {
Test_5_A obj = new Test_5_A();
}
}
class Test_5_A {
static {
System.out.println("Test_5_A Static Block");
}
}
输出:
Test_5_A Static Block
理由:
手动 new 对象 会进行类加载
Test6
public class Test_6 {
public static void main(String[] args) {
System.out.println(Test_6_A.str);
}
}
class Test_6_A {
public static final String str = "A Str";
static {
System.out.println("Test_6_A Static Block");
}
}
输出:
A Str
理由:
如果是final修饰的变量,将变量值存放到常量池
String 是常量,在编译阶段就已经生成好了
Test7
public class Test_7 {
public static void main(String[] args) {
System.out.println(Test_7_A.uuid);
}
}
class Test_7_A {
public static final String uuid = UUID.randomUUID().toString();
static {
System.out.println("Test_7_A Static Block");
}
}
输出:
Test_7_A Static Block
17f9496b-08a4-4387-a63d-5af4a1837315
理由:
UUID.randomUUID() 需要动态生成,所以会进行类加载
Test8
public class Test_8 {
static {
System.out.println("Test_8 Static Block");
}
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("com.luban.classload.Test_1_A");
}
}
输出:
Test_8 Static Block
理由:
反射会进行类加载
Test21
public class Test_21 {
public static void main(String[] args) {
Test_21_A obj = Test_21_A.getInstance();
System.out.println(Test_21_A.val1);
System.out.println(Test_21_A.val2);
}
}
class Test_21_A {
public static int val1;
public static int val2 = 1;
public static Test_21_A instance = new Test_21_A();
Test_21_A() {
val1++;
val2++;
}
public static Test_21_A getInstance() {
return instance;
}
}
输出:
1
2
理由:
static 顺序执行
Test22
public class Test_22 {
public static void main(String[] args) {
Test_22_A obj = Test_22_A.getInstance();
System.out.println(Test_22_A.val1);
System.out.println(Test_22_A.val2);
}
}
class Test_22_A {
public static int val1;
public static Test_22_A instance = new Test_22_A();
Test_22_A() {
val1++;
val2++;
}
public static int val2 = 1;
public static Test_22_A getInstance() {
return instance;
}
}
输出:
1
1
理由:
Test_22_A.getInstance();//类加载 先回初始化静态变量 此时val1 和 val1 = 0
//初始化阶段
先执行 public static int val1; 此时val1 = 0
再执行public static Test_22_A instance = new Test_22_A(); 调用构造方法 此时val1 = 1 此时val2 = 1
再执行 public static int val2 = 1; 覆盖val2的值 此时val1 = 1 此时val2 = 1
所以输出 1 1
类加载器
jvm中有两种类型的类加载器。一种是C++编写的BootStrapClassLoader,其他的都是由java编写的ClassLoader,java的类加载器都继承java.lang.ClassLoader抽象类
各种类加载器有着逻辑上的父子关系,但不是真正的父子关系。
每个类加载器需要指定父加载器,BootStrapClassLoader是最顶级的ClassLoader,不需要指定父加载器。
当一个类需要被类加载器加载时,该类加载器会指定父加载器去寻找请求的类是否被加载,如果没有被加载,则直到 找到BootStrapClassLoader,如果BootStrapClassLoader还没有加载。则一层一层往下去寻找class加载。这种类加载的方式叫做双亲委派模型
- 自底向上检查类是否已加载
先通过findLoadedClass()方法从最底端类加载器开始检查类是否已经加载。如果已经加载,则根据resolve参数决定是否要执行连接过程,并返回Class对象。如果没有加载,则通过parent.loadClass()委托其父类加载器执行相同的检查操作(默认不做连接处理)。直到顶级类加载器,即parent为空时,由findBootstrapClassOrNull()方法尝试到Bootstrap ClassLoader中检查目标类。 - 自顶向下尝试加载类
如果仍然没有找到目标类,则从Bootstrap ClassLoader开始,通过findClass()方法尝试到对应的类目录下去加载目标类。如果加载成功,则根据resolve参数决定是否要执行连接过程,并返回Class对象。如果加载失败,则由其子类加载器尝试加载,直到最底端类加载器也加载失败,最终抛出ClassNotFoundException。
根加载器bootstrapClassLoader
c++编写的,不是一个类而是一段程序。用来加载java的核心类,(%JAVA_HOME%/jre/lib/rt.jar)里面所有的class
private static void bootstrapClassLoader() {
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL.toExternalForm());
}
}
扩展加载器 extClassLoader
继承ClassLoader,负责加载jre下的扩展目录
private static void extClassLoader() {
ClassLoader extClassLoader = TestClassLoader.class.getClassLoader().getParent();
System.out.println(extClassLoader);
URLClassLoader extClassLoaderUrl= (URLClassLoader)extClassLoader;
URL[] urLs = extClassLoaderUrl.getURLs();
for (URL urL : urLs) {
System.out.println(urL.toExternalForm());
}
}
应用类加载器 appClassLoader
默认加载用户程序的类加载器 ,父类是extClassLoader。 对应的文件是应用程序classpath目录下的所有jar和class等
private static void appClassLoader() {
ClassLoader appClassLoader = TestClassLoader.class.getClassLoader();
System.out.println(appClassLoader);
URLClassLoader appClassLoaderUrl= (URLClassLoader)appClassLoader;
URL[] urLs = appClassLoaderUrl.getURLs();
for (URL urL : urLs) {
System.out.println(urL.toExternalForm());
}
}
打破双亲委派
双亲委派模式的优点和缺点。
优点:越基础的类由越顶层的加载器加载,保护java的核心库不被修改。再有就是防止类被重复加载
缺点:无法做到向下委派或者不委派。比如父类加载器需要委托子类去加载class文件(jdbc中的Driver)
-
自定义类加载器,重写loadClass方法和findClass方法
-
SPI机制打破双亲委派模式
我们现在来一下使用jdbc加载mysql数据源,可以看到在注释Class.forName(“com.mysql.cj.jdbc.Driver”); 情况下还能拿到mysql的连接,这是为什么呢?
import java.sql.Connection; import java.sql.DriverManager; public class TestJdbc { public static void main(String[] args) throws Exception { //Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/sh?useSSL=false&serverTimezone=Asia/Shanghai"; String username = "root"; String password = "ok"; Connection con = DriverManager.getConnection(url,username,password); System.out.println(con); } }
我们来分析一下DriverManger的源码,DriverManger在rt.jar下的java.sql.*,也就是说DriverManger是由根加载器加载的
DriverManger有一段静态代码块,loadInitialDrivers();
我们再来看一下这个方法的代码,其中核心代码是ServiceLoader.load(Driver.class); ServiceLoader本质上使用的是 ThreadContextClassLoader (线程上下文类加载器),我们跟进一下方法便知,引用书中的一句话:
线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
当我们的ServiceLoader创建好了之后,就会去寻找符合的类去使用线程上线文类加载器去加载,那如何去寻找呢,我们接着代码往下走
我们跟着断点来看driversIterator.hasNext()做了什么事情,经过一系列的调用,最终执行到了hasNextService()这个方法。然后要根据fullName去寻找,fullName生成的规则是META-INF/services/+接口名,然后使用parse()读取项目中引用jar包的该路径中的文件,以mysql-connector-java-8.0.19.jar为例,我们来看一下它的目录,发现是有对应的文件的,里面的内容只有一个一段:com.mysql.cj.jdbc.Driver 就是我们要加载的具体Driver的实现类。
这个方法的作用就是根据fullName找到要加载类的全限定名,
拿到需要加载的类的全限定名之后,就会进行加载该类
到现在SPI机制已经结束了,SPI机制做到了向下委派,使用到的核心类是ThreadContextClassLoader (线程上下文类加载器)
完成SPI需要以下几个步骤:
- 定义接口
- 定义实现接口的实现类, 在项目的\src\main\resources\下创建\META-INF\services目录 ,并在该目录下创建接口名字的文件,文件中输入要实现加载的全限定名的类名(如果有多个类要加载,则换行输入)。
- 使用ServiceLoader.load(接口.class)来完成实现类的加载。