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;
    	}
    }
相關文章
相關標籤/搜索