3.深刻jvm内核-原理、诊断与优化-6. 类装载器

  1. class装载验证流程html

    1. 加载java

      装载类的第一个阶段
       取得类的二进制流
       转为方法区数据结构
       在Java堆中生成对应的java.lang.Class对象
    2. 连接mysql

      1. 验证sql

      2. 准备数据库

      3. 解析设计模式

    3. 初始化数组

      执行类构造器<clinit>
       static变量 赋值语句
       static{}语句
       子类的<clinit>调用前保证父类的<clinit>被调用
       <clinit>是线程安全的
  2. 什么是类装载器ClassLoader安全

    ClassLoader是一个抽象类
     ClassLoader的实例将读入Java字节码将类装载到JVM中
     ClassLoader能够定制,知足不一样的字节码流获取方式
     ClassLoader负责类装载过程当中的加载阶段
  3. JDK中ClassLoader默认设计模式数据结构

    1. 方法app

      ClassLoader的重要方法
       public Class<?> loadClass(String name) throws ClassNotFoundException
       载入并返回一个Class
       protected final Class<?> defineClass(byte[] b, int off, int len)
       定义一个类,不公开调用
       protected Class<?> findClass(String name) throws ClassNotFoundException
       loadClass回调该方法,自定义ClassLoader的推荐作法
       protected final Class<?> findLoadedClass(String name) 
       寻找已经加载的类
    2. 分类

      BootStrap ClassLoader (启动ClassLoader)
       Extension ClassLoader (扩展ClassLoader)
       App ClassLoader (应用ClassLoader/系统ClassLoader)
       Custom ClassLoader(自定义ClassLoader)
      
       每一个ClassLoader都有一个Parent做为父亲
    3. 协同工做

      1. 测试类加载顺序

        public class FindClassOrder {
        	public static void main(String args[]){
        	HelloLoader loader=new HelloLoader();
        	loader.print();
        	}
        	}
        public class HelloLoader {
        	public void print(){
        		System.out.println("I am in apploader");
        	}
        	}

        直接运行以上代码: I am in apploader

        加上参数 -Xbootclasspath/a:/Users/heliming/IdeaProjects/democloud/jvm/src/main/java
        //编译这个java文件的class文件放入/Users/heliming/IdeaProjects/democloud/jvm/src/main/java目录
        	public class HelloLoader {
        	public void print(){
        		System.out.println("I am in bootloader");
        	}
        	}

        I am in bootloader
        	此时AppLoader中不会加载HelloLoader
        	I am in apploader 在classpath中却没有加载
        	说明类加载是从上往下的
      2. 测试查找类的时候,是从下往上的

        强制在apploader中加载

        /**
        	 * description: https://www.cnblogs.com/cl-rr/p/9081817.html defineClass()方法更多的是用来加载再也不classes下的文件,或者是在AOP时覆盖原来类的字节码,须要注意的是,对于同名类使用2次及以上defineClass()回抛出异常。
        	 *
        	 * @author: dawn.he QQ:       905845006
        	 * @email: dawn.he@cloudwise.com
        	 * @email: 905845006@qq.com
        	 * @date: 2019/9/24    6:22 PM
        	 */
        	//package com.zejian.classloader;
        
        	import java.io.*;
        	import java.lang.reflect.Method;
        
        	/**
        	 * Created by zejian on 2017/6/21.
        	 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
        	 */
        	public class FileClassLoader extends ClassLoader {
        		private long lastTime;
        		private String rootDir;
        
        		public FileClassLoader(String rootDir) {
        			this.rootDir = rootDir;
        		}
        
        		/**
        		 * 编写findClass方法的逻辑
        		 * @param name
        		 * @return
        		 * @throws ClassNotFoundException
        		 */
        		@Override
        		protected Class<?> findClass(String name) throws ClassNotFoundException {
        			// 获取类的class文件字节数组
        			byte[] classData = getClassData(name);
        			if (classData == null) {
        				throw new ClassNotFoundException();
        			} else {
        				//直接生成class对象
        				return defineClass(name, classData, 0, classData.length);
        			}
        		}
        
        		/**
        		 * 编写获取class文件并转换为字节码流的逻辑
        		 * @param className
        		 * @return
        		 */
        		private byte[] getClassData(String className) {
        			// 读取类文件的字节
        			String path = classNameToPath(className);
        			try {
        				InputStream ins = new FileInputStream(path);
        				ByteArrayOutputStream baos = new ByteArrayOutputStream();
        				int bufferSize = 4096;
        				byte[] buffer = new byte[bufferSize];
        				int bytesNumRead = 0;
        				// 读取类文件的字节码
        				while ((bytesNumRead = ins.read(buffer)) != -1) {
        					baos.write(buffer, 0, bytesNumRead);
        				}
        				return baos.toByteArray();
        			} catch (IOException e) {
        				e.printStackTrace();
        			}
        			return null;
        		}
        
        		/**
        		 * 类文件的彻底路径
        		 * @param className
        		 * @return
        		 */
        		private String classNameToPath(String className) {
        			return rootDir + File.separatorChar
        					+ className.replace('.', File.separatorChar) + ".class";
        		}
        
        		public static void main(String[] args) throws ClassNotFoundException {
        	//        String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
        			String rootDir="/Users/heliming/IdeaProjects/democloud/jvm/target/classes/";
        
        			//建立自定义文件类加载器
        			FileClassLoader loader = new FileClassLoader(rootDir);
        
        			try {
        				//加载指定的class文件
        	//            Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
        				Class<?> object1=loader.loadClass("HelloLoader");
        				Object o = object1.newInstance();
        				Method method=o.getClass().getDeclaredMethod("print", null);
        				method.invoke(o, null);
        				//输出结果:I am in apploader
        			} catch (Exception e) {
        				e.printStackTrace();
        			}
        		}
        	}

        打印:I am in apploader

        在查找类的时候,先在底层的Loader查找,是从下往上的。Apploader能找到,就不会去上层加载器加载

      3. 替换掉上边的main函数,测试findClass只能加载一次

        public static void main(String[] args) throws ClassNotFoundException {
        			String rootDir = "/Users/heliming/IdeaProjects/democloud/jvm/target/classes/";
        			//建立自定义文件类加载器
        			FileClassLoader loader = new FileClassLoader(rootDir);
        			FileClassLoader loader2 = new FileClassLoader(rootDir);
        
        			try {
        
        				Class<?> object1 = loader.loadClass("HelloLoader");
        
        				Object o = object1.newInstance();
        				Method method = o.getClass().getDeclaredMethod("print", null);
        				method.invoke(o, null);
        				Class<?> object2 = loader2.loadClass("HelloLoader");
        				o = object1.newInstance();
        				method = o.getClass().getDeclaredMethod("print", null);
        				method.invoke(o, null);
        				System.out.println("loadClass->obj1:" + object1.hashCode());
        				System.out.println("loadClass->obj2:" + object2.hashCode());
        
        				//加载指定的class文件,直接调用findClass(),绕过检测机制,建立不一样class对象。
        				Class<?> object3 = loader.findClass("HelloLoader");
        
        				//findClass只能加载一次 若是再次加载就会报错重复加载类
        				//Class<?> object5 = loader.findClass("HelloLoader");
        
        
        				Class<?> object4 = loader2.findClass("HelloLoader");
        
        				System.out.println("loadClass->obj3:" + object3.hashCode());
        				System.out.println("loadClass->obj4:" + object4.hashCode());
        				/**
        				 * 输出结果:
        				 * loadClass->obj1:644117698
        				 loadClass->obj2:644117698
        				 findClass->obj3:723074861
        				 findClass->obj4:895328852
        				 */
        
        			} catch (Exception e) {
        				e.printStackTrace();
        			}
        		}
    4. 双亲委派模式的问题

    5. 解决:

      Thread. setContextClassLoader()
      	上下文加载器
      	是一个角色
      	用以解决顶层ClassLoader没法访问底层ClassLoader的类的问题
      	基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例

      从图可知rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,因为SPI中的类常常须要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)没法经过Bootstrap类加载器加载,所以只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程当中抛弃双亲委派加载链模式,使程序能够逆向使用类加载器,固然这也使得Java类加载器变得更加灵活。为了进一步证明这种场景,不妨看看DriverManager类的源码,DriverManager是Java核心rt.jar包中的类,该类用来管理不一样数据库的实现驱动即Driver,它们都实现了Java核心包中的java.sql.Driver接口,如mysql驱动包中的com.mysql.jdbc.Driver,这里主要看看如何加载外部实现类,在DriverManager初始化时会执行以下代码

      //DriverManager是Java核心包rt.jar的类
      	public class DriverManager {
      		//省略没必要要的代码
      		static {
      			loadInitialDrivers();//执行该方法
      			println("JDBC DriverManager initialized");
      		}
      
      	//loadInitialDrivers方法
      	 private static void loadInitialDrivers() {
      		 sun.misc.Providers()
      		 AccessController.doPrivileged(new PrivilegedAction<Void>() {
      				public Void run() {
      					//加载外部的Driver的实现类
      					ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
      				  //省略没必要要的代码......
      				}
      			});
      		}

      在DriverManager类初始化时执行了loadInitialDrivers()方法,在该方法中经过ServiceLoader.load(Driver.class);去加载外部实现的驱动类,ServiceLoader类会去读取mysql的jdbc.jar下META-INF文件的内容,以下所示

      而com.mysql.jdbc.Driver继承类以下:

      public class Driver extends com.mysql.cj.jdbc.Driver {
      		public Driver() throws SQLException {
      			super();
      		}
      
      		static {
      			System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
      					+ "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
      		}
      	}

      从注释能够看出日常咱们使用com.mysql.jdbc.Driver已被丢弃了,取而代之的是com.mysql.cj.jdbc.Driver,也就是说官方再也不建议咱们使用以下代码注册mysql驱动

      //不建议使用该方式注册驱动类
      	Class.forName("com.mysql.jdbc.Driver");
      	String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
      	// 经过java库获取数据库链接
      	Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");

      而是直接去掉注册步骤,以下便可

      String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
      	// 经过java库获取数据库链接
      	Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");

      这样ServiceLoader会帮助咱们处理一切,并最终经过load()方法加载,看看load()方法实现

      public static <S> ServiceLoader<S> load(Class<S> service) {
      		 //经过线程上下文类加载器加载
      		  ClassLoader cl = Thread.currentThread().getContextClassLoader();
      		  return ServiceLoader.load(service, cl);
      	  }

      很明显了确实经过线程上下文类加载器加载的,实际上核心包的SPI类对外部实现类的加载都是基于线程上下文类加载器执行的,经过这种方式实现了Java核心代码内部去调用外部实现类。咱们知道线程上下文类加载器默认状况下就是AppClassLoader,那为何不直接经过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?实际上是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不一样服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,由于这些服务使用的线程上下文类加载器并不是AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不一样。,因此咱们应用该少用getSystemClassLoader()。总之不一样的服务使用的可能默认ClassLoader是不一样的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免没必要要的问题。ok~.关于线程上下文类加载器暂且聊到这,前面阐述的DriverManager类,你们能够自行看看源码,相信会有更多的体会,另外关于ServiceLoader本篇并无过多的阐述,毕竟咱们主题是类加载器,但ServiceLoader是个很不错的解耦机制,你们能够自行查阅其相关用法。

  4. 打破常规模式 and 热替换

    双亲模式的破坏
    	双亲模式是默认的模式,但不是必须这么作
    	Tomcat的WebappClassLoader 就会先加载本身的Class,找不到再委托parent
    	OSGi的ClassLoader造成网状结构,根据须要自由加载Class

    在java目录下

    javac Worker.java
    
    启动main函数 
    修改Worker.java
    再次javac Worker.java
    输出改变了

    HelloMain.java

    import java.io.File;
    import java.lang.reflect.Method;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class HelloMain {
    	private URLClassLoader classLoader;
    	private Object worker;
    	private long lastTime;
    //	private String classDir="/Users/heliming/IdeaProjects/democloud/jvm/target/classes/";
    	private String classDir="/Users/heliming/IdeaProjects/democloud/jvm/src/main/java/";
    	public static void main(String[] args) throws Exception {
    		HelloMain helloMain=new HelloMain();
    		helloMain.execute();
    	}
    
    	private void execute() throws Exception {
    		while(true){
    			//监测是否须要加载
    			if(checkIsNeedLoad()){
    				System.out.println("检测到新版本,准备从新加载");
    				reload();
    				System.out.println("从新加载完成");
    			}
    			//一秒
    			invokeMethod();
    			Thread.sleep(1000);
    
    		}
    	}
    
    	private void invokeMethod() throws Exception {
    		//经过反射方式调用
    		//使用反射的主要缘由是:不想Work被appclassloader加载,
    //		若是被appclassloader加载的话,再经过自定义加载器加载会有点问题
    		Method method=worker.getClass().getDeclaredMethod("sayHello", null);
    		method.invoke(worker, null);
    	}
    
    	private void reload() throws Exception {
    		classLoader = new MyClassLoader(new URL[] { new URL(
    				"file:"+classDir)});
    		worker =  classLoader.loadClass("Worker")
    				.newInstance();
    		System.out.println(worker.getClass());
    
    	}
    
    	private boolean checkIsNeedLoad() {
    		File file=new File(classDir+ "Worker.class");
    		long newTime=file.lastModified();
    		if(lastTime<newTime){
    			lastTime=newTime;
    			return true;
    		}
    		return false;
    	}
    
    
    }

    Worker.java

    public class Worker {
    	public void sayHello(){
    		System.out.println("version:fds");
    	}
    }

    MyClassLoader.java

    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class MyClassLoader extends URLClassLoader {
    
    	public MyClassLoader(URL[] urls) {
    		super(urls);
    	}
    
    	// 打破双亲模式,保证本身的类会被本身的classloader加载
    
    	@Override
    	protected synchronized Class<?> loadClass(String name, boolean resolve)
    			throws ClassNotFoundException {
    		Class c = findLoadedClass(name);
    		if (c == null) {
    			try {
    				//这里若是是先加载本身没法找到object类会报错的因此catch下
    				c=findClass(name);
    			} catch (Exception e) {
    			}
    		}
    		if(c==null){
    			c=super.loadClass(name, resolve);
    		}
    		return c;
    	}
    }
相关文章
相关标签/搜索