JVM性能专题一:JVM类加载机制

一、Java类加载运行全过程

  P1:通过Java命令执行代码的大致过程:

    1)window系统下java.exe调用底层的jvm.dll文件创建Java虚拟机(c++实现)

    2)创建一个引导类加载器实例(c++实现)

    3)c++调用Java代码创建JVM启动器,实例sun.misc.Launcher,该类由引导类加载器负责加载,创建其他类加载器

    4)sun.misc.Launcher.getLauncher()获取运行类自己的类加载器ClassLoader,是AppClassLoader的实例

    5)launcher.getClassLoader()调用loadClass加载要运行的类

    6)加载完成的时候JVM会执行该类的main方法入口

    7)程序运行结束,JVM销毁

  P2:其中loadClass的类加载过程有如下几步:

    1)加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class(InstanceMirrorKlass)对象,作为方法区这个类的各种数据的访问入口

    2)验证:校验字节码文件的正确性

    3)准备:给类的静态变量分配内存,并赋予默认值

    4)解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用

    5)初始化:对类的静态变量初始化为指定的值,执行静态代码块

  P3:类被加载到方法区中:

    主要包含运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息

    1)类加载器的引用:这个类到类加载器实例的引用

    2)对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的class类型的对象实例放到堆(Heap)中,作为开发人员访问方法区中类定义的入口和切入点

    3)主类在运行过程中如果使用到其他类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才会加载

  P4:什么时候会触发类加载:

    1)使用new关键字实例化对象

    2)读取或者设置一个类的静态变量的时候

    3)调用类的静态方法的时候

    4)对类进行反射调用的时候

    5)初始化子类,父类会先被初始化

    6)对类使用动态代理的时候需要被先初始化

二、JVM核心类加载器

  P1:类加载过程主要通过类加载器来实现的:

    1)引导类加载器:负责加载支撑JVM运行的位于 JRE的lib目录下的核心类库,比如rt.jar、charset.jar等

    2)扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包

    3)应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类

    4)自定义类加载器:负责加载用户自定义路径下的类包

  P2:打印类加载器代码清单:

1 public class TestClassLoader {
2 
3   public static void main(String[] args) {
4      System.out.println(String.class.getClassLoader());
5      System.out.println(DESKeyFactory.class.getClassLoader());
6      System.out.println(TestClassLoader.class.getClassLoader());
7   }
8 }

  运行结果:

1 null
2 sun.misc.Launcher$ExtClassLoader@4b67cf4d
3 sun.misc.Launcher$AppClassLoader@18b4aac2

注意:引导类加载器打印结果是null,该加载器对象不是Java对象是c++对象,不做显示

  P3:类加载器初始化过程:

    1)创建JVM启动器实例sun.misc.Launcher(该类初始化使用了单例模式,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例)

    2)在Launcher构造器方法内部,创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)

    3)JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序

  P4:类加载器初始化过程代码清单:

 1 public Launcher() { //Launcher的构造方法
 2         Launcher.ExtClassLoader var1;
 3         try {
 4             var1 = Launcher.ExtClassLoader.getExtClassLoader(); //构造扩展类加载器,在构造的过程中将其父加载器设置为null
 5         } catch (IOException var10) {
 6             throw new InternalError("Could not create extension class loader", var10);
 7         }
 8 
 9         try { //构造应用类加载器,在构造的过程中将其父类加载器设置为ExtClassLoader
10             this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); //Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自己写的应用程序
11         } catch (IOException var9) {
12             throw new InternalError("Could not create application class loader", var9);
13         }
14 
15         Thread.currentThread().setContextClassLoader(this.loader);
16         String var2 = System.getProperty("java.security.manager");
17         if (var2 != null) {
18             SecurityManager var3 = null;
19             if (!"".equals(var2) && !"default".equals(var2)) {
20                 try {
21                     var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
22                 } catch (IllegalAccessException var5) {
23                 } catch (InstantiationException var6) {
24                 } catch (ClassNotFoundException var7) {
25                 } catch (ClassCastException var8) {
26                 }
27             } else {
28                 var3 = new SecurityManager();
29             }
30 
31             if (var3 == null) {
32                 throw new InternalError("Could not create SecurityManager: " + var2);
33             }
34 
35             System.setSecurityManager(var3);
36         }
37 
38     }

三、类加载双亲委派机制

  P1:双亲委派机制:

    1)这里类加载就是一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,父类加载器一直向上委托,直到委托到引导类加载器

    2)每个加载器会加载自己指定的类路径,如果被加载的目标类在该区域,则自己加载,如果目标类不在该区域,则向下委托

    总结:双亲委派机制简单来说,先让父亲加载,不行再由儿子自己加载

  P2:双亲委派机制代码清单:

 1 protected Class<?> loadClass(String name, boolean resolve)
 2         throws ClassNotFoundException
 3     { //ClassLoader的loadClass方法,里面实现了双亲委派机制
 4         synchronized (getClassLoadingLock(name)) {
 5             // First, check if the class has already been loaded
 6             Class<?> c = findLoadedClass(name); //检查当前类加载器是否已经加载了该类
 7             if (c == null) {
 8                 long t0 = System.nanoTime();
 9                 try {
10                     if (parent != null) { //如果当前加载器父加载器不为空则委托父加载器加载该类
11                         c = parent.loadClass(name, false);
12                     } else { //如果当前加载器为空则委托引导类加载器加载该类
13                         c = findBootstrapClassOrNull(name);
14                     }
15                 } catch (ClassNotFoundException e) {
16                     // ClassNotFoundException thrown if class not found
17                     // from the non-null parent class loader
18                 }
19 
20                 if (c == null) {
21                     // If still not found, then invoke findClass in order
22                     // to find the class.
23                     long t1 = System.nanoTime();
24                     c = findClass(name); //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
25 
26                     // this is the defining class loader; record the stats
27                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
28                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
29                     sun.misc.PerfCounter.getFindClasses().increment();
30                 }
31             }
32             if (resolve) { //不会执行
33                 resolveClass(c);
34             }
35             return c;
36         }
37     }

    1)首先,检查一下指定名称的类是否已经加载过了,如果加载过了,就不需要再加载,直接返回

    2)如果此类没有加载过,那么,再判断一下是否有父类加载器,如果有父类加载器,则由父类加载器加载(即调用parent.loadClass(name,false);)或者是调用bootstrap类加载器来加载

    3)如果父类加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载

  P3:为什么要设计双亲委派机制?

    1)沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便防止核心API类库被随意篡改

    2)避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

  P4:类加载示例:

1 package java.lang;
2 
3 public class String {
4     public static void main(String[] args) {
5         System.out.println("********My String Class*********");
6     }
7 }

  运行结果:

错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

四、手写自定义类加载器打破双亲委派机制

  P1:自定义类加载器代码清单:

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

  运行结果:

****自己的类加载器加载类调用方法****
sun.misc.Launcher$AppClassLoader

如上所示:如果控制台输出的类加载器为AppClassLoader,那么请删掉项目中的src/com/spring/Person.java以及target/classes/com/spring/Person.class文件。
造成该现象的原因:向上委派了,APPClassLoader可以在自己的类路径下找到该文件

 

****自己的类加载器加载类调用方法****
com.map.MyClassLoaderTest$MyClassLoader

    1)自定义类加载器只需要继承java.lang.ClassLoader类

    2)该类两个核心方法。一个是loadClass(String,boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要重写findClass方法

   P2:打破双亲委派代码清单:

 1 public class MyClassLoaderTest {
 2 
 3     static class MyClassLoader extends ClassLoader{
 4         private String classPath;
 5 
 6         public MyClassLoader(String classPath){
 7             this.classPath = classPath;
 8         }
 9 
10         private byte[] loadByte(String name) throws Exception {
11             name = name.replaceAll("\\.", "/");
12             FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
13             int len = fis.available();
14             byte[] data = new byte[len];
15             fis.read(data);
16             fis.close();
17             return data;
18         }
19 
20         protected Class<?> findClass(String name) throws ClassNotFoundException {
21             try {
22                 byte[] data = loadByte(name); //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取最终的字节数组
23                 return defineClass(name,data,0,data.length);
24             }catch (Exception e){
25                 e.printStackTrace();
26                 throw new ClassNotFoundException();
27             }
28         }
29 
30         /**
31          * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
32          * @param name
33          * @param resolve
34          * @return
35          * @throws ClassNotFoundException
36          */
37         protected Class<?> loadClass(String name, boolean resolve)
38                 throws ClassNotFoundException
39         {
40             synchronized (getClassLoadingLock(name)) {
41                 // First, check if the class has already been loaded
42                 Class<?> c = findLoadedClass(name);
43                 if (c == null) {
44                         long t1 = System.nanoTime();
45                         if (!name.startsWith("com.spring")){ //如果不是com.spring包下的类,向上委派
46                             c = this.getParent().loadClass(name);
47                         }else {  //如果是,则自己加载
48                             c = findClass(name);
49                         }
50 
51                         // this is the defining class loader; record the stats
52                         sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
53                         sun.misc.PerfCounter.getFindClasses().increment();
54                 }
55                 if (resolve) {
56                     resolveClass(c);
57                 }
58                 return c;
59             }
60         }
61     }
62 
63     public static void main(String[] args) throws Exception {
64         //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
65         MyClassLoader classLoader = new MyClassLoader("D:/test");
66         //D盘创建 test\com\spring几级目录,将Person类的复制类Person.class丢入该目录
67         Class<?> clazz = classLoader.loadClass("com.spring.Person");
68         Object obj = clazz.newInstance();
69         Method method = clazz.getDeclaredMethod("sout", null);
70         method.invoke(obj,null);
71         System.out.println(clazz.getClassLoader().getClass().getName());
72     }
73 }

   运行结果:

****自己的类加载器加载类调用方法****
com.map.MyClassLoaderTest$MyClassLoader

如果出现问题,请仔细想想类加载机制以及双亲委派机制,每一个类加载之前都会加载Object类,这个只能引导类加载器去加载

   注意:同一个JVM内,两个相同包和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为是同一个类

posted @ 2021-01-24 13:18  我看见到处是阳光  阅读(124)  评论(0编辑  收藏  举报