类初始化及加载类过程
- 执行命令java com.tuling.JVMTest.class。
- 系统会调用java启动程序通过jvm.dll来启动JVM。
- 创建引导类加载器。
- 通过创建的JVM启动器由引导类加载器去创建其他类加载器。JVMTest.class会获取自己的类加载器,通过类加载器加载到内存中。
- 加载到内存后JVM会调用JVMTest.class的main()执行程序。
- JVM销毁
其中第四步即是类加载的一个过程。而加载的详细过程又分为五步 - 加载:通过类加载器去加载到内存。
- 验证:校验class文件是否符合字节码规范。
- 准备:对类中的静态变量分配内存进行第一次默认值赋值,即int = 0 Object = null等。注:final方法准备步骤会直接赋值。
- 解析:符号引用替换为直接引用,对静态方法等只需加载一次不会再变的资源直接指向代码命令所在的内存地址,这是静态链接(类加载期间完成)。动态链接则会保持代码中的符号引用,只有在程序运行时才转化为直接引用(程序运行时完成)。
- 初始化:对类的静态变量初始化为指定值,执行静态代码块。
java程序的类加载实则为懒加载,除了静态只有真正使用new 出来的时候才会进行加载。
public class JVMInitDemo {
static {
System.out.println("init ...");
}
public static void main(String[] args) {
A a = new A();
System.out.println("runing... ");
}
}
class A {
static {
System.out.println("A init...");
}
public A() {
System.out.println("A runing");
}
}
结果:
init ...
A init...
A runing
runing...
由此可见A类是在new出来时才被加载的。
类加载器和双亲委派
类加载器
● 引导类加载器:加载JRE的lib目录下的核心类库,比如rt.jar。
● 扩展类加载器:加载JRE的lib目录下ext扩展目录中的jar包。
● 应用程序类加载器:加载ClassPath下的包,主要就是自己写的包。
● 自定义类加载器:加载自定义路径下的包。
public class ClazzLoad {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.AESKeyGenerator.class.getClassLoader());
System.out.println(ClazzLoad.class.getClassLoader());
}
}
结果:
null // 引导类加载器
sun.misc.Launcher$ExtClassLoader@77459877 // 扩展类加载器
sun.misc.Launcher$AppClassLoader@18b4aac2 // 应用程序类加载器
sun.misc.Launcher是JVM启动器的一个核心类,C++在调用完引导类加载器后会调用它来生成其他类加载器。
引导类加载器为null的原因是是由C++生成的,java代码不能打印。
双亲委派机制
双亲委派机制是指在加载类时先通过AppClassLoader查看类是否被其加载,如果没有则委派父类查看是否加载,委派直到顶级引导类加载器BootStrapClassLoader,如果其可加载则直接返回,找不到则再向下委派。如果所有类加载器都不能加载则抛出异常。
为什么要有双亲委派?
● 沙箱安全机制:通过向上层层委派的方式,防止核心类被侵入篡改。
● 防止类的重复加载:在项目中我们一般用到最多的是ClassPath下自己写的类。这些类一般都由AppClassLoade加载。被类加载器加载过的类会维护在一个类似集合的地方。只需通过findLoadedClass(name)获取,提高效率,保证被加载类的唯一性。
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();
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
}
// 如果类没有被上级加载 则向下委派
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
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;
}
}
全盘负责委托机制
全盘负责是当一个类被类加载器装载时,除非显示的使用某个类加载器。否则该类所引用或依赖的其他类也由这个类加载器去加载。
自定义类加载器
通过继承ClassLoader实现其findClass()对类进行装载。而findClass()时需要把字节数组传人defineClass(name,buf,0,buf.length),这个过程就是通过C++实现类的加载、验证、准备、解析、初始化的过程。
打破双亲委派
在自定义的基础上实现loadClass()并添加逻辑。当加载我们需要打破的类时,使其不走loadClass()逻辑而是直接走自己实现的findClass()进行加载。
TomCat为什么要打破双亲委派?
● 因为TomCat一个Web容器可以部署多个应用程序。而多个应用程序可能会有相同的第三方依赖。当依赖相同但版本不同时可能会出现同一包名和类名被类加载器加载时覆盖的现象。因此要保证不同应用程序间类库是相互独立和隔离的。
● 在部分打破双亲委派的同时,还需保证有相同依赖及相同版本加载的类可以共享。否则多个应用程序在相同容器下,同样的类会有多份。
● 出于安全考虑,Web容器与应用程序所依赖的类库应相互隔离,不能混淆公用一个类加载器。
● Web容器需要支持JSP文件的热加载。JSP其实也是class文件,如果修改后类名相同,类加载器会使用修改前被加载的类,修改后的文件是不会被重新加载的。而要解决这一点,就需要在JSP文件发生变动的时候销毁之前所用的类加载器,重新创建类加载器去加载修改后的JSP文件。
总结
一个class文件是如何加载到jvm并执行的?
- 执行java xxx.class 命令
- java.exe启动并调用java.dll文件。启动并初始化JVM
- JVM会初始化BootStrapClassLoader
- 通过C++调用sun.misc.Launcher#getLaunche,并且初始化getExtClassLoader()、getAppClassLoader(extcl)
- sun.misc.Launcher.getClassLoader()类会获取自己的加载器
- 执行loadClass()加载类
- JVM会调用类的main方法,执行
类加载过程
加载、验证、准备、解析、初始化
注:加载只是load进内存,只有完成加载所有的流程才会被放入jvm元空间。
双亲委派
loadClass()时会先调用parent.loadClass()层层向上委派,如果不属于上级加载的范围内则调用findClass()向下委派去加载。
好处
沙箱安全:防止核心类被侵入。
保证类的唯一性,提高效率。
自定义类加载器
继承ClassLoader() 实现findClass(),自己控制加载类的地址。
打破双亲委派
重写loadClass()核心类依旧交由上级加载器去加载。需要打破的由自己来加载。
TomCat为什么要部分打破双亲委派
● web容器可运行多个应用程序,应用程序相同依赖不同版本会造成冲突或覆盖。需要每个应用程序各自维护自己的类加载器来保证隔离,还可保证web容易和应用程序之间的隔离性。
● 保证多个应用程序加载相同依赖的相同版本时,类加载器中只加载一份。
● jsp也是class文件,如果修改后不重新初始化类加载器,被加载的类还保持原来的即不可实现热加载。可通过web容器监听jsp文件的变化,然后销毁对应servlet类加载器,重新生成然后更改引用,并使其重新加载jsp文件,来实现jsp的热加载。