文章很長,而且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 為您奉上珍貴的學習資源 :
免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 經典圖書:《Java高並發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高並發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高並發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《尼恩Java面試寶典 最新版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取
面試題:什么時候要打破雙親委派機制
來自社群的兩個面試題,其實也是兩個基礎的 面試題,大家一定要掌握
社群問題:
先說下第一題的結論
場景1:
如果委托類沒有實現接口的話,就不能使用newProxyInstance方法,進而不能使用JDK動態代理
場景2:
Cglib是針對類來實現代理的,對指定的目標類生成一個子類,通過方法攔截技術攔截所有父類方法的調用。因為是生成子類,所以就不能用在final修飾的類上。
綜合起來,就是 被final修飾的類 ,不可以被spring代理
接下來就是第二題:如何打破雙親委托機制?
什么是雙親委派機制
當某個類加載器需要加載某個.class
文件時,它首先把這個任務委托給他的上級類加載器,遞歸這個操作,如果上級的類加載器沒有加載,自己才會去加載這個類。
類加載器的類別
BootstrapClassLoader(啟動類加載器)
c++
編寫,加載java
核心庫 java.*
,構造ExtClassLoader
和AppClassLoader
。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作
ExtClassLoader (標准擴展類加載器)
java
編寫,加載擴展庫,如classpath
中的jre
,javax.*
或者
java.ext.dir
指定位置中的類,開發者可以直接使用標准擴展類加載器。
AppClassLoader(系統類加載器)
java`編寫,加載程序所在的目錄,如`user.dir`所在的位置的`class
CustomClassLoader(用戶自定義類加載器)
java
編寫,用戶自定義的類加載器,可加載指定路徑的class
文件
源碼分析
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先檢查這個classsh是否已經加載過了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示沒有加載,如果有父類的加載器則讓父類加載器加載
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父類的加載器為空 則說明遞歸到bootStrapClassloader了
//bootStrapClassloader比較特殊無法通過get獲取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然沒有加載過,則遞歸回來,嘗試自己去加載class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
委派機制的流程圖
雙親委派的作用
①防止加載同一個.class。通過委托去詢問上級是否已經加載過該.class,如果加載過了,則不需要重新加載。保證了數據安全。
②保證核心.class不被篡改。通過委托的方式,保證核心.class不被篡改,即使被篡改也不會被加載,即使被加載也不會是同一個class對象,因為不同的加載器加載同一個.class也不是同一個Class對象。這樣則保證了Class的執行安全。
如何打破雙親委派(/如何破壞雙親委派)
這個問題很經典,面試如果問到JVM,這個問題大概率會被問到,這個時候就輪到給面試官好感,以及漲工資的時刻了
當然可以破壞了,我們知道類的加載方式默認是雙親委派,如果我們有一個類想要通過自定義的類加載器來加載這個類,而不是通過系統默認的類加載器,說白了就是不走雙親委派那一套,而是走自定義的類加載器
我們知道雙親委派的機制是ClassLoader中的loadClass方法實現的,打破雙親委派,其實就是重寫這個方法,來用我們自己的方式來實現即可
當然這里要注意一下,Object.class這是對象的頂級類,改變類的類加載器的時候要注意,如果全部改了,Object.class就找不到了,加載不了了
所以呢,這里重寫的時候,要注意分類解決,把你想要通過自定義類加載器加載的和想通過默認類加載器加載的分隔開
如果不想打破雙親委派模型,就重寫ClassLoader類中的findClass()方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。
而如果想打破雙親委派模型則需要重寫ClassLoader類loadClass()方法(當然其中的坑也不會少)。典型的打破雙親委派模型的框架和中間件有tomcat與osgi
舉例:tomcat是如何打破雙親委派的
通過自定義加載器的過程,我們知道,實現自定義的classloader,需要重新loadClass以及findClass,我們先看
webappsClassLoaderBase的下面兩個方法;
/**
* Load the class with the specified name, searching using the following
* algorithm until it finds and returns the class. If the class cannot
* be found, returns <code>ClassNotFoundException</code>.
* <ul>
* <li>Call <code>findLoadedClass(String)</code> to check if the
* class has already been loaded. If it has, the same
* <code>Class</code> object is returned.</li>
* <li>If the <code>delegate</code> property is set to <code>true</code>,
* call the <code>loadClass()</code> method of the parent class
* loader, if any.</li>
* <li>Call <code>findClass()</code> to find this class in our locally
* defined repositories.</li>
* <li>Call the <code>loadClass()</code> method of our parent
* class loader, if any.</li>
* </ul>
* If the class was found using the above steps, and the
* <code>resolve</code> flag is <code>true</code>, this method will then
* call <code>resolveClass(Class)</code> on the resulting Class object.
*
* @param name The binary name of the class to be loaded
* @param resolve If <code>true</code> then resolve the class
*
* @exception ClassNotFoundException if the class was not found
*/
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
// Log access to stopped class loader
checkStateForClassLoading(name);
// (0) Check our previously loaded local class cache 本地緩存
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);//鏈接classloader todo
return clazz;
}
// (0.1) Check our previously loaded class cache
clazz = findLoadedClass(name);//校驗jvm 的appclassloader的緩存中是否存在
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding Java SE classes. This implements
// SRV.10.7.2
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();//系統的bootstrap classloader 的classloader
boolean tryLoadingFromJavaseLoader;
try {
// Use getResource as it won't trigger an expensive
// ClassNotFoundException if the resource is not available from
// the Java SE class loader. However (see
// https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
// details) when running under a security manager in rare cases
// this call may trigger a ClassCircularityError.
// See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for
// details of how this may trigger a StackOverflowError
// Given these reported errors, catch Throwable to ensure any
// other edge cases are also caught
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// Swallow all exceptions apart from those that must be re-thrown
ExceptionUtils.handleThrowable(t);
// The getResource() trick won't work for this class. We have to
// try loading it directly and accept that we might get a
// ClassNotFoundException.
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
//利用javaser的加載方式 ,即雙親委派的模式,核心類庫
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
//是否使用委托方式,即利用父類加載器加載該類的方式
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
//不使用父類加載器的方式,直接重寫findclass,
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) Delegate to parent unconditionally
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
protected void checkStateForClassLoading(String className) throws ClassNotFoundException {
// It is not permitted to load new classes once the web application has
// been stopped.
try {
checkStateForResourceLoading(className);
} catch (IllegalStateException ise) {
throw new ClassNotFoundException(ise.getMessage(), ise);
}
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug(" findClass(" + name + ")");
checkStateForClassLoading(name);
// (1) Permission to define this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
if (log.isTraceEnabled())
log.trace(" securityManager.checkPackageDefinition");
securityManager.checkPackageDefinition(name.substring(0,i));
} catch (Exception se) {
if (log.isTraceEnabled())
log.trace(" -->Exception-->ClassNotFoundException", se);
throw new ClassNotFoundException(name, se);
}
}
}
// Ask our superclass to locate this class, if possible
// (throws ClassNotFoundException if it is not found)
Class<?> clazz = null;
try {
if (log.isTraceEnabled())
log.trace(" findClassInternal(" + name + ")");
try {
if (securityManager != null) {
PrivilegedAction<Class<?>> dp =
new PrivilegedFindClassByName(name);
clazz = AccessController.doPrivileged(dp);
} else {
//從本地的具體類名查找 內部方式 ;//webapps lib
clazz = findClassInternal(name);
}
} catch(AccessControlException ace) {
log.warn("WebappClassLoader.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
if ((clazz == null) && hasExternalRepositories) {
try {
//調用父類的加載方式
clazz = super.findClass(name);
} catch(AccessControlException ace) {
log.warn("WebappClassLoader.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
}
if (clazz == null) {
if (log.isDebugEnabled())
log.debug(" --> Returning ClassNotFoundException");
throw new ClassNotFoundException(name);
}
} catch (ClassNotFoundException e) {
if (log.isTraceEnabled())
log.trace(" --> Passing on ClassNotFoundException");
throw e;
}
// Return the class we have located
if (log.isTraceEnabled())
log.debug(" Returning class " + clazz);
if (log.isTraceEnabled()) {
ClassLoader cl;
if (Globals.IS_SECURITY_ENABLED){
cl = AccessController.doPrivileged(
new PrivilegedGetClassLoader(clazz));
} else {
//如果父類再加載不到的化,
cl = clazz.getClassLoader();
}
log.debug(" Loaded by " + cl.toString());
}
return clazz;
}
Web應用默認的類加載順序是(打破了雙親委派規則):
- 先從JVM的BootStrapClassLoader中加載。
- 加載Web應用下
/WEB-INF/classes
中的類。 - 加載Web應用下
/WEB-INF/lib/*.jap
中的jar包中的類。 - 加載上面定義的System路徑下面的類。
- 加載上面定義的Common路徑下面的類。
如果在配置文件中配置了``,那么就是遵循雙親委派規則,加載順序如下:
- 先從JVM的BootStrapClassLoader中加載。
- 加載上面定義的System路徑下面的類。
- 加載上面定義的Common路徑下面的類。
- 加載Web應用下
/WEB-INF/classes
中的類。 - 加載Web應用下
/WEB-INF/lib/*.jap
中的jar包中的類。
Tomcat對用戶類庫與類加載器的規划
在其目錄結構下有三組目錄(“/common/”、“/server/”、“/shared/”)可以存放Java類庫,另外還可以加上Web應用程序本身的目錄“/WEB-INF/”,一共4組,把Java類庫放置在這些目錄中的含義分別如下:
1)放置在/commom目錄中:類庫可被Tomcat和所有的Web應用程序共同使用
2)放置在/server目錄中:類庫可被Tomcat使用,對所有的Web應用程序都不可見
3)放置在/shared目錄中:類庫可被所有的Web應用程序所共同使用,但對Tomcat自己不可見
4)放置在/WebApp/WEB-INF目錄中:類庫僅僅可以被此Web應用程序使用,對Tomcat和其他Web應用程序都不可見
為了支持這套目錄結構,並對目錄里面的類庫進行加載和隔離,Tomcat自定義了多個類加載器,這些類加載器按照經典的雙親委派模型來實現,所下圖:
最上面的三個類加載器是JDK默認提供的類加載器,這三個加載器的的作用之前也說過,這里不再贅述了,可以在http://www.cnblogs.com/ghoster/p/7594224.html簡單了解。而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebAppClassLoader則是Tomcat自己定義的類加載器,他們分別加載/common/、/server/、/shared/和/WebApp/WEB-INF/中的Java類庫。其中WebApp類加載器和jsp類加載器通常會存在多個實例,每一個Web應用程序對應一個WebApp類加載器,每一個jsp文件對應一個Jsp類加載器
從上圖的委派關系可以看出,CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader自己能加載的類則與對方相互隔離。WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。而JasperLoader的加載范圍僅僅是這個JSP文件所編譯出來的哪一個Class,它出現的目的就是為了被丟棄:當服務器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並通過在建立一個新的Jsp類加載器來實現JSP文件的HotSwap功能
OSGi:tomcat是如何打破雙親委派的
既然說到OSGI,就要來解釋一下OSGi是什么,以及它的作用
OSGi(Open Service Gateway Initiative):是OSGi聯盟指定的一個基於Java語言的動態模塊化規范,這個規范最初是由Sun、IBM、愛立信等公司聯合發起,目的是使服務提供商通過住宅網管為各種家用智能設備提供各種服務,后來這個規范在Java的其他技術領域也有不錯的發展,現在已經成為Java世界中的“事實上”的模塊化標准,並且已經有了Equinox、Felix等成熟的實現。OSGi在Java程序員中最著名的應用案例就是Eclipse IDE
OSGi中的每一個模塊(稱為Bundle)與普通的Java類庫區別並不大,兩者一般都以JAR格式進行封裝,並且內部存儲的都是Java Package和Class。但是一個Bundle可以聲明它所依賴的Java Package(通過Import-Package描述),也可以聲明他允許導出發布的Java Package(通過Export-Package描述)。在OSGi里面,Bundle之間的依賴關系從傳統的上層模塊依賴底層模塊轉變為平級模塊之間的依賴(至少外觀上如此),而且類庫的可見性能得到精確的控制,一個模塊里只有被Export過的Package才可能由外界訪問,其他的Package和Class將會隱藏起來。除了更精確的模塊划分和可見性控制外,引入OSGi的另外一個重要理由是,基於OSGi的程序很可能可以實現模塊級的熱插拔功能,當程序升級更新或調試除錯時,可以只停用、重新安裝然后啟動程序的其中一部分,這對企業級程序開發來說是一個非常有誘惑性的特性
OSGi之所以能有上述“誘人”的特點,要歸功於它靈活的類加載器架構。OSGi的Bundle類加載器之間只有規則,沒有固定的委派關系。例如,某個Bundle聲明了一個它依賴的Package,如果有其他的Bundle聲明發布了這個Package,那么所有對這個Package的類加載動作都會為派給發布他的Bundle類加載器去完成。不涉及某個具體的Package時,各個Bundle加載器是平級關系,只有具體使用某個Package和Class的時候,才會根據Package導入導出定義來構造Bundle間的委派和依賴
另外,一個Bundle類加載器為其他Bundle提供服務時,會根據Export-Package列表嚴格控制訪問范圍。如果一個類存在於Bundle的類庫中但是沒有被Export,那么這個Bundle的類加載器能找到這個類,但不會提供給其他Bundle使用,而且OSGi平台也不會把其他Bundle的類加載請求分配給這個Bundle來處理
一個例子:假設存在BundleA、BundleB、BundleC三個模塊,並且這三個Bundle定義的依賴關系如下:
BundleA:聲明發布了packageA,依賴了java.*的包
BundleB:聲明依賴了packageA和packageC,同時也依賴了Java.*的包
BundleC:聲明發布了packageC,依賴了packageA
那么,這三個Bundle之間的類加載器及父類加載器之間的關系如下圖:
由於沒有涉及到具體的OSGi實現,所以上圖中的類加載器沒有指明具體的加載器實現,只是一個體現了加載器之間關系的概念模型,並且只是體現了OSGi中最簡單的加載器委派關系。一般來說,在OSGi中,加載一個類可能發生的查找行為和委派關系會比上圖中顯示的復雜,類加載時的查找規則如下:
1)以java.*開頭的類,委派給父類加載器加載
2)否則,委派列表名單內的類,委派給父類加載器加載
3)否則,Import列表中的類,委派給Export這個類的Bundle的類加載器加載
4)否則,查找當前Bundle的ClassPath,使用自己的類加載器加載
5)否則,查找是否在自己的Fragment Bundle中,如果是,則委派給Fragment bundle的類加載器加載
6)否則,查找Dynamic Import列表的Bundle,委派給對應Bundle的類加載器加載
7)否則,查找失敗
從之前的圖可以看出,在OSGi里面,加載器的關系不再是雙親委派模型的樹形架構,而是已經進一步發展成了一種更復雜的、運行時才能確定的網狀結構。
相關面試題:一個類的靜態塊是否可能被執行兩次
一個自於網易面試官的一個問題,一個類的靜態塊是否可能被執行兩次。
答案,如果一個類,被兩個 osgi的bundle加載, 然后又有實例被初始化,其靜態塊會被執行兩次
參考文獻
https://www.jianshu.com/p/1e4011617650
https://www.cnblogs.com/enroute/p/13865807.html
https://www.jianshu.com/p/7706a42ba200
https://blog.csdn.net/m0_37556444/article/details/81912283
https://blog.csdn.net/m0_37556444/article/details/81912283