JVM类加载机制
1、整体流程:
2、加载扩展类加载器以及应用类加载器
JVM自动调用getLauncher()方法获取launcher实例,而Launcher实例是在引导类加载器加载Launcher类的“初始化”阶段进行实例化的(launcher属性被static修饰)。
下面看一下Launcher类的构造方法,可以看到Launcher类是如何加载扩展类加载器以及应用类加载器的:
首先创建扩展类加载器,扩展类加载器是Launcher类的静态内部类,调用扩展类加载器的getExtClassLoader()方法,这里使用单例模式中的双重检查创建ExtClassLoader的实例。扩展类加载器的父加载器是引导类加载器,但是由于引导类加载器是C++实现,所以扩展类加载器的parent属性(parent属性位于顶级父类ClassLoader类中)为null。
然后加载应用类加载器,应用类加载器也是Launcher类的静态内部类,调用应用类加载器的getAppClassLoader()方法,并以扩展类加载器的实例作为入参,最终会将其赋值给AppClassLoader的parent属性(parent属性位于顶级父类ClassLoader类中),该属性会在JVM的双亲委派机制实现中使用。
加载完成后,会将应用类加载器实例赋值给Launcher类的loader属性,所以当调用Launcher类的getClassLoader()方法时,将会返回应用类加载器实例。
3、类加载过程
JVM会调用类加载器的loadClass()方法进行类加载,类加载的过程如下:
加载:将字节码文件加载到内存中,将其存放到运行时数据区的方法区内,并在堆区中创建一个该类的Class对象,Class对象中封装了方法区内的数据结构,并提供了访问方法区数据结构的接口。字节码可以存在于本地磁盘、网络上、zip或jar等归档包、数据库等地方。
验证:校验字节码文件的格式,确保加载的类的正确性。
准备:给类的静态变量分配内存,并初始化默认值,比如int类型就初始化为0,boolean类型就初始化为false等,而非初始化为代码中显式赋予的值;
解析:将类中的符号引用转换为直接引用,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,可以理解为在内存中的地址。
初始化:将类的静态变量初始化为代码中显式赋予的值,并执行类中的静态代码块
类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的 引用、对应class实例的引用等信息。可以使用javap -v Test.class命令,查看这些内容的定义:
类的加载时机:jar包、war包或者其他相关类并不是在应用启动时全部加载的,只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
通过new的方式创建类的实例
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(如
Class.forName(“com.ngu.oa.Test”)
)初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类(
JavaTest
),直接使用java.exe
命令来运行某个主类
4、双亲委派机制
首先介绍一下引导类加载器、扩展类加载器、应用类加载器的作用:
引导类加载器:负责加载位于jre/lib目录下的核心类库,如rt.jar。
扩展类加载器:负责加载位于jre/lib/ext目录下的jar包。
应用类加载器:负责加载classpath路径下的类,也就是自己写的类。
类加载器父子层级关系:
双亲委派机制:在加载某个类时,先查询要加载的类是否已经加载过,如果没有加载过,先委托给父类加载器(这里的父加载器并不是指的继承关系(extends),而是组合关系,该父类加载器指向ClassLoader的parent属性)加载,依次向上委托,直到引导类加载器,如果引导类加载器无法加载,则依次向下的类加载器尝试加载,如果都不能加载,则由自己进行加载,如果自己也加载不了,则会抛出异常ClassNotFoundException。
双亲委派机制的源码如下:
首先尝试从缓存中查询该类是否已经加载过,如果已经加载过则直接返回,否则当前类加载器是否存在父类加载器,如果存在则继续调用父类加载器的loadClass方法,依次向上委托,否则调用启动类加载器进行加载,如果仍未能加载该类,则调用URLClassLoader中的findClass()方法依次向下,由子类加载器加载,如果都未能加载,则抛出ClassNotFoundException。
实现双亲委派机制的原因:
沙箱安全机制:防止核心类库被篡改,比如自己在类路径下定义一个java.lang.String类,是无法被加载的。
避免类重复加载:当父类已经加载该类时,子类加载器不需要再次加载,保证了类的唯一性。
5、全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
6、缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
7、自定义类加载器:自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),该方法实现了双亲委派机制,如果需要打破双亲委派机制,需要重新该方法;另一个方法是findClass(String),默认实现是一个空方法,所以我们自定义类加载器主要是重写findClass()方法。