深入理解类加载机制:part1类加载过程
一般来说我们把类加载过程分为三个主要步骤,加载,链接和初始化。具体行为在java虚拟机规范里有非常详细的定义
一、加载
加载过程是java将字节码数据从不同的数据源读取到jvm中,并映射成jvm可以识别的数据结构(class对象),这里说的不同数据源,可以jar包,可以是class文件,甚至可以是网络数据源等。加载是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。下面是不同数据源:
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件
二、链接
第二个阶段是链接,这是核心步骤。把原始类的定义信息,平滑的转化入jvm的运行过程中。这里可以细分为三个步骤
验证、准备和解析。
1.验证
尽管编译器已经生成了满足所有静态和结构约束的类文件,但是java虚拟机无法保证要求加载的任何文件都是java编译器生产的。所以需要验证这些文件是由可信赖的编译器生成的还是由试图利用java虚拟机的攻击者生成的。
另外一点原因是版本偏差,用户可能已经成功编译了一个类,比如A,他是B的子类,但是它编译后,可能B的定义已经改变,某些方法可能被删除,方法的修饰符可能从public,变为了private。
所以说验证阶段是虚拟机安全的重要保障,jvm需要核验字节信息是否符合java虚拟机规范,否则被认为是verifyError。这样就防止了恶意信息或不合规信息危害jvm运行。
2.准备
准备阶段创建类或接口中的静态变量,并初始化静态变量的初始值,但这里的初始化不是显示初始化,侧重点在于分配所需的内存空间,不会去执行jvm初始化指令。
1)这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2)这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
3.解析
这一步会将常量池中的符号引用转化为直接引用。
- 符号引用:字符串,能根据这个字符串定位到指定的数据,比如java/lang/StringBuilder
- 直接引用:内存地址
三、初始化
这一步真正去执行初始化代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态代码块内的逻辑,编译器在编译阶段会把这段逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
简单举个例子:
public static int a = 100; 原始类型的静态变量,在链接的准备阶段,给变量a分配内存空间,并且初始化值为0.在初始化阶段会将100赋值给a变量。执行静态代码块中的逻辑
本文参考:
1.极客时间:java核心技术36讲
2.https://blog.csdn.net/fgets/article/details/52934178?from=singlemessage&isappinstalled=0
3.java虚拟机规范
深入理解类加载机制:part2双亲委派模型
要想真正理解双亲委派模型,需要了解java中类加载器的架构和职责,至少要懂具体有哪些内建的类加载器,以及如何自定义类加载器?从应用角度来说,解决某些类加载的问题,如java程序启动较慢,有没有办法减小类加载的开销。
一、Oracle JDK 内建的三种类加载器
先来张经典的图
1.启动类加载器(BootStrap Class-Loader)
它是启动类加载器,jdk赋予了它加载程序的AllPermission,加载 jre/lib下面的jar文件,如rt.jar等。那么我们怎么去验证到底哪些文件是Bootstrap Class-Loader加载的呢?
可以通过执行下面代码:
System.out.println(System.getProperty("sun.boot.class.path"));
输出结果为:
"D:\MyStudySoftware\jdk8\jre\lib\resources.jar;D:\MyStudySoftware\jdk8\jre\lib\rt.jar;D:\MyStudySoftware\jdk8\jre\lib\sunrsasign.jar;D:\MyStudySoftware\jdk8\jre\lib\jsse.jar;D:\MyStudySoftware\jdk8\jre\lib\jce.jar;D:\MyStudySoftware\jdk8\jre\lib\charsets.jar;D:\MyStudySoftware\jdk8\jre\lib\jfr.jar;D:\MyStudySoftware\jdk8\jre\classes"
其中:D:\MyStudySoftware\jdk8 是我的jdk安装目录
所以可以精简为:
JAVA_HOME\jre\lib\resources.jar;
JAVA_HOME\jre\lib\rt.jar;
JAVA_HOME\jre\lib\sunrsasign.jar;
JAVA_HOME\jre\lib\jsse.jar;
JAVA_HOME\jre\lib\jce.jar;
JAVA_HOME\jre\lib\charsets.jar;
JAVA_HOME\jre\lib\jfr.jar;
JAVA_HOME\jre\classes
那么为什么System.getProperty(“sun.boot.class.path”) 这段代码可以获取到Bootstrap Class-Loader加载的文件路径呢?
为了更好的理解,我们可以看下 sun.misc.Launcher的源代码,它是一个虚拟机的入口程序,可以看到它的静态变量bootClassPath就是我们打印的代码
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
...
}
知识扩展:
有时候我们想修改jdk底层的代码,并让其加载到jvm内存中,我们怎么实现呢?
可以通过指定新的bootClassPath方式实现,jvm为我们提供了三种形式:
jvm的启动参数为:
java -Xbootclasspath:<you_boot_classpath>you_app 将启动加载路径修改
java -Xbootclasspath/a:<you_dir>you_app 意味着:append, 将指定目录添加到bootClassPath后面
java -Xbootclasspath/p:<you_dir>you_app 意味着:prepend, 将指定目录添加到bootClassPath前面
2.扩展类加载器(Extension Class-Loader)
负责加载我们放到jre/lib/ext 下面的jar包,该目录也可以通过设置:
java -Djava.ext.dirs=your_ext_dir_helloworld 来指定。
同样我们也所以用上述打印启动类加载器加载目录的思路来打印扩展类加载器的目录
System.out.println(System.getProperty("java.ext.dirs"));
输出结果为:
"D:\MyStudySoftware\jdk8\jre\lib\ext;C:\windows\Sun\Java\lib\ext"
3.应用类加载器(Application Class-Loader)
就是加载我们最熟悉的classpath的内容,它的父类加载器是Extension Class-Loader,所有在System.getProperty(“java.class.path”) 目录下的类都可以被这个类加载器加载。我们要实现自己的类加载器,不管是直接实现抽象类ClassLoader,还是继承URLClassLoader类,或者其他子类。它的父加载器都是AppClassLoader。
二、理论是别人的,实践和应用是自己的,下面让我们来一起实践一下看看类加载器的真实面目
1.打印类加载器
public static void printClassLoaders() throws ClassNotFoundException {
System.out.println("Classloader of this class:"
+ Myclass.class.getClassLoader());
System.out.println("Classloader of Logging:"
+ Logging.class.getClassLoader());
System.out.println("Classloader of ArrayList:"
+ ArrayList.class.getClassLoader());
}
输出结果:
Classloader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Classloader of Logging:sun.misc.Launcher$ExtClassLoader@677327b6
Classloader of ArrayList:null
我们可以看到三种不同的类加载器已经被打印出来了。但是bootstrap打印出来是null,这是因为bootstrap是用native code编写,它并不属于jvm的类等级层次,因为ClassLoader并没有遵守CLassLode人的加载规则,另外,Bootstrap Class-Loader,并没有子类,Extension Class-Loader 的父类也不是Bootstrap Class-Loader,我们再应用中能提取到的顶级父类是Extension Class-Loader。所以我认为应用类加载器等级结构应该如下图所示:
[外链图片转存失败(img-Y7v1wb97-1564566923059)(E:\study2019\网络协议\img\1564557703154.png)]
未完待续,接下来准备实现的代码如下
2.实现自定义类加载器
3.tomcat如何实践自己的类加载器的?
4.实现热部署
类是Extension Class-Loader。所以我认为应用类加载器等级结构应该如下图所示:
未完待续,接下来准备实现的代码如下