JVM筆記一java和Tomcat類加載機制
java和Tomcat類加載機制
-
Java類加載
-
* loadClass加載步驟
- 類加載機制
- 類加載器初始化過程
- 雙親委派機制
- 全盤負責委托機制
- 類關系圖
- 自定義類加載器
- 打破雙親委派機制
-
Tomcat類加載器
-
* 為了解決以上問題,tomcat是如何實現類加載機制的?
Java類加載
當我們用java命令運行某個類的main函數啟動程序時,首先需要通過類加載器把主類加載到jvm中。類似流程圖如下。
loadClass加載步驟
類全生命周期:加載 >>驗證>>準備>>解析>>初始化>>使用>>卸載。
加載:在硬盤上查找并通過IO兌入字節碼文件,使用到類時才會加載,例如調用類的main方法,new對象等等,在加載階段會在內存中生成一個代表這個類的java.lang.Class對象(推薦),放在堆中,作為訪問這個類在方法區中類元數據的各個數據的接口。
驗證:校驗字節碼文件的正確性。如:文件格式的驗證,元數據的驗證,字節碼的驗證,符號引用的驗證。
準備:給類的靜態變量分配內存,并賦予默認值,此處給默認值,不一定是我們程序中賦予的值。如果被final修飾,在編譯的時候會給屬性添加ConstantValue屬性,準備階段直接完成賦值,即沒有賦初值這一步
解析:將符號引用 替換為直接引用,該階段會把一些靜態方法(符號引用,比如main()方法)替換為指向數據所存內存的指針或者句柄等(直接引用),這是所謂的靜態鏈接 過程(類加載期間完成),動態鏈接 是在程序運行期間完成的,將符號引用替換為直接引用(// TODO 待補充),解析后的信息存儲在ConstantPoolCache類實例中
初始化:對類的靜態變量初始化為指定的值,執行靜態代碼塊,比如:initData 的666值是此時才賦予的。構造放在在靜態代碼塊之后。
public static final int initData=666;
類被加載到方法區中后,主要包含運行時常量池、類型信息、字段信息、方法信息、類加載器的引用、對應class實例的引用等信息。
類加載器的引用 :這個類到 類加載器實例 的引用。
對應class實例的引用 :類加載器在加載類信息放到方法區中后,會創建一個對應的Class類型的對象實例放到堆(Heap)中,作為開發人員訪問方法區中類定義的入口和切入點。
注意:主類在運行過程中如果使用到其他類,會逐步加載這些列。jar包或者war包里的類不是一次性全部加載的,是使用到時才加載。代碼示例如下:
public class TestDynamicLoad {static {System.out.println("*************load TestDynamicLoad************");}public static void main(String[] args) {new A();System.out.println("*************load test************");B b = null; //B不會加載,除非這里執行 new B()}}class A {static {System.out.println("*************load A************");}public A() {System.out.println("*************initial A************");}
}class B {static {System.out.println("*************load B************");}public B() {System.out.println("*************initial B************");}
}
結果:
*************load TestDynamicLoad************
*************load A************
*************initial A************
*************load test************
類加載機制
類加載過程主要是通過類加載器來實現的,java里有如下幾種類加載器。
- 引導類加載器(bootstrapLoader):負責加載支撐JVM運行的位于JRE的lib目錄下的核心類庫,比如:rt.jar、charsets.jar等
- 擴展類加載器(ExtClassLoader):負責加載支撐JVM運行的位于JRE的lib目錄下的ext擴展目錄中的JAR類包
- 應用程序類加載器(AppClassLoader):負責加載ClassPath路徑下的類包,主要就是加載自己寫的那些類。自定義加載器:負責加載用戶自定義路徑下的類包。
類加載器初始化過程
由上面的加載過程圖可知,JVM啟動,實例sun.misc.Launcher類,而sun.misc.Launcher構造方法內部,創建了兩個類加載器,分別是sun.misc.Launcher.ExtClassLoader(擴展類加載器)和sun.misc.Launcher.AppClassLoader(應用類加載器)。
JVM會默認調用Launcher中的getClassLoader()方法,返回的加載器會是AppClassLoader來加載我們的應用程序。代碼截取
//Launcher的構造方法
public Launcher() {Launcher.ExtClassLoader var1;try {//構造擴展類加載器,在構造的過程中將其父加載器設置為nullvar1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {//構造應用類加載器,在構造的過程中將其父加載器設置為ExtClassLoader,//Launcher的loader屬性值是AppClassLoader,我們一般都是用這個類加載器來加載我們自
己寫的應用程序this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}Thread.currentThread().setContextClassLoader(this.loader);String var2 = System.getProperty("java.security.manager");。。。 。。。 //省略一些不需關注代碼}public ExtClassLoader(File[] var1) throws IOException {// Launcher.ExtClassLoader.getExtClassLoader(); 實例化會走到地方,他父加載器傳的是nullsuper(getExtURLs(var1), (ClassLoader)null, Launcher.factory);SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
雙親委派機制
JVM類加載器是有親子層級結構的,如下圖:
這里類加載其實就是一個雙親委派機制,加載某個類時,launcher的getClassLoader會給出appClassLoader這個加載器,調用其最上層抽象ClassLoader的loadClass方法,其流程是會先找自己有沒**有加載過(并不是加載)**這個類,如果有直接返回,如果沒有,則委托父加載器尋找目標類,而此時父加載器(擴展類加載器)同樣是實現ClassLoader的類,同樣走loadClass方法,邏輯也是找自身是否加載過此類,如果沒有,則繼續委托其父加載器;如果依然找不到目標類,則在自己的類加載路徑中查找并載入目標類。
比如我們的Math類,最先會找應用程序類加載器加載,應用程序類加載器會先委托擴展類加載 器加載,擴展類加載器再委托引導類加載器,頂層引導類加載器在自己的類加載路徑里找了半天沒找到Math類,則向下退回加載Math類的請求,擴展類加載器收到回復就自己加載,在自己的類加載路徑里找了半天也沒找到Math類,又向下退回Math類的加載請求給應用程序類加載器,應用程序類加載器于是在自己的類加載路徑里找Math類,結果找到了就自己加載了。
//ClassLoader的loadClass方法,里面實現了雙親委派機制
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// // 檢查當前類加載器是否已經加載了該類Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) { //如果當前加載器父加載器不為空則委托父加載器加載該類c = parent.loadClass(name, false);} else { //如果當前加載器父加載器為空則委托引導類加載器加載該類c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();//都會調用URLClassLoader的findClass方法在加載器的類路徑里查找并加載該類c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) { //不會執行resolveClass(c);}return c;}
雙親委派說簡單的,就是先找父親加載,不行再有兒子自己加載,此處注意:父類加載器和父類不是一個概念。
全盤負責委托機制
“全盤負責”是指當一個ClassLoader裝載一個類時,除非顯示的使用另一個ClassLoader,該類所有依賴及引用的類也有這個ClassLoader一次載入完畢。(因為正常情況下,依賴和引用類是只有在使用的時候才加載的。)
類關系圖
自定義類加載器
自定義類加載器只需要繼承java.lang.ClassLoader類,該類由兩個核心方法,一個是loadClass(String,boolean),實現了雙親委派機制,還有一個是findClass,默認實現是空方法,所以我們自定義類加載器主要是重寫findClass方法(app和ext因為都是繼承URLClassLoader,此方法在URLClassLoader中實現了)。
package com.tuling.jvm;import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Method;/*** @Description: 自定義類加載器* @ClassName: MyClassLoaderTest* @Author: * @Date: 2021/8/11 0:42**/
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);//defineClass將一個字節數組轉為Class對象,這個字節數組是class文件讀取后最終的字節 數組。return defineClass(name, data, 0, data.length);} catch (Exception e) {e.printStackTrace();throw new ClassNotFoundException();}}}public static void main(String args[]) throws Exception {//初始化自定義類加載器,會先初始化父類ClassLoader,其中會把自定義類加載器的父加載 器設置為應用程序類加載器AppClassLoaderMyClassLoader classLoader = new MyClassLoader("D:"+ File.separator+"program_test");//D盤創建 test/com/tuling/jvm 幾級目錄,將User類的復制類User1.class丟入該目錄Class clazz = classLoader.loadClass("com.tuling.jvm.User1");Object obj = clazz.newInstance();Method method = clazz.getDeclaredMethod("sout", null);method.invoke(obj, null);System.out.println(clazz.getClassLoader().getClass().getName());}
}
打破雙親委派機制
package com.tuling.jvm;import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Method;/*** @Description: 雙親委派和反* @ClassName: MyClassLoaderTest* @Author: * @Date: 2021/8/11 0:42**/
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);//defineClass將一個字節數組轉為Class對象,這個字節數組是class文件讀取后最終的字節 數組。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 loadedClass<?> c = findLoadedClass(name);if (c == null) {// 特定類打破 這里的判斷和實現方式可以更換,思想是這樣if (name.startsWith("com.tuling.jvm")) {c = findClass(name);} else {c = super.loadClass(name, false);}}if (resolve) {resolveClass(c);}return c;}}}public static void main(String args[]) throws Exception {//初始化自定義類加載器,會先初始化父類ClassLoader,其中會把自定義類加載器的父加載 器設置為應用程序類加載器AppClassLoaderMyClassLoader classLoader = new MyClassLoader("D:" + File.separator + "program_test");//D盤創建 test/com/tuling/jvm 幾級目錄,將User類的復制類User1.class丟入該目錄Class clazz = classLoader.loadClass("com.tuling.jvm.User1");Object obj = clazz.newInstance();Method method = clazz.getDeclaredMethod("sout", null);method.invoke(obj, null);System.out.println(clazz.getClassLoader().getClass().getName());}
}
Tomcat類加載器
思考:tomcat是web容器,他需要解決什么問題?
- 一個web容器可能需要部署兩個及以上的應用程序,不同的應用程序可能會依賴同一個第三方類庫的不同版本 ,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。
- 部署在同一個web容器中的**相同的類庫相同的版本可以共享。**否則,如果服務器有10個應用程序,那么要有10分相同的類庫加載進入虛擬機
- **web容器也有自己依賴的類庫,不能與應用程序的類庫混淆,**基于安全考慮,應該讓容器的類庫和程序的類庫隔離開來。
- web容器要支持JSP的修改,jsp文件最終也要編譯成class文件才能在虛擬機中運行,但是jsp修改頻繁,需要熱加載。
為了解決以上問題,tomcat是如何實現類加載機制的?
- commonClassLoader:tomcat最基本的類加載器,加載路徑中的class可以被Tomcat容器本身以及各個WebApp訪問;
- catalinaClassLoader:tomcat容器私有的類加載器,加載路徑中的class對于WebApp不可見。
- sharedClassLoader:各個WebApp共享的類加載器,加載路徑中的class對于所有WebApp可見,但是對于Tomcat容器不可見
- WebAppClassLoader:各個WebApp私有的類加載器,加載路徑中的class只對于當前WebApp可見,比如加載war包里的相關類,每個war包應用都有自己的WebAppClassLoader,實現相互隔離,比如不同war包應用引入了不同的spring版本,這樣實現就能加載給咱的spring版本。webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器,打破了雙親委派機制 。
委派關系:
- CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,從而實現了公有類庫的共用,而CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對方相互隔離。
- WebAppClassLoader可以使用SharedClassLoader加載到的類,但WebAppClassLoader實例之間相互隔離。
- JasperLoader的加載范圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是為了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,并通過再建立一個新的Jsp類加載器來實現JSP文件的HotSwap功能。
加載類關系 :
**注意:**同一個JVM內,兩個相同包名和類名的類對象可以共存,因為他們的類加載器可以不一樣,所以看兩個類對象是否是同一個,除了看類的包名和類名是否都相同之外,還需要他們的類加載器也是同一個才能認為他們是同一個。
課后小問題
1.為什么先執行靜態代碼塊,后執行構造方法?
答:因為在初始化的時候,會執行靜態代碼塊的內容,而構造方法是在new對象的時候調用的。這也正證實加載并不會new一個完整對象出來。而僅僅只會有一個class對象到堆中。
2.為什么要設計雙親委派機制?
答:(1)沙箱安全機制:自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心API庫被隨意篡改。
(2)避免類的重復加載:當父親已經加載了該類時,就沒有必要子Classloader在加載一次,保證被加載的類的唯一性。
3.Tomcat如果使用默認的雙親委派類加載機制行不行? 為什么?
答:不行。這就涉及到Tomcat要解決的問題。
- 如果使用默認的類加載器機制,那么是無法加載兩個相同類庫的不同版本的,默認的類加載器是不管類的版本,只根據全名查找,且只有一份。
- 如何實現jsp的熱加載問題,因為jsp會編譯成calss文件,如果修改了jsp,但是類名并沒有變,類加載器會直接取方法區中的已存在的信息,修改的并不會被加載到。所以,Tomcat的處理辦法是,當你修改jsp,他會卸載掉這個jsp的類加載器,重新創建新的類加載器,加載jsp文件。
4.Tomcat打破了類加載機制,是否可以惡意定義HashMap?是否安全
答:不可以,因為上層類加載器依然沒有變,根據列關系圖,都會走到secureClassLoader,做安全校驗。