类的加载运行全过程

执行命令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方法加载要运行的类
image.png
loadClass的类加载过程(生命周期):
加载->验证->准备->解析->初始化->使用->卸载;其中验证、准备、解析 统称为链接(Linking)
加载:在硬盘上查找并通过IO读入字节码文件加载到机器内存中,并在内存中生成一个代表这个类的原型(即类模板对象,一个快照),只有使用到类时才会加载(类的懒加载)。
在类加载时,JVM必须完成以下3件事:
1.通过类的全名,获取类的二进制数据流
2.解析类的二进制数据流为方法区内的数据结构(类模板)
3.创建java.lang.class类的实例(堆中),表示该类型(指向方法区),作为方法区这个类的各种数据的访问入口(Class clazz=Class.forName(“com.xxx.yyy”);,主要用于反射用)
image.png
链接:-验证阶段:校验字节码文件的正确性,合法性。(cafebabe)
-准备阶段:给类的静态变量分配内存,并将其初始化为默认值,默认值是由虚拟机决定的,与类型有关。
注:1.这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配内
存了,准备阶段会显示赋值(真实值);
2.这里不会为实例变量分配初始化,因为类变量会被分配在方法区中,而实例变量会随着对象一起分配到Java堆中;
-解析阶段:将符号引用替换为直接引用【在这个阶段会把一些静态方法(类,接口,字段和方法的符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄(直接引用)】, 这就是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,例如方法调用等,因为对象的实例在运行期才能确定对象的内存地址。
初始化:对类的静态变量初始化为用户指定的值,执行静态代码块。
执行类的初始化方法:()方法,由JVM自动生成

类被加载到方法区中后主要包含运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应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下去加载,如下图:
image.png image.png
java创建类加载器的源码图:
未命名文件.png
因此,父加载器只是属于每个自己加载器的parent属性,在创建的时候就已经给其赋值了,并不是继承关系,所有的类加载器的父类都是ClassLoader类。

java类加载过程源码图:
未命名文件 (1).png
从上图可知,在加载类的时候,会从Launcher.getClassLoader()开始,而这个方法返回的加载器loader正是AppClassLoader,也就是说,不管加载什么类,都会从应用程序类加载器开始加载。最后的defineClass()方法中就是类加载的逻辑,基本是本地方法。由此可见,类加载的双亲委派逻辑是在ClassLoader的loadClass()方法中,
而真正查找类的逻辑是在findClass()方法中,目前findClass()方法只有URLClassLoader实现了,如果要想自定义类加载器,只需继承ClassLoader类,重写findClass()方法即可,如果想要打破双亲委派机制,另外再重写loadClass()方法,实现自己的逻辑,不委派给双亲加载即可。

自定义类加载器示例:

  1. public class MyClassLoaderTest {
  2. static class MyClassLoader extends ClassLoader {
  3. private String classPath;
  4. public MyClassLoader(String classPath) {
  5. this.classPath = classPath;
  6. }
  7. private byte[] loadByte(String name) throws Exception {
  8. name = name.replaceAll("\\.", "/");
  9. FileInputStream fis = new FileInputStream(classPath + "/" + name
  10. + ".class");
  11. int len = fis.available();
  12. byte[] data = new byte[len];
  13. fis.read(data);
  14. fis.close();
  15. return data;
  16. }
  17. protected Class<?> findClass(String name) throws ClassNotFoundException {
  18. try {
  19. byte[] data = loadByte(name);
  20. //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节
  21. 数组。
  22. return defineClass(name, data, 0, data.length);
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. throw new ClassNotFoundException();
  26. }
  27. }
  28. }
  29. public static void main(String args[]) throws Exception {
  30. //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载
  31. 器设置为应用程序类加载器AppClassLoader
  32. MyClassLoader classLoader = new MyClassLoader("D:/test");
  33. //D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
  34. Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
  35. Object obj = clazz.newInstance();
  36. Method method = clazz.getDeclaredMethod("sout", null);
  37. method.invoke(obj, null);
  38. System.out.println(clazz.getClassLoader().getClass().getName());
  39. }
  40. }
  41. 运行结果:
  42. =======自己的加载器加载类调用方法=======
  43. 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如果使用默认的双亲委派类加载机制行不行,为什么?

不行,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。