类加载运行全过程
在windows系统下,当我们要执行xxx.java的main方法时,其实就是要运行这个类的字节码文件。比如说在idea工具当中点击右键执行某个类的main方法时,整个运行过程是怎样的呢?大致过程如下:
- 由java.exe调用底层jvm.dll文件创建java虚拟机(C++实现)。在这个过程中创建引导类加载器实例(C++实现)。这一步就是由C++创建了一个jvm并实例了一个引导类加载器。
- 由C++调用jvm的启动所需要的程序(java代码),比如sun.misc.Launcher。这个类由引导类加载器加载,通过这个类创建其他类加载器。这一步就是由c++调用java代码创建类加载器
- 通过类加载器加载具体的类。
- 当类加载到jvm当中后,由C++调用类的main方法
- main方法结束后jvm销毁
以上就是执行某个类的main方法的大致过程。其中一二四步骤涉及到C++的代码。这里暂时不做研究。本小结主要研究第三步,通过类加载器加载类的过程。
类加载器加载类的过程
类加载器加载类主要经过这几个步骤:加载、验证、准备、解析、初始化。
- 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的 main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口。下篇在研究jvm的各个内存区域。这一步主要把字节码文件加载到jvm中。加载呢不是立即就加载到jvm的内存中还要经过下面几个步骤。
- 验证:校验字节码文件的正确性
- 准备:给类的静态变量分配内存,并赋予默认值。比如说 static int aa = 5; 在准备阶段就会给aa分配内存空间,并赋予int类型的默认值0,就是说这时候aa = 0
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下篇会讲到动态链接。
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块
这里解释下什么是符号引用、直接引用、动态链接、静态链接。比如说代码里面的main方法、返回类型、修饰符、方法名等等都可以称之为符号引用。当我们把字节码加载到jvm当中这些符号引用都会有一个对应的内存地址,这个内存地址就是直接引用。在这个过程中,如果是在类加载期间完成的称之为静态链接,如果运行时完成称之为动态链接。
注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。 jar包或war包里的类不是一次性全部加载的,是使用到时才加载。用一个示例代码演示一下
package common;
public class TestDynamicLoad {
static {
System.out.println("*************load TestDynamicLoad************");
}
public static void main(String[] args) {
new A();
System.out.println("*************load test************");
B b = null;
}
static class A {
static {
System.out.println("*************load A************");
}
public A() {
System.out.println("*************initial A************");
}
}
class B {
static {
System.out.println("*************load B************");
}
public B() {
System.out.println("*************initial B************");
}
}
}
运行结果:
根据上述的类加载流程,当运行main方法时就是,开始加载了TestDynamicLoad,加载的过程中初始化步骤会调用静态代码块。然后执行new A();开始加载A。之后只对B进行了定义,并没有真正是使用,所以是不会加载的。
类加载器和双亲委派机制
类加载器类型
上面的类加载过程主要是通过类加载器来实现的,Java里有如下几种类加载器
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
- 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR
- 类包应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
- 自定义加载器:负责加载用户自定义路径下的类包
用个示例演示下各个类加载器:
package common;
import sun.misc.Launcher;
import java.net.URL;
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
System.out.println(TestJDKClassLoader.class.getClassLoader());
System.out.println("---------------------------------------------------");
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassloader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassloader.getParent();
System.out.println("the bootstrapLoader : " + bootstrapLoader);
System.out.println("the extClassloader : " + extClassloader);
System.out.println("the appClassLoader : " + appClassLoader);
System.out.println("---------------------------------------------------");
System.out.println("bootstrapLoader加载以下文件:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i]);
}
System.out.println("---------------------------------------------------");
System.out.println("extClassloader加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("-----------------------------------------------------");
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
其中com.sun.crypto.provider.DESKeyFactory 是属于扩展包下面的类,位于JRE的lib目录下的ext扩展目录中的JAR。第一个分隔线上面的三行代码就是打印各自的类加载器。看看运行结果
可以看到String类的类加载器是null。为什么是null ? 不应该是引导类加载器。对就是引导类加载器。开头就说了引导类加载器是由C++实现的所以在java环境中看不到。
DESKeyFactory位于ext扩展目下所以类加载器是ExtClassLoader
TestJDKClassLoader 是自己写的所以类加载器是AppClassLoader
为什么类加载器前面都有sun.misc.Launcher。开头说了ExtClassLoader和AppClassLoader是由sun.misc.Launcher这个类创建的。那么我们看看Launcher 是如何创建类加载器的。下面暂时Launcher的部分源码。
C++调用getLauncher()方法。这个方法直接返回launcher,这个launcher就是第一个红框里面的launcher。执行了new launcher(); 所以接着launcher 的构造方法。
launcher 的构造方法:
看源码就要看重点,在来看看getExtClassLoader()这个方法。
getExtClassLoader():
ExtClassLoader是Launcher的静态内部类。继承了URLClassLoader。重点是红框部分,new Launcher.ExtClassLoader。所以ExtClassLoader就被new出来了。当然还有很多细节,具体细节可以去源码,就不深入解读。AppClassLoader同理。
URLClassLoader 就是根据传进来的路径去加载类。
双亲委派机制
下图就是双亲委派的流程
双亲委派机制:当需要加载类的时候,比如说需要加载A这个类,默认由应用程序类加载器(AppClassLoader)去加载,这时候AppClassLoader会去已经加载过的类里面去找有没有,有就返回,如果没有,委托父加载器,也就是扩展类加载器(ExtClassLoader),去ExtClassLoader加载过的类里面去找,如果还是没有继续委托引导类加载器(bootstrapLoader),去bootstrapLoader加载过的类里面去找,如果没有就由bootstrapLoader开始加载。由于A类是自己定义的类。bootstrapLoader加载失败,就由ExtClassLoader去加载,同样ExtClassLoader加载失败,就由AppClassLoader去加载。
整个过程就是双亲委派机制。
看到这里可能会有一些疑问?
- 为什么一开始就要通过AppClassLoader去找有没有加载过类,而不是直接从bootstrapLoader 去找呢。这样不就节省了从AppClassLoader到ExtClassLoader 再到bootstrapLoader 这个过程了吗。
- 为什么要设计双亲委派机制
下面会解答上述问题。
全盘负责委托机制
“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类 所依赖及引用的其他类也由这个ClassLoder载入。
类加载器如何加载类
先回顾一下类加载的过程。Windows下C++通过jvm.dll
创建jvm,同时实例化引导类加载器。引导类加载器调用Launcher 的getLauncher()
方法得到Launcher 对象,通过Launcher 的构造方法创建出ExtClassLoader
和AppClassLoader
这两个类加载器。这些上面有讲到过。
当类加载器创建好后,需要加载类时,C++会调用Launcher 的getClassLoader()
方法获取当前类的类加载器。在调用这个类加载器的loadClass()
方法去加载类。
来看看Launcher的另一部分源码
C++会调用Launcher 的getClassLoader()
直接返回定义的private ClassLoader loader 。那么这个loader是在什么时候给他赋值了呢。在回过头来看看Launcher 的构造方法源码
也就是说在Launcher 的构造方法中给private ClassLoader loader 赋的值一开始是AppClassLoader。
这也就是为什么一开始是AppClassLoader先去找有没有加载过这个类。那为什么这么设计呢?
我们项目中95%的类都是自己定义的,都是AppClassLoader去加载。第一次去加载的时候或许比较麻烦,但是第二次用到的时候AppClassLoader就能会直接找到。如果从bootstrapLoader 开始去加载,每一次需要用到类的时候都会经过bootstrapLoader和ExtClassLoader 的,而这两个加载器是加载不到我们定义的类的。所以这样会更麻烦。 以上就回答了第一个疑问。
回到正题。刚才说了C++会调用Launcher 的getClassLoader()
方法获取当前类的类加载器。在调用这个类加载器的loadClass()
方法去加载类。
来看看这部分源码,以AppClassLoader为样例
最终这个loadClass()方法会调用父类的loadClass()方法。来看看继承结构
所有的类加载器的父类是ClassLoader
。ExtClassLoader 不是AppClassLoader的父类,而是父加载器,不要理解错了。所以最终会调到ClassLoader的loadClass()方法。双亲委派机制的逻辑就在这个方法里面
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//去找已经加载过的类里面有没有要加载的类,有就返回
// findLoadedClass 这个方法最终调用的是C++的本地方法
Class<?> c = findLoadedClass(name);
//c != null 说明这个类已经加载过了 会直接返回
// c== null 执行下面的逻辑
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//判断父加载器是不是为空,如果不为空调用父加载器的loadClass方法。
//父加载器的loadClass方法最终还是会调到ClassLoader的loadClass这个方法
c = parent.loadClass(name, false);
} else {
//如果父加载器为空,调用引导类加载器去找有没有加载过,没有就会去加载。
//findBootstrapClassOrNull 调用的也是C++的方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 上面的if方法就是从AppClassLoader开始委托到引导类加载器。并通过引导类加载器去加载
//下面的if 就是从ExtClassLoader 开始加载类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 去加载类 最终调用的是URLClassLoader的findClass 方法
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;
}
}
最终加载类的是通过 c = findClass(name)
;这行代码去加载。ExtClassLoader和AppClassLoader 都没有这个方法,实际上调用的是他们的父类URLClassLoader
的findClass
方法 。所以来看看URLClassLoader.findClass 源码
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
//核心部分
//根据当前类加载器的加载范围 判断有没有需要加载的类
// res == null 说明没有 就执行 else 方法
// res != null 执行defineClass 方法
if (res != null) {
try {
//这个方法是把类加载到jvm当中 加载过程就是验证、准备、解析、初始化
// defineClass调用是C++
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
以上源码 重要部分都通过注释方式说明了代码的含义,而且大部分代码都是调用C++的代码。下面通过一个例子说下整个过程。
假设需要加载一个Order类。Order 是web应用的一个类。加载大致过程:
-
C++调用getLauncher()方法得到一个单例的Launcher对象。在Launcher的构造方法 会先创建AppClassLoader this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
-
C++调用Launcher的 getClassLoader()方法 获取类加载器。此时获取到的就是AppClassLoader
-
C++调用AppClassLoader 的loadClass()方法。AppClassLoader 的loadClass()方法 最终会调到ClassLoader的loadClass()方法。
-
在ClassLoader的loadClass()方法中 先执行Class<?> c = findLoadedClass(name); 去找AppClassLoader加载过的类里面有没有Order这个类。因为是第一次加载 所以返回null。
-
判断 AppClassLoader 的父加载器是不是等于null 。
AppClassLoader 的父加载器是 ExtClassLoader。所以调用ExtClassLoader的loadClass()
。 最终还是会调用到ClassLoader的loadClass()
方法中。重复第四步。这时找的是ExtClassLoader加载过的类有没有Order 类。也没有。重复第五步。判断ExtClassLoader父加载器是不是空。此时 为空 执行 了else 的方法c = findBootstrapClassOrNull(name)
; 注意:此时AppClassLoader的加载过程没有跑完。在AppClassLoader加载过程中调用了ExtClassLoader的加载过程。等ExtClassLoader加载完成后,还会回调AppClassLoader 的加载过程。 -
c = findBootstrapClassOrNull(name); 这一步就是通过引导类加载器去找有没有加载过Order 这个类。如果没有就通过引导类加载器去加载。引导类加载器加载不到这个类所以返回null
-
接着执行下面这些代码
如果返回null 执行findClass(name)
; 此时的加载器是ExtClassLoader,ExtClassLoader的流程还没跑完。所以调用到ExtClassLoader的findClass(name)
; 方法。 由于ExtClassLoader没有这个方法,实际上调用的是其父类URLClassLoader的findClass方法。 -
在URLClassLoader的findClass方法中执行下面的代码。此时还在ExtClassLoader的执行流程内
ExtClassLoader自然加载不到Order类。 所以res == null 执行else 方法 返回了null 到此 ExtClassLoader的执行流程结束。 -
ExtClassLoader的执行流程结束 返回到了AppClassLoader 的执行流程 同样执行下面的代码
此时调用的是AppClassLoader的findClass方法。最终还是调用到URLClassLoader的findClass方法。重复第八步。 -
通过AppClassLoader 可以加载到Order这个类。所以res != null 执行
return defineClass(name, res);
-
在
return defineClass(name, res)
;这一步中去加载Order 这个类。defineClass
调用的是C++的本地方法。加载过程就是 验证、准备、解析、初始化这几个步骤。最终把Order类加载到了jvm当中。
为什么要设计双亲委派机制
比如说 自定义一个 java.lang.String.class这个类。这个类里面有一些僵尸程序或者病毒什么的。jvm启动的时候引导类加载器会加载jvm的核心代码库,就是jre下的lib目录下的核心类库,比如 rt.jar、charsets.jar等 。 java.lang.String.class就包括在其中。那么jvm还会不会加载我们自己定义的java.lang.String.class?如果加载到了自定义的 java.lang.String.class,这个类有病毒僵尸程序什么的很不安全。
实际上呢根据双亲委派机制,引导类加载器加载到了jre下的String这个类。当需要加载自定义的String类时,根据双亲委派委托到了引导类加载器。而自定义的String类的全限定名和jre下的String的全限定名一样,此时引导类加载器发现 我在jre下面已经加载过这个String类了,就不需要加载了。所以jvm内存当中只有一个String类就是 jre下的String类。
这样做有什么好处呢。
- 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改
- 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性
补充
这一部分还有自定义类加载器、如何打破双亲委派机制和tomcat 如何打破双亲委派机制 还没写。后续当做调优一的番外来写吧,不然篇幅太长。