JVM类加载机制解析
一、java类运行的主要流程(以windows操作系统为例)
通过java命令运行一个java类的主要流程如下图所示:
二、类加载过程详解
1. 类加载过程
类加载过程分为以下几步:
(1)加载
大家都知道,生成的class字节码文件都是存在本地磁盘中的,而加载这一步就是把磁盘中的class字节码文件放到java虚拟机内存中。
(2)验证
在加载前对class字节码文件的正确性进行校验。
(3)准备
为类中的静态变量分配内存并赋默认值。
例如:
public static int a = 1; -> public static int a = 0;
public static boolean b = true; -> public static boolean b = false;
public static Param c = new Param(); -> public static Param c = null;
如果是final修饰的常量则直接赋值为指定值
public static final int d = 1; -> public static final int d = 1;
(4)解析
将符号引用转换为直接引用。
符号:类中的各个名词(例如:main()、String[] args等)
符号引用:类中的各类包,类,接口,字段,方法等元素的全限定名
直接引用:指向数据在虚拟机内容中地址的指针或句柄
相关概念后面会详细分析。
(5)初始化
将静态变量初始化为指定的值并执行静态块。
(6)使用
使用加载完成后的类。
(7)卸载
当类的所有实例都被回收、类对应的类加载器被回收、该类的对应没有任何引用以及无法通过反射访问该类时类会被卸载。
2.类加载时机
java的类加载可以算是懒加载,只有当真正使用到这个类的时候,才会去加载这个类。
public class TestLazyLoad {
static {
System.out.println("-------------load TestLazyLoad-------------");
}
public static void main(String[] args) {
ClassForUse classForUse = new ClassForUse();
ClassForInit classForInit = null;
}
}
class ClassForUse {
static {
System.out.println("-------------load ClassForUse-------------");
}
public ClassForUse() {
System.out.println("-------------init ClassForUse-------------");
}
}
class ClassForInit {
static {
System.out.println("-------------load classForInit-------------");
}
public ClassForInit() {
System.out.println("-------------init classForInit-------------");
}
}
执行TestLazyLoad类的main方法后打印结果:
-------------load TestLazyLoad-------------
-------------load ClassForUse-------------
-------------init ClassForUse-------------
如果将第9行代码改为:ClassForInit classForInit = new ClassForInit(),则打印结果为:
-------------load TestLazyLoad-------------
-------------load ClassForUse-------------
-------------init ClassForUse-------------
-------------load classForInit-------------
-------------init classForInit-------------
由此可以看出,当只声明了ClassForInit类时,该类并没有被加载,
只有当初始化了ClassForinit即使用了该类时,该类才会被加载。
三、类加载器
1. 类加载器的分类
- 引导类加载器:加载支撑JVM运行的位于JRE的lib目录中的核心类库或-Xbootclasspath参数指定的路径
- 扩展类加载器:加载支撑JVM运行的位于JRE的lib目录下的ext目录中的jar包或系统变量java.ext.dirs指定的路径
- 应用程序类加载器:加载ClassPath下的类(自己写的类)
- 自定义类加载器:加载自定义路径的类
前三个类加载器都是JDK提供的,可以通过下面的例子来看看JDK提供的类加载器的具体内容:
public class TestClassLoader {
public static void main(String[] args) {
// 引导类加载器
System.out.println(String.class.getClassLoader());
// 扩展类加载器
System.out.println(ZipDirectoryStream.class.getClassLoader());
// 应用程序类加载器
System.out.println(TestClassLoader.class.getClassLoader());
}
}
执行TestClassLoader的main方法会输出内容为:
null
sun.misc.Launcher$ExtClassLoader@a15670a
sun.misc.Launcher$AppClassLoader@1c898b41
因为引导类加载器是由c++实现的,所以第一行显示null
由此可以得出:
java中使用null表示引导类加载器
用ExtClassLoader表示扩展类加载器
用AppClassLoader表示应用程序类加载器
2. 类加载器初始化过程
可以通过跟踪sun.misc.Launcher来立即类加载器的初始化过程。
由文章开头部分的总流程图可以看出是由c++调用java代码创建sun.misc.Launcher,然后再创建其他类加载器
下面我们通过查看sun.misc.Launcher的源码来解析创建其他类加载器的过程:
Launcher类中有一个静态变量:
private static Launcher launcher = new Launcher();
说明Launcher使用了单例模式,保证整个JVM虚拟机里只有一个Lanucher实例
然后查看Lanucher的构造方法:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader");
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader");
}
// ...省略
}
由构造方法可以看出,Launcher在初始化时创建了ExtClassLoader和AppClassLoader两个类加载器。
先跟踪创建ExtClassLoader的过程:
Launcher.ExtClassLoader.getExtClassLoader() ->
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
// 跟踪getExtDirs方法可得核心代码:String var0 = System.getProperty("java.ext.dirs");
// 即证明了ExtClassLoader加载的就是System.getProperty("java.ext.dirs")对应目录下的类
final File[] var0 = getExtDirs();
// ...省略
return new Launcher.ExtClassLoader(var0);
// ...省略
}
Launcher.ExtClassLoader(var0) ->
public ExtClassLoader(File[] var1) throws IOException {
// 此处第二个参数如果继续跟踪会发现最终是赋值为ExtClassLoader的父类
// 而null对应的类加载器在前文已经分析可以表示为引导类加载器
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
}
// ...省略调用父类构造方法的过程
private ClassLoader(Void unused, ClassLoader parent) {
// 此处的parent就对应上面的(ClassLoader) null,也就是引导类加载器
// 说明ExtClassLoader的父类是引导类加载器
this.parent = parent;
// ...省略
}
再跟踪创建AppClassLoader的过程:
Launcher.AppClassLoader.getAppClassLoader(var1) ->
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
// 证明了AppClassLoader加载的就是System.getProperty("java.class.path")对应目录下的类
final String var1 = System.getProperty("java.class.path");
// ...省略
return new Launcher.AppClassLoader(var1x, var0);
// ...省略
}
Launcher.AppClassLoader(var1x, var0) ->
AppClassLoader(URL[] var1, ClassLoader var2) {
// 此处调用父类的构造方法和ExtClassLoader调用的父类构造方法一样,后面就不继续跟踪了
// 而此处的var2就是上面传入的之前创建的ExtClassLoader
// 说明AppClassLoader的父类是ExtClassLoader
super(var1, var2, Launcher.factory);
}
总上所述,c++调用java代码初始化Launcher时也创建了ExtClassLoader和AppClassLoader,并且
也维护了两者的父类关系即ExtClassLoader的父类为引导类加载器,
AppClassLoader的父类为ExtClassLoader
下面可以通过一个例子来查看三个类加载器之间的关系:
public class TestClassLoaderParent {
public static void main(String[] args) {
// 从ClassLoader.getSystemClassLoader()源码中可得
// 该方法返回值为Launcher中的loader属性,
// 从Launcher的构造方法又可以得到,Launcher中的loader属性就是AppClassLoader
// 所以此处用ClassLoader.getSystemClassLoader()来获取当前JVM中的AppClassLoader
java.lang.ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
java.lang.ClassLoader extClassLoader = appClassLoader.getParent();
java.lang.ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println(" appClassLoader:" + appClassLoader);
System.out.println("appClassLoader's parent:" + extClassLoader);
System.out.println("extClassLoader's parent:" + bootStrapClassLoader);
}
}
运行代码输出的结果为:
appClassLoader:sun.misc.Launcher$AppClassLoader@4921a90
appClassLoader's parent:sun.misc.Launcher$ExtClassLoader@140de648
extClassLoader's parent:null
跟上文的结论完全符合