类的加载运行全过程
执行命令java com.xx.xx.class字节码文件
1.运行java.exe执行文件,该文件调用jvm.dll文件创建JVM(C++实现);
2.启动JVM的过程中,会创建引导类加载器实例BootstrapClassLoader(也是C++实现,所以在java中引导类加载器的实例是null);
3.接着C++继续调用java代码来创建JVM启动器实例sun.misc.Launcher,而该类正是由引导类加载器负责加载,在该类加载过程中,调用了无参构造方法,同时创建了扩展类加载器(ExtClassLoader)和应用程序类加载器(AppClassLoader);
4.launcher.getClassLoader获取运行类自己的加载器,是AppClassLoader的实例,调用loadClass方法加载要运行的类
loadClass的类加载过程(生命周期):
加载->验证->准备->解析->初始化->使用->卸载;其中验证、准备、解析 统称为链接(Linking)
加载:在硬盘上查找并通过IO读入字节码文件加载到机器内存中,并在内存中生成一个代表这个类的原型(即类模板对象,一个快照),只有使用到类时才会加载(类的懒加载)。
在类加载时,JVM必须完成以下3件事:
1.通过类的全名,获取类的二进制数据流
2.解析类的二进制数据流为方法区内的数据结构(类模板)
3.创建java.lang.class类的实例(堆中),表示该类型(指向方法区),作为方法区这个类的各种数据的访问入口(Class clazz=Class.forName(“com.xxx.yyy”);,主要用于反射用)
链接:-验证阶段:校验字节码文件的正确性,合法性。(cafebabe)
-准备阶段:给类的静态变量分配内存,并将其初始化为默认值,默认值是由虚拟机决定的,与类型有关。
注:1.这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配内
存了,准备阶段会显示赋值(真实值);
2.这里不会为实例变量分配初始化,因为类变量会被分配在方法区中,而实例变量会随着对象一起分配到Java堆中;
-解析阶段:将符号引用替换为直接引用【在这个阶段会把一些静态方法(类,接口,字段和方法的符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄(直接引用)】, 这就是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,例如方法调用等,因为对象的实例在运行期才能确定对象的内存地址。
初始化:对类的静态变量初始化为用户指定的值,执行静态代码块。
执行类的初始化方法:
类被加载到方法区中后主要包含运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
运行时常量池:在一个类被编译完,会生成这些静态的信息,每个操作会指向常量池中的内容(在经过javap -v Math.class反编译后,每个操作后的 #数字 表示的就是指向对应常量池的内容);在经过类加载的解析过程后,这些静态信息(符号引用)替换为直接引用(直接地址),那么存储该信息的常量池也会变为运行时常量池。
类加载器的引用:这个类到类加载器实例的引用 。
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
类加载六大时机
1.创建类的实例,也就是new一个对象
2.访问类的静态变量
3.访问类的静态方法
4.反射,Class.forName
5.初始化一个类的子类(会首先初始化子类的父类)
6.虚拟机启动时,定义了main()方法的那个类会先触发类加载
类加载器和双亲委派机制
类加载过程主要是通过类加载器来实现的,java里面有如下几种类加载器::
引导类加载器(BootstrapClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
扩展类加载器(ExtClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包
应用程序类加载器(AppClassLoader): 负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
用户自定义类加载器:负责加载用户自定义路径下的类包
类加载过程(双亲委派机制):当加载一个类的时候,最先用应用程序类加载器进行加载,应用程序类加载器首先查看已加载的类中是否有当前需要加载的类,如果有就直接返回,如果没有,则委托父加载器(扩展类加载器)进行加载,扩展类加载器也会从已经加载的类中查找,如果有就直接返回,如果没有,则委托父加载器(引导类加载器)进行加载,如果没有找到,则返回给子加载器(扩展类加载器),扩展类加载器会去ext目录下找,如果没有找到,继续返回子加载器(应用程序类加载器),应用程序类加载器会去classpath下去加载,如下图:
java创建类加载器的源码图:
因此,父加载器只是属于每个自己加载器的parent属性,在创建的时候就已经给其赋值了,并不是继承关系,所有的类加载器的父类都是ClassLoader类。
java类加载过程源码图:
从上图可知,在加载类的时候,会从Launcher.getClassLoader()开始,而这个方法返回的加载器loader正是AppClassLoader,也就是说,不管加载什么类,都会从应用程序类加载器开始加载。最后的defineClass()方法中就是类加载的逻辑,基本是本地方法。由此可见,类加载的双亲委派逻辑是在ClassLoader的loadClass()方法中,
而真正查找类的逻辑是在findClass()方法中,目前findClass()方法只有URLClassLoader实现了,如果要想自定义类加载器,只需继承ClassLoader类,重写findClass()方法即可,如果想要打破双亲委派机制,另外再重写loadClass()方法,实现自己的逻辑,不委派给双亲加载即可。
自定义类加载器示例:
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节
数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
public static void main(String args[]) throws Exception {
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载
器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
//D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
=======自己的加载器加载类调用方法=======
com.tuling.jvm.MyClassLoaderTest$MyClassLoader
打破双亲委派示例:
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
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) {
// 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.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
//尝试用自己改写类加载机制去加载自己写的java.lang.String.class
Class clazz = classLoader.loadClass("java.lang.String");
Object obj = clazz.newInstance();
Method method= clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
问题一:解释下双亲委派机制以及为什么要设计双亲委派机制?
双亲委派机制的解释:双亲委派机制加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
设计双亲委派机制的原因:双亲委派机制可以实现沙箱安全机制与避免类的重复加载。
沙箱安全机制:自己写的java.lang.String.class类不会被加载,可以防止核心API库被随意篡改;
避免类的重复加载:当父加载器已经加载该类时,就直接返回,没必要再加载一次,保证了被加载类只会被加载一次。
问题二:为什么不直接从引导类加载器加载,必须先调用应用程序类加载器进行加载?
因为第一次加载类的时候,应用程序类加载器加载完类后,就会缓存到该加载器中,而且加载的90%以上都是用户写的类,因此应用程序类加载器使用频繁,虽然在首次加载类时多了一次委派操作,但是后面再获取相同类后,就不用再向上委托了,直接从应用程序类加载器的缓存中获取,这样效率会更高。
问题三:Tomcat如果使用默认的双亲委派类加载机制行不行,为什么?
不行,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。