JVM系列——类加载day3-2
类加载
加载
将类的字节码载入方法区中,内部采用C++的instanceKlassI描述java类,它的重要field有:
- _java_mirror即java的类镜像,例如对 string 来说,就是String.class,作用是把klass暴露给java使用
- _super即父类
- _fields即成员变量_methods 即方法
- _constants即常量池
- _class_loader即类加载器
- _vtable虚方法表
- _itable接口方法表
如果这个类还有父类没有加载,先加载父类
加载和链接可能是交替运行的
instanceKlass是存储在元空间中的,_java_mirror则是在堆中的
链接
实现验证,用于验证类是否符合JVM的规范,是安全性检查
准备
为static变量分配空间,设置默认值
- static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
- static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static变量是final的基本类型,那么编译阶段值就确定了,赋值在准备阶段完成如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
解析
将常量池中的符号引用解析为直接引用
意思是在未进行解析之前,常量池中就算有这个类,他仅仅是表示一个符号而已,不具备类的特性,只有进行解析之后,他才是类
初始化
初始化即调用<cinit>()v
,虚拟机会保证这个类的构造方法的线程安全
类的初始化时懒惰的:
- main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时,类会初始化
- 子类初始化,如果父类还没初始化,会引发,类的初始化
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName,会导致类的初始化
- new会导致初始化
不会导致类初始化的情况:
- 访问类的static final静态常量(基本类型和字符串)不会触发初始化
- 类对象.class不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass方法
- Class.forName的参数2为false时
类加载器(Java8)
类加载器的层级
- Bootstrap ClassLoader(启动类加载器):加载JAVA_HOME/jre/lib的类,无法直接访问
- Extension ClassLoader(扩展类加载器):加载JAVA_HOME/jre/lib/ext的类,上级为Bootstrap,显示为null
- Application ClassLoader(应用程序类加载器):加载classpath的类,上级为Extension
- 自定义类加载:加载自定义的类,上级为Application
启动类加载器
使用启动类加载器进行类加载
Class.forName("类的引用地址")
//获取类加载器,调用getClassLoader()方法
loadClass.getClassLoader()
//指定加载
java -Xbootclasspath/a:. 类路径
-Xbootclasspath
:表示设置bootclasspath
/a:.
:表示将当前目录追加至bootclasspath后
java -Xbootclasspath :<new bootclasspath>
java -Xbootclasspath/a :<追加路径>
java -Xbootclasspath/p:<追加路径>
扩展类加载器
对类进行打包
//打包为jar包
jar -cvf jar包名.jar 需要打包的类路径
将打包完的类放置到lib目录下
打包完后在当前项目的目录下
拿到打包好的jar包之后,我们找到jdk的安装目录,找到jre下的lib下的ext
目录,将其粘贴进去即可
此时你再去执行
Class.forName("类的引用地址")
//获取类加载器,调用getClassLoader()方法
loadClass.getClassLoader()
得到的就是显示ExtClassLoader对其进行了加载
看到这里有人就说我没有jre目录啊,我用的是java11。。。,那么接下来介绍一下Java8以上的
如下,你可以看到我的jdk17是没有jre的
我们进入bin目录找到jlink.exe
,他的作用是帮我们生成联系(jre)
打开终端运行如下代码(这里注意一下,是在jdk主目录里不是在bin里,不过这个不知道的话好像也不会看这个系列的文章)
bin\jlink.exe --module-path jmods --add-modules java.desktop --output jre
此时jre就有了
这里仅是告诉你怎么生成jre而已,但是这个jre是没有ext的
在 JDK9 以后,这种扩展机制被模块化带来的天然扩展能力所取代。由于扩展类加载器是由 Java 代码实现的,开发者可以直接在程序中使用扩展类加载器来加载 Class 文件
双亲委派
指的是调用类加载器的loadClass方法时,查找类的规则
源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
源码注释翻译
加载具有指定二进制名称的类。此方法的默认实现按以下顺序搜索类:
调用findLoadedClass(String)检查该类是否已加载。
在父类加载器上调用loadClass方法。如果父对象为null,则使用构建在虚拟机中的类加载器。
调用findClass(String)方法来查找该类。
如果使用上述步骤找到该类,并且resolve标志为true,则该方法将在生成的类对象上调用resolveClass(class)方法。
鼓励类加载器的子类重写findClass(String),而不是此方法。
除非重写,否则该方法在整个类加载过程中与getClassLoadingLock方法的结果同步。
步骤
- 检查当前类是否已被加载(findLoadedClass方法)
- 若未被加载,检查当前类加载器是否有上级,有则委派上级进行查找(parent.loadClass)
- 若没有上级,则委派BootstrapClassLoader(findBootstrapClassOrNull)
- 若所有都找不到,则本类加载器调用
findClass
方法进行类加载
线程上下文类加载器
在使用JDBC时需要加载Driver驱动,但实际上不写也是可以的
Class.forName("com.mysql.jdbc.Driver")
在DriverManager的源码中采用了static静态代码块中的loadInitialDrivers方法
static{
loadInitialDrivers();
}
但当我们获取他的类加载器时显示为null,表明时BootstrapClassLoader
,这样就会去jre/lib
下找mysql-connector-java
的jar包,但实际上根本没有驱动jar包理应不能加载,这是后就要看一下loadInitialDrivers()方法
工作方式:
- 使用ServiceLoader机制加载驱动:这里也是使用的应用程序类加载器进行加载打破了双亲委派机制
- 使用jdbc.drivers定义的驱动名加载驱动:调用Class.forName进行加载
Class.forName( aDriver,true,classLoader.getSystemClassLoader())
这里的SystemClassLoader就是ApplicationClassLoader,这里打破了双亲委派机制
ServiceLoader
他是Service Provider Interface接口(SPI)
有如下约定:
在jar包的META-INF/services包下,以接口全限定名为文件,文件内容是实现类的名称
按照此规定设计的jar包就可以配合ServiceLoader找到实现类进行实例化,实现解耦
按照下面的方式得实现类,体现了面向接口编程+解耦的思想,在多个框架中都有运用
ServiceLoader<接口类型>allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型>iter = allImpls.iterator();
while(iter.hasNext()){
iter.next():
}
ServiceLoader.load方法
这里就用到了线程上下文类加载器,看到Thread.currentThread().getContextClassLoader()
说明是获取了当前的线程,使用线程获取上下文类加载器,这个就是线程上下文类加载器
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
这说明线程上下文类加载器其实就是当前线程使用的类加载器,也就是应用程序类加载器
自定义类加载器
使用场景
- 想加载非classpath随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
使用步骤
- 继承 ClassLoader 父类
- 要遵从双亲委派机制,重写findClass方法
注意不是重写loadClass方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的 defineClass方法来加载类
- 使用者调用该类加载器的 loadClass方法
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class SelfDesignClassLoader extends ClassLoader{
//重写findClass
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classPath = ""+name+".class";
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Files.copy(Paths.get(classPath),byteArrayOutputStream);
//得到字节数组
byte[] bytes = byteArrayOutputStream.toByteArray();
//将字节转*.class
return defineClass(name,bytes,0,bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("class not found");
}
}
}
java9及以上类加载器
java9及以上后类加载已然出现变化
从这里看出
类加载器变成了
- BootClassLoader(启动类加载器):加载JAVA_HOME/jre/lib的类,无法直接访问
- PlatformClassLoader(平台类加载器):加载JAVA_HOME/jre/lib/ext的类,上级为Boot,显示为null
- AppClassLoader(应用程序类加载器):加载classpath的类,上级为 Platform
- CustomerClassLoader(自定义类加载):加载自定义的类,上级为App