Tomcat類加載器及應用間class隔離與共享

vjgx1361 8年前發布 | 37K 次閱讀 Tomcat 應用服務器

Tomcat的用戶一定都使用過其應用部署功能,無論是直接拷貝文件到webapps目錄,還是修改server.xml以目錄的形式部署,或者是增加虛擬主機,指定新的appBase等等。

但部署應用時,不知道你是否曾注意過這幾點:

  1. 如果在一個Tomcat內部署多個應用,甚至多個應用內使用了某個類似的幾個不同版本,但它們之間卻互不影響。這是如何做到的。

  2. 如果多個應用都用到了某類似的相同版本,是否可以統一提供,不在各個應用內分別提供,占用內存呢。

  3. 還有時候,在開發Web應用時,在pom.xml中添加了servlet-api的依賴,那實際應用的class加載時,會加載你的servlet-api 這個jar嗎

以上提到的這幾點,在Tomcat以及各類的應用服務器中,都是通過類加載器( ClasssLoader )來實現的。通過本文,你可以了解到Tomcat內部提供的各種類加載器,Web應用的class和資源等加載的方式,以及其內部的實現原理。在遇到類似問題時,更胸有成竹。

類加載器

Java語言本身,以及現在其它的一些基于JVM之上的語言(Groovy,Jython, Scala...),都是在將代碼編譯生成class文件,以實現跨多平臺,write once, run anywhere。最終的這些class文件,在應用中,又被加載到JVM虛擬機中,開始工作。而把class文件加載到JVM的組件,就是我們所說的類加載器。而對于類加載器的抽象,能面對更多的class數據提供形式,例如網絡、文件系統等。

Java中常見的那個 ClassNotFoundExceptionNoClassDefFoundError 就是類加載器告訴我們的。

Servlet規范指出,容器用于加載Web應用內Servlet的class loader, 允許加載位于Web應用內的資源。但不允許重寫java.*, javax.*以及容器實現的類。同時每個應用內使用 Thread.currentThread.getContextClassLoader() 獲得的類加載器,都是該應用區別于其它應用的類加載器等等。

根據Servlet規范,各個應用服務器廠商自行實現。所以像其他的一些應用服務器一樣, Tomcat也 提供了多種的類加載器 ,以便應用服務器內的class以及部署的Web應用類文件運行在容器中時,可以使用不同的class repositories。

在Java中,類加載器是以一種父子關系樹來組織的。除Bootstrap外,都會包含一個parent 類加載器。(這里寫parent 類加載器,而不是父類加載器,不是為了裝X,是為了避免和Java里的 父類 混淆) 一般以類加載器需要加載一個class或者資源文件的時候,他會先委托給他的parent類加載器,讓parent類加載器先來加載,如果沒有,才再在自己的路徑上加載 。這就是人們常說的雙親委托,即把類加載的請求委托給parent。

但是...,這里需要注意一下

對于Web應用的類加載,和上面的雙親委托是有區別的。

在Tomcat中,涉及到的類加載器大致有以下幾類,像官方文檔里這張表示一樣,這里對于Bootstrap和System這種加載Java基礎類的我們不做分析,主要來看一下后面的Common和WebappX這兩類class loader。

Bootstrap
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 ...

Webapp類加載器

正如上面內容所說,Webapp類加載器,相對于傳統的Java的類加載器,最主要的區別是

子優先(child first)

也就是說,在Web應用內,需要加載一個類的時候,不是先委托給parent,而是先自己加載,在自己的類路徑上找不到才會再委托parent。

但是此處的子優先有些地方需要注意的是,Java的基礎類不允許其重新加載,以及servlet-api也不允許重新加載。

那為什么要先child之后再parent呢?我們前面說是Servlet規范規定的。但確實也是實際需要。假如我們兩個應用內使用了相同命名空間里的一個class,一個使用的是Spring 2.x,一個使用的是Spring 3.x。如果是parent先加載的話,在第一個應用加載后,第二個應用再需要的時候,就直接從parent里拿到了,但是卻不符合需要。

另外一點是,各個Web應用的類加載器,是相互獨立的,即WebappClassloader的多個實例,只有這樣,多個應用之間才可能使用不同版本的相同命令空間下的類庫,而不互相受影響。

該類加載器會加載Web應用的WEB-INF/classes內的class和資源文件,以及WEB-INF/lib下的所有jar文件。

當然,有些時候,有需要還按照傳統的Java類加載器加載class時,Tomcat內提供了配置,可以實現父優先。

Common 類加載器

通過上面的class loader組織的圖,可以知道Common 類加載器,是做為webapp類加載器的parent存在的。它是在以下文件中進行配置的:

TOMCAT_HOME/conf/catalina.properties

文檔中給的樣例:

對于目錄結尾的,視為class文件的加載路徑,對于目錄/*.jar結尾的,則視為目錄下所有jar會被加載。

這個配置,默認已經包含了Tomcat的base下的lib目錄和home下的lib目錄。

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

所以,lib目錄下的class和jar文件,在啟動時就都被加載了。

一般來說,這個類加載器用來加載一些既需要Tomcat容器內和所有應用共同可見的class,應用的class不建議放到這兒加載。

介紹完這兩個加載器之后,我們來看文章開始時提到的幾個問題:

  • 多個應用之間類庫不互相沖突,是由于使用了不同的類加載器進行加載的。彼此之間如同路人。即使看起來同樣一個類,使用不同的類加載器加載,也是不同的對象,這點要引起注意。
  • 多個應用之間,如果大家使用了相同的類庫,而且數據眾多,為了避免重復加載占用內存,就可以用到我們的Common 類加載器。只要在配置中指定對應的目錄,然后提取出共用的文件即可。我在之前的公司開發應用服務器時,就有客戶有這樣的需求。
  • 對于我們應用內提供的Servlet-api,其實應用服務器是不會加載的,因為容器已經自已加載過了。當然,這里不是因為父優先還是子優先的問題,而是這類內容,是不允許被重寫的。如果你應用內有一個叫javax.servlet.Servlet的class,那加載后可能就影響了應用內的正常運行了。

我們看在Tomcat6.x中加載一個包含servlet 3.x api的jar,會直接提示jar not loaded.

類加載器實現分析

在Tomcat啟動時,會創建一系列的類加載器,在其主類Bootstrap的初始化過程中,會先初始化classloader,然后將其綁定到Thread中。

public void init() throws Exception {

    initClassLoaders();

Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);  }

其中initClassLoaders方法,會根據catalina.properties的配置,創建相應的classloader。由于默認只配置了common.loader屬性,所以其中只會創建一個出來

private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
            commonLoader=this.getClass().getClassLoader();
        }
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

所以,后面線程中綁定的都一直是commonClassLoader。

然后,當一個應用啟動的時候,會為其創建對應的WebappClassLoader。此時會將commonClassLoader設置為其parent。下面的代碼是StandardContext類在啟動時創建WebappLoader的代碼

if (getLoader() == null) {
    WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
    webappLoader.setDelegate(getDelegate());
    setLoader(webappLoader);
}

這里的getParentClassLoader會從當前組件的classLoader一直向上,找parent classLoader設置。之后注意下一行代碼

webappLoader. setDelegate

這就是在設置后面Web應用的類查找時是父優先還是子優先。這個配置可以在server.xml里,對Context組件進行配置。

即在Context元素下可以嵌套一個 Loader 元素,配置Loader的delegate即可,其默認為false,即子優先。類似于這樣

<Context>

<Loader className="" delegate ="true"/>

</Context>

注意Loader還有一個屬性是reloadable,用于表明對于/WEB-INF/classes/ 和 /WEB-INF/lib 下資源發生變化時,是否重新加載應用。這個特性在開發的時候,還是很有用的。

如果你的應用并沒有配置這個屬性,想要重新加載一個應用,只需要使用manager里的reload功能就可以。

有點跑題,回到我們說的delgate上面來,配置之后,可以指定Web應用類加載時,到底是使用父優先還是子優先。

這里的WebappLoader,就開始了正式的創建WebappClassLoader

private WebappClassLoaderBase createClassLoader()
throws Exception {

    Class<?> clazz = Class.forName(loaderClass);
    WebappClassLoaderBase classLoader = null;

if (parentClassLoader == null) {
parentClassLoader = context.getParentClassLoader();
    }
    Class<?>[] argTypes = { ClassLoader.class };
    Object[] args = { parentClassLoader };
    Constructor<?> constr = clazz.getConstructor(argTypes);
    classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}

配置等信息使用前面Loader內的配置。

應用的classLoader也配置好之后,我們再來看真正應用需要class的時候,是如何子優先的。

在loadClass的時候,會調用到WebappClassLoader的loadClass方法,此時,查找一個class的步驟總結這樣幾步:

這里把方法中分步的注釋拿來羅列一下,

  1. (0) Check our previously loaded local class cache

  2. (0.1) Check our previously loaded class cache

  3. (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

  4. 然后,會判斷是否啟用了securityManager,啟用時會進行packageAccess的檢查。

主要判斷已加載的類里是否已經包含,然后避免Java SE的classes被覆蓋,packageAccess的檢查。

之后,開始了我們的父優先子優先的流程。這里判斷是否使用delegate時,對于一些容器提供的class,也會跳過。

boolean delegateLoad = delegate || filter (name);

這里的filter就用來過濾容器提供的類以及servlet-api的類。

protected synchronized boolean filter(String name) {

if (name == null)
return false;

// Looking up the package
    String packageName = null;
int pos = name.lastIndexOf('.');
if (pos != -1)
        packageName = name.substring(0, pos);
else
        return false;

packageTriggersPermit.reset(packageName);
if (packageTriggersPermit.lookingAt()) {
return false;
    }

然后確定到底是父優先,還是子優先,開始類的加載

父優先

// (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
    }
}

此時如果沒找到,就走到下面的代碼,開始查找本地的資源庫(repository)和子優先時一樣:

// (2) Search local repositories
if (log.isDebugEnabled())
log.debug("  Searching local repositories");
try {
    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
}

如果父優先和子優先都沒能查找到需要的class,此時會拋出

throw new ClassNotFoundException(name);

關于上面代碼,有一個地方,感興趣的同學可以再深入了解下,

clazz = Class.forName(name, false, parent);

也許你這么多年一直直接用Class.forName,沒管過后面還可以多傳兩個參數。

 

 

來自:https://zhuanlan.zhihu.com/p/24168200

 

 本文由用戶 vjgx1361 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!