上文讲到一个.java文件是如何变成一个.class文件以及Class文件的组成,在Class文件中描述的各类信息,最终都需要加载到虚拟机中之后才能被运行和使用。那么一个.class文件是如何加载到虚拟机中使用的呢?它是通过类加载器通过类加载的过程实现的。一个类的加载过程分为加载、验证、准备、解析、初始化、使用、销毁,JVM通过类加载器实现完成加载这一步骤,类加载器又分为BootStrapClassLorder、ExtensionClassLoader、ApplicationClassLoader、自定义类加载器。
一、类加载
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析三个部分统称为连接。
需要注意的是加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。
何时会进行类加载
1、遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段(而加载、验证、准备自然需要在此之前开始)
new:创建类实例 使用new关键字实例化对象的时候
getstatic、putstatic访问类的域和类实例域 读取或设置一个类型的静态字段
invokestatic 调用命名类中的静态方法
2、使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
3、当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
... 等等
接口的加载过程与类加载过程稍有不同 编译器仍然会为接口生成“
1.加载:
加载,是指查找字节流,并且据此创建类或者接口的过程。加载阶段既可以使用Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过定义自己的类加载器去控制字节流的获取方式(重写一个类加载器的findClass()或loadClass()方法),实现根据自己的想法来赋予应用程序获取运行代码的动态性。
但是对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的,因为数组它并没有对应的字节流,由Java虚拟机直接生成的。
在加载阶段,Java虚拟机需要完成以下三件事情
1、通过一个类的全限定名来获取定义此类的二进制字节流。这里不一定是class文件 可以从ZIP压缩包、JAR、EAR、WAR等格式中读取,可以从网络中获取,也可以运行时获取,也就是动态代理技术。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载一个类的时候会去先加载其父类,而且会有懒加载的机制(就是用到的时候才去加载这个类)。
public class Test_2 {
public static void main(String[] args) {
System.out.println(B.a);
}
}
class A{
public static String a = "str";
static {
System.out.println("AAAAAA");
}
}
class B extends A{
static {
a+="aaa";
System.out.println("BBBBB");
}
}
输出 AAAAAA str
为什么B类没有被加载?
因为JVM会先判断是否加载,才会有初始化的动作。
JVM又是懒加载 只有用到的时候才会去加载,所以JVM判断只要加载A就可以了,B的内部没有任何东西被使用,所以B并没有加载。
JVM加载类是采用懒加载,用到的时候再去加载,一些根类是采用预加载,一开始就会加载进虚拟机里,比如String Interge常用的。
2.验证:
验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
1、文件格式验证:包含验证是否以魔数0xCAFEBABE开头。主、次版本号是否在当前Java虚拟机接受范围之内等信息的验证元数据验证:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)这个类的父类是否继承了不允许被继承的类(被final修饰的类)等信息的验证 侧重点是验证描述的信息符合《Java语言规范》的要求
2、字节码验证:对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,
3、符号引用验证:最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用中通过字符串描述的全限定名是否能找到对应的类等信息的验证
需要注意的是验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段 可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3.准备:
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
需要注意的是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中
public static int value = 123; 执行完之后变成了 value = 0;
把value赋值为123的动作要到类的初始化阶段才会被执行
public static final int value = 123; 准备阶段执行完之后变成了 value = 123;
如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,
总结:
1、final static修饰的在准备阶段直接分配内存并赋值了
2、static修饰的是在准备阶段进行分配内存会初始化赋一个默认值 初始化阶段的
3、非静态的是在初始化阶段的
4.解析:
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。java文件通过编译之后会变成符号引用
类似这样的 #7.#28 这样的为符号引用 在16进制文件里就是 0A 00 07 00 1C
解析阶段就是把#7这样的符号引用变成直接引用
1.符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义定位到目标即可。
你比如说某个方法的符号引用,
如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。
#1=Methodref #9.#33 这样的为符号引用
2.直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
0x123123123123 这样的地址为直接引用(不指向常量池 而是直接指向内存地址)
为什么会有符号引用?
因为在类没有加载的时候,也不能确保其调用的资源被加载,更何况还有可能调用自身的方法或者字段,就算能确保,其调用的资源也不会每次在程序启动时,都加载在同一个地址。简而言之,在编译阶段,字节码文件根本不知道这些资源在哪,所以根本没办法使用直接引用,于是只能使用符号引用代替,而类加载过程中的解析也只是解析一部分,只对类加载时可以确定的符号引用进行解析。比如父类、接口、静态字段、调用的静态方法等(静态链接)。还有一部分,比如方法中的局部变量、实例字段等在程序运行期间完成的;也就是使用前才去解析它(动态链接)。
《Java虚拟机规范》之中并未规定解析阶段发生的具体时间,虚拟机实现可以根据需要来自行判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(静态链接),还是等到一个符号引用将要被使用前才去解析它(动态链接)。对同一个符号引用进行多次解析请求是很常见的事情,除invokedynamic指令以外,虚拟机实现可以对第一次解析的结果进行缓存。因为invokedynamic指令的目的本来就是用于动态语言支持,这里“动态”的含义是指必须等到程序实际运行到这条指令时,解析动作才能进行。相对地,其余可触发解析的指令都是“静态”的,可以在刚刚完成加载阶段,还没有开始执行代码时就提前进行解析。Java有了Lambda表达式和接口的默认方法,它们在底层调用时就会用到invokedynamic指令.
5.初始化:
除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。初始化阶段就是执行类构造器clinit()方法的过程。clinit()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。
1、clinit()方法是由编译器自动收集类中的所有类变量(静态变量)的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的clinit()方法与类的构造函数(即在虚拟机视角中的实例构造器init()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的clinit()方法执行前,父类的clinit()方法已经执行完毕。clinit()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成
2、init()对象构造时用以初始化对象的,构造器以及非静态初始化块中的代码。
二、类加载器
- BootStrapClassLorder 根类(启动类)加载器: 它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类。
- ExtensionClassLoader 扩展类加载器:它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
- ApplicationClassLoader 系统类(应用程序类)加载器:如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
- 自定义类加载器 自定义类加载器只需要继承java.lang.ClassLoader类 用于加载自己定义目录下的类 其父类加载器默认为ApplicationClassLoader。
boot 类加载器是加载 jre/lib
ext 类加载器是加载 jre/ext/lib
app 类加载器是加载 classpath
classpath是我们程序执行时候打印的目录 classpath 也就是我们程序员自己写的代码
/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/bin/java -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=55758:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
-classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/dnsns.jar:
...
Main函数所在的类是用什么类加载器加载的?
App
jvm要去加载main函数所在的类
boot -> Ext -> App
1. 类加载器初始化过程:
- C++调用java代码创建JVM启动器 实例sun.misc.Launcher 该类由引导类加载器负责加载其它类加载器 sun.misc.Launcher.getLauncher()
- Launcher.getLauncher()方法里做的事情就是初始化ExtensionClassLoader和ApplicationClassLoader 并把他们的关系构造好 ApplicationClassLoader的父类加载器是ExtensionClassLoader
- 父类加载器要注意 不是继承关系 只是父类加载器 他们继承的类都是ClassLoader
ExtClassLoader AppClassLoader 都是Launcher类里的内部类 他们都是继承URLClassLoader(最终继承的都是ClassLoader)
public class Launcher {
...
static class ExtClassLoader extends URLClassLoader {...}
static class AppClassLoader extends URLClassLoader {...}
...
}
public class URLClassLoader extends SecureClassLoader implements Closeable {...}
public class SecureClassLoader extends ClassLoader {...}
// 查看类加载器和父加载器
public class Test_14 {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(DESKeyFactory.class.getClassLoader());
System.out.println(Test_14.class.getClassLoader());
System.out.println("*************************");
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println("appClassLoader父类加载器是:" + extClassLoader);
System.out.println("extClassLoader父类加载器是:" + bootStrapClassLoader);
}
}
null // String的类加载器是引导类加载器 打印null说明了它不是Java类实现的 是C++实现的 所以获取不到
sun.misc.Launcher$ExtClassLoader@eed1f14
sun.misc.Launcher$AppClassLoader@14dad5dc
*************************
appClassLoader父类加载器是:sun.misc.Launcher$ExtClassLoader@eed1f14
extClassLoader父类加载器是:null
2.不同的类加载器加载一个类不相等是为什么?
类加载器加载的类的存储文件空间不一样 boot类加载器有一块内存 Ext类加载器也有一块内存 App加载器也有一块内存 自定义的类加载器也有一块内存
3.什么是双亲委派(就是向上委派)
参考:https://mp.weixin.qq.com/s/E5ZwfpOLqGRK3ZtcsaXAuw
双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,
请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,
即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
// 双亲委派的代码实现逻辑
classLoader1.loadClass("");
点击loaderClass进去
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
}
4.双亲委派的作用?
1、效率:通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
2、安全:java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
3、提供了扩展性: 比如加密 class文件可以反编译 不安全 我们可以对class文件进行加密 用自定义的类加载器进行加载
// 证明了一个加载类重复加载一个类只会加载一次
public class Test_3 extends ClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
Test_3 classLoader1 = new Test_3();
Class<?> class1 = classLoader1.loadClass("com.leetcode.JVM.Test_3");
System.out.println(class1.hashCode());
Test_3 classLoader2 = new Test_3();
Class<?> class2 = classLoader2.loadClass("com.leetcode.JVM.Test_3");
System.out.println(class2.hashCode());
System.out.println("*******************************");
System.out.println(class1==class2);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("ClassLoader");
return null;
}
}
791452441
791452441
*******************************
true
System.out.println(class1==class2); 输出了true为什么?
HashCode相等 证明了一个加载类重复加载一个类只会加载一次
5.双亲委派的局限性
1、无法做到不委派
2、无法做到向下委派
6.怎么打破双向委派
1、自定义加载器去实现 extends ClassLoader 重写loadClass不委派
2、通过线程上下文类加载器去加载所需的SPI服务代码 SPI(Service Provider Interface) SPI是通过向下委派打破双亲委派 是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制
public class User {
public void sout() {
System.out.println("____User");
}
}
// 在E盘创建一层文件目录对应com.leetcode.JVM
// 把User.class文件放到创建的目录文件夹里
public class Test_15 extends ClassLoader {
public static void main(String[] args) throws Exception {
Test_15 test = new Test_15("E:/log");
Class clazz = test.loadClass("com.leetcode.JVM.User", false);
Object object = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(object, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
private String classPath;
Test_15(String name) {
this.classPath = name;
}
private byte[] lodeByte(String name) throws Exception {
name = name.replaceAll("\\.","/");
FileInputStream file = new FileInputStream(classPath + "/" + name + ".class");
int available = file.available();
byte[] data = new byte[available];
file.read(data);
file.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = lodeByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
@Override
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();
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 简单处理 只有这个路径下的才使用自己的类加载器
// 不然无法加载父类Object类 会报错
// java类中的核心包都是不允许使用自己的类加载器去加载的(java.lang包下的) 因为沙箱安全机制
if (!name.startsWith("com.leetcode.JVM")) {
c = this.getParent().loadClass(name);
} else {
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;
}
}
}
7.Tomcat和com.mysql.jdbc.Driver打破双亲委派机制
1、Tomcat中使用自定义的类加载器去加载 不向上委托加载 但公共使用的类还是使用双亲委派。一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的
不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是
独立的,保证相互隔离
2、mysql.jdbc.Driver使用SPI机制去加载 向下委托加载。 因为在某些情况下父类加载器需要委托子类加载器去加载class文件 以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供 DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派。
8.沙箱安全和全盘负责委托机制
防止打破双亲委派修改系统类保护核心库类String Interge等,全盘负责委托机制,指的是当一个ClassLoader装载一个类时,除非显示的使用另外一个ClassLoader,该类所依赖及引用的类也由这个类的ClassLoader加载。