罕見類加載衝突問題:LinkageError

問題描述

假設有C1類和C2類都依賴C0,C1和C2分別用不同的2個類加載器加載,而這兩個類加載器都能在自己的類加載路徑中加載到C0,這個時候如果在C1中調用C2的某個方法(注:這個方法的簽名中依賴了C0)就會出現LinkageError錯誤。

用例模擬及分析

衝突的依賴類,模擬問題描述中的C0

package loader;

public class ConflictDependence {
}

發生錯誤的類,模擬問題描述中的C2

package loader;

public class ErrorTester {
    static {
        // 在其它ClassLoader中必需使用一次這個 MyDependence 依賴,
        // 這樣在另一個ClassLoader使用的時候就會出錯
        System.out.println("ErrorTester MyDependence Location: " +
                ConflictDependence.class.getProtectionDomain().getCodeSource());
    }

    // 不會出錯的方法
//    public static void test0() {
//        System.out.println("ErrorTester MyDependence Location: " +
//                ConflictDependence.class.getProtectionDomain().getCodeSource());
//    }

    // 出錯方法
//    public static void test(ConflictDependence a) {
//    }

    // 出錯方法
    public static ConflictDependence test() {
        return null;
    }
}

被另一個類加載器加載的類,模擬問題描述中的C1

package loader;

public class SubClass {
    private ConflictDependence myDependence;

    public SubClass() {
        System.out.println(ConflictDependence.class.getProtectionDomain().getCodeSource());
        ConflictDependence test = ErrorTester.test();
    }
}

測試類

package loader;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class Test {
    public static void main(String[] args) throws Exception {
        URL[] urls = new URL[]{new File("/home/conquer/Desktop/jse.jar").toURI().toURL()};
        SubFirstLoader newLoader = new SubFirstLoader(urls);
        ErrorTester.test();
        Class<?> b = newLoader.loadClass("loader.SubClass");
        b.newInstance();
    }

    static class SubFirstLoader extends URLClassLoader {
        public SubFirstLoader(URL[] urls) {
            super(urls);
        }
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            try {
                // 從自身類路徑中加載
                return this.findClass(name);
            } catch (Exception e) {
                // 從父ClassLoader加載
                return super.loadClass(name);
            }
        }
    }
}

模擬說明:
1.將以上的4個類打jar包,名爲爲jse.jar或其他名字
2.從jar中刪除ErrorTester和Test兩個class文件
3.修改測試程序Test中的jar路徑,運行測試即可看到出錯信息

問題分析:
測試中使用了兩個類加載器來加載類,使用系統類加載器加載ErrorTester,這個類依賴了ConflictDependence,可以看到靜態塊裏同時也打印出系統類加載器也從自己的加載路徑中加載了ConflictDependence,然後使用自定義的類加載器(一個子優先的類加載器)加載了Sub Class,並通過反射構造其實例,構造過程中依賴了ConflictDependence,所以會去嘗試加載ConflictDependence,由於自定義的加載器是子優先模式的,會先從自身類加載路徑查找並加載,所以可以加載到一個ConflictDependence,注意這個ConflictDependence和系統類加載器加載的並不是同一個,在SubClass中調用了ErrorTester.test()方法,這個方法的簽名依賴的是系統類加載器加載的ConflictDependence,而SubClass也有同名的不同實例的ConflictDependence,因此就產生了衝突,這裏表現爲LinkageError,就是SubClass將自己加載的ConflictDependence鏈接到ErrorTester.test()方法(這個方法的簽名在當前內存中依賴的是系統類加載器加載的ConflictDependence)時產生錯誤。
繼續測試發現,調用test0()方法並無錯誤,這是因爲test0()方法的簽名沒有依賴到衝突的
ConflictDependence(這裏需要注意方法體內的ConflictDependence並不會產生衝突錯誤,這是因爲方法體的字節碼不會編譯到SubClass的字節碼中,類加載器約束檢查只會在當前這個SubClass類的方法中檢查衝突,其他方法體的衝突是在其他方法體內檢查的,簡單說就是類衝突檢查的單元是在一個方法體內),
另外需要說明的是方法的參數和方法的返回值都屬於方法的簽名,方法的簽名都會編譯到調用端的方法體內,所以上面的void test(ConflictDependence a)方法和ConflictDependence test()都會出錯。
另外需要注意的是:ErrorTester中使用了靜態塊一個是可以看出當前實例加載的是哪個ConflictDependence,另一個原因是爲了觸發一下讓系統類加載加載一下ConflictDependence,以促成後面的衝突問題產生,其實如果不在靜態塊中觸發,在test()的方法體中觸發也是可以產生此問題的,同理SubClass中的打印也是爲了觸發當前類的類加載器加載一下ConflictDependence。

關於更多類加載問題,請參考:
ClassLoader問題剖析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章