一、类加载
什么是类加载?在IDEA中写一个Student.java文件,在对应的目录下执行
>javac Student.java
将Student.java文件编译成对应的Student.class文件,再通过ClassLoader里面的loadClass方法,将Student.class二进制字节码文件加载到jvm的元空间中成为Student.class的类类型对象。这一套流程就是所谓的 类加载
二、类的生命周期
类加载进内存也是分步骤的:
类的生命周期.png
1.加载
指的是把class字节码文件通过类加载器装载到jvm元空间中。
1.字节码来源,可以是本地磁盘class文件,jar包中的class文件,网络上的class文件,以及动态代理修改后的class文件实时编译
2.通过类加载器里面的classloader方法加载到元空间中
2.验证
保证加载进来的字节流符合jvm虚拟机规范,不会造成安全错误
1.文件格式的验证,文件中是否有不规范的或者附加的其他信息,例如常量池中是否有不被支持的常量
2.元数据的验证,保证器描述的信息符合java语言的规范要求,例如类是否有父类,是否继承了不被允许的final类
3.字节码的验证,保证程序语义的合理性,比如保证类型转换的合理性
4.符号引用的验证,比如校验引用中通过全限定名是否能够找到一应的类,校验符号引用中的访问性(public),是否可以被当前类访问等等
3.准备
主要是为基本数据类型的变量分配内存,并且赋予初始值。例如:int初始值是0,reference初始值为null
4.解析
将常量池的符号引用替换成直接引用的过程。例如final String = new String("java"),new String("java")在堆内存的地址是123
改成final String->123。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换成具体的内存地址或偏移量,也就是直接引用
5.初始化
在这个阶段主要对类变量初始化,是执行构造器的过程
1.支队static修饰的变量或语句进行初始化
2.如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类
3.如果同时包含静态变量和静态代码块,则按照自上而下的顺序依次执行
三、类加载的原则
1.类加载器的种类
JVM中的类加载器.pngBootstrap Classloader(启动类加载器)
启动类加载器(Bootstrap Classloader)负责将<JAVA_HOME>/lib目录下并且被虚拟机识别的类库加载到虚拟机内存中。我们常用基础库,例如java.util.,java.io.,java.lang.**等等都是由根加载器加载。
Extension Classloader(扩展类加载器)
扩展类加载器(Extention Classloader)负责加载JVM扩展类,比如swing系列、内置的js引擎、xml解析器等,这些类库以javax开头,它们的jar包位于<JAVA_HOME>/lib/ext目录中。
Application Classloader(应用类加载器)
应用程序加载器(Application Classloader)也叫系统类加载器,它负责加载用户路径(ClassPath)上所指定的类库。我们自己编写的代码以及使用的第三方的jar包都是由它来加载的自定义加载器(Custom Classloader)通常是我们为了某些特殊目的实现的自定义加载器,后面我们得会详细介绍到它的作用以及使用场景。
XXXClassloader(自定义加载器)
通过
class XXXClassloader extends ClassLoader{}
实现父类ClassLoader中的 loadClass(String name)
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
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
findClass(查找.class是否被加载过)空方法,自定义加载器自己实现,
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return super.findClass(name);
}
线程上下文类加载器
JVM的SPI机制其实就是用到了线程上下文类加载器,每个线程启动的时候,jvm会默认将Application ClassLoader作为线程的类加载器。这时候线程直接使用Application ClassLoader去加载对应的类,并没有将需要加载的类交给自己的上一级类加载器。
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
如何破坏双亲委托机制
1.首先根据源码可以知道,如果自定义的方法不想违背双亲委派模型,则只需要重写findclass方法即可,如果想违背双亲委派模型,则还需要重写loadclass方法。
2.常见的SPI机制,例如class.forName("")加载数据库驱动的方法,使用SPI机制破坏了双亲委托机制
3.使用线程上下文类加载器,默认为Application ClassLoader直接加载对应的类文件,所以也是打破双亲委派机制的一种方式。
2.双亲委托机制
classloader的双亲委托机制是指多个类加载器之间存在父子关系的时候,某个class类具体由哪个加载器进行加载的问题。其具体的过程表现为:当一个类加载的过程中,它首先不会去加载,而是委托给自己的父类去加载,父类又委托给自己的父类。因此所有的类加载都会委托给顶层的父类,即Bootstrap Classloader进行加载,然后父类自己无法完成这个加载请求,子加载器才会尝试自己去加载。使用双亲委派模型,Java类随着它的加载器一起具备了一种带有优先级的层次关系,通过这种层次模型,可以避免类的重复加载,也可以避免核心类被不同的类加载器加载到内存中造成冲突和混乱,从而保证了Java核心库的安全。、
注意:Bootstrap Classloader,Extention Classloader,Application Classloader三个加载器类仅仅是组合关系,有优先级,并不是实际开发中的父子类关系
双亲委托的作用
1、安全,可避免用户自己编写的类动态替换Java的核心类,如java.lang.String。,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
2、避免全限定命名的类重复加载(使用了findLoadClass()判断当前类是否已加载)。Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
3.沙箱安全机制
主要用来防止恶意代码污染java源代码。
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
四、类加载器的使用场景
1.用来做微服务module之间的jar包隔离
2.用来做热加载
3.用来做热部署
五、类加载相关面试题
1.class.forName()与classLoader()方法的区别
class.forName()前者除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。而classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
Class.forName得到的class是已经初始化完成的,Classloder.loaderClass得到的class是还没有链接的。
具体如何实现的可以看阿里的这篇技术文档,类加载器的使用场景
网友评论