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内,两个相同包和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为是同一个类