上期说了JVM的发展史,这期咱们来讲一下博主所理解的JVM类加载子系统
首先是类加载过程,在加载过程,这里我们提一下类加载器
JVM分为两种加载:一种是引导类加载器,另一种是自定义类加载器。
引导类加载器(Bootstrap ClassLoader):这个类加载器是使用C/C++实现的,嵌套在JVM内部。它的作用是来加载Java核心库,用于提供JVM自身需要的类,他没有继承自ClassLoader类,更没有父加载器,它加载扩展类加载器和应用程序类加载器,并且指定为他们的父加载器,出于安全考虑,引导类加载器只加载包名为java,javax,sun等开头的类,需要注意的是该加载器无法通过Java方法等一些方式获取。下列用代码来查看它加载的路径:
package com.tl666.jvm.classload;
import sun.misc.Launcher;
import java.net.URL;
public class ClassLoadTest2 {
public static void main(String[] args) {
//获取引导类加载的加载路径
System.out.println("引导类加载的加载路径:");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL);
}
}
}
输出结果如下:
自定义类加载器:定义,继承于ClassLoader的类加载器统称为自定义类加载器,JVM给我们默认实现了两个类加载器,分别为扩展类加载器和应用程序类加载器,它们都是sun.misc.Launcher类的两个静态内部类,他们两个的关系为,扩展类加载器是应用程序类加载器的父加载器。
扩展类加载器(ExtClassLoader):由Java语言编写,继承至URLClassLoader,间接继承ClassLoader,父类加载器为引导类加载器,下列通过代码来查看它加载类的路径有哪些:
//获取扩展类加载器加载的路径
System.out.println("扩展类加载器加载的路径:");
String property = System.getProperty("java.ext.dirs");
for (String s : property.split(";")) {
System.out.println(s);
}
这里需要注意的是:如果用户自定义创建的Jar文件放置这个上图路径下,也会由扩展类加载器加载。
应用程序类加载器(AppClassLoader):由Java语言编写,继承至URLClassLoader,间接继承ClassLoader,父类加载器为扩展类加载器,它的加载路径为:环境变量classpath或者系统属性 java.class.path 指定下的类库。它有个特殊的地方就是它是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成。它的获取方式有通过调用ClassLoader.getSystemClassLoader();方法获取,或者通过用户自定义类的this.getClass().getClassLoader();等也能获取。
测试代码:
package com.tl666.jvm.classload;
public class ClassLoadTest {
public static void main(String[] args) {
//获取系统类默认加载器(AppClassLoader)
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("应用类加载器:"+appClassLoader);
ClassLoader extClassLoader = appClassLoader.getParent(); //获取上级类加载器
System.out.println("扩展类加载器:"+extClassLoader);
ClassLoader bootstrapClassLoader = extClassLoader.getParent();//获取不到引导类加载器
System.out.println("引导类加载器:"+bootstrapClassLoader);//null
//获取用户自定义类的类加载器
ClassLoader classLoader = ClassLoadTest.class.getClassLoader();
if(classLoader == appClassLoader){
//得出结论,用户自定义的类是用的系统类加载器进行加载
System.out.println("与"+appClassLoader+"是同一个类加载器");
}else if(classLoader == extClassLoader){
System.out.println("与"+extClassLoader+"是同一个类加载器");
}else{
System.out.println(classLoader);
}
//java的核心类库采用的是引导类加载器加载,这里获取不到
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null 得出结论,该类由引导类加载器加载
}
}
由上图可以知道分为三个阶段:
加载阶段(Loading):通过类的全限定名得到二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
链接阶段(Linking):
验证(Verify):确保class文件的字节流中包含信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
准备(Prepare):为类变量分配内存并且设置该类变量的默认初始值,既零值,但是不包括用final修饰的static,因为final在编译时候就会分配了,准备阶段会显示初始化。比如下列例子:
static int count = 1;
final static int sum = 1;
在准备阶段,第一句代码会先把count初始赋值为0,然后在把1赋值给count,而第二句代码则会在编译时就把1赋值给了sum。
解析(Resolve):将常量池内的符号引用转换为直接引用的过程,解析操作会伴随着JVM完成初始化之后在执行,符号引用就是通过一组符号来描述所引用的目标,而直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄
直接引用和符号引用参考链接解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等。对应常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info等。
在字节码中,如果在类中不存在静态变量或者静态代码块。则不会出现类构造函数
初始化(Initialization):初始化阶段就是执行类的构造器< clinit >()过程,此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。构造器方法中的指令按语句在源文件中出现的顺序执行,< clinit >()不同于< cinit >类构造器。若该类有父类,JVM会保证子类的< clinit >()执行前,父类的< clinit >()已经执行完毕。JVM必须保证一个类的< clinit >()方法在多线程下被同步加锁的。
同步加锁的意思就是两个线程,当一个线程还没加载完时,是不允许第二个线程去加载的
示例代码如下:
package com.tl666.jvm.classload;
/**
* 虚拟机必须保证一个类的()方法在多线程下是同步加锁的
* 同步加锁的意思就是两个线程,当一个线程还没加载完时,是不允许第二个线程去加载的
* 下列为测试用例
* 输出结果为
* 加载开始
* 加载开始
* Thread-0:加载中
* 如果不是同步加锁会出现
* 加载开始
* 加载开始
* Thread-0:加载中
* Thread-1:加载中
*/
public class SyncClass {
public static void main(String[] args) {
Runnable r = () ->{
System.out.println(Thread.currentThread().getName()+":加载开始");
MyClass1 myClass1 = new MyClass1();
System.out.println("加载结束");
};
new Thread(r).start();
new Thread(r).start();
}
}
class MyClass1 {
static {
if (true) {
System.out.println(Thread.currentThread().getName() + ":加载中");
while (true) {
}
}
}
}
控制台输出如下:
验证了上面说的结论,线程0还没加载完是不允许线程1去加载。
好了本期的内容就到这里的,下期继续分析自定义类加载器以及字节码的加密等。