天天看點

曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可***增加注釋,修改代碼并debug)

背景

大家知道,jdk安裝的目錄下,一般會有個src.zip包,這個包基本對應了rt.jar這個包。rt.jar這個包裡面,就放了jdk中,jdk采用java實作的那部分類庫代碼,比如java.lang包下面的,什麼ArrayList之類的。

如何才能調試這部分代碼呢,這裡的調試,是說,能夠修改源代碼、加注釋、直接debug。

步驟

經過一番思考和探索後,可以這樣:

  1. 解壓src.zip包,因為解壓後,裡面有8000多個檔案,比較大,我們也不需要調試所有的代碼,我就挑了這個包下面的代碼:
    曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可***增加注釋,修改代碼并debug)
    上面看到,類比較多,我們不需要那麼多,隻用下面這部分:
    曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可***增加注釋,修改代碼并debug)
  2. 建立一個普通的maven工程,然後把上面的java包下面的,拷貝到自己的工程的src目錄下

    因為awt、applet之類的,現在都沒人用了,我也就沒拷貝那部分。

    pom.xml真的沒東西,不過還是貼一下:

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
        <groupId>org.learnjdk</groupId>
    
        <modelVersion>4.0.0</modelVersion>
        <packaging>jar</packaging>
        <version>4.7.0</version>
    
        <artifactId>jdk-debug</artifactId>
        <name>jdk-debug</name>
    
        <properties>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
        </properties>
    
        <dependencies>
    
        </dependencies>
    </project>
               
    最後,工程大概就是這樣的。
    曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可***增加注釋,修改代碼并debug)
    然後,自己在test檔案夾下,我建了一個HelloWorld:
    import java.util.ArrayList;
    
    public class HelloWorld {
        public static void main(String[] args) {
            ArrayList<String> list = new ArrayList<>();
            list.add("abc");
        }
    }
               
    理論上來說,就可以調試了嗎?naive!

    F:\gitee-ckl\rocketmq-all-4.7.0-source>java -version

    java version "1.8.0_11"

    Java(TM) SE Runtime Environment (build 1.8.0_11-b12)

    Java HotSpot(TM) 64-Bit Server VM (build 25.11-b03, mixed mode)

    我這邊是jdk的版本,會有個報錯,是源碼不完全比對class導緻的,我這邊會報找不到java.lang.Iterable#iterator方法,大家直接反編譯一下jdk的這個class,就能看到缺了啥了,我這邊加上這個方法就好了:
    public interface Iterable<T> {
        /**
         * Returns an iterator over elements of type {@code T}.
         *
         * @return an Iterator.
         */
        public abstract Iterator<T> iterator();
        
    	...
    }
               

測試demo有什麼問題

大家運作就知道了,根本走不到我們工程裡定義的class

其實整體來說,那個maven工程是沒問題的。走不到那個class,是因為,classloader的問題。

在運作上面的helloWorld時,目前classloader是sun.misc.Launcher.AppClassLoader,它的父類是

sun.misc.Launcher.ExtClassLoader,而sun.misc.Launcher.ExtClassLoader的父類,就是BootStrap類加載器了。

因為AppClassLoader是遵循雙親委派的,是以,在運作下面這個代碼的時候:

import java.util.ArrayList;

public class HelloWorld {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("abc");
    }
}
           

看到ArrayList,AppClassLoader會交給ExtClassLoader去加載,ExtClassLoader會交給BootStrapClassloader去加載,BootStrapClassloader本身負責加載jdk下的rt.jar等核心jar包,而Arraylist正好就是在jdk下面的rt.jar中,是以,最終,Arraylist是由BootStrapClassloader加載的。

那就和我們的工程裡的代碼沒關系了,根本不加載你的。

怎麼讓BootStrap優先加載我們的類

核心其實就是變成了,讓BootStrapClassLoader優先加載我們的類,在我的知識了解裡,BootStrapClassLoader預設就是加載rt.jar的東西,怎麼才能加載我們的呢?隻能求助網際網路了。

然後我查到了這篇文章,-Xbootclasspath

裡面說,用這個參數可以改變BootStrapClassLoader的加載路徑,于是我試了一下:

-Xbootclasspath/p:"F:\gitee-ckl\jdk-debug\target\classes"

           

idea裡,就加在這裡面:

曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可***增加注釋,修改代碼并debug)

然後,大家直接run的話,可以發現,已經沒問題了。

因為我改了工程裡的源碼的:

public boolean add(E e) {
        System.out.println("xxxxx");
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
           

是以我這邊運作的時候,會列印xxxxx:

曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可***增加注釋,修改代碼并debug)

但是,如果你debug,行号應該是對不上的,是以,我們還要這麼操作一波:

idea裡,在左側的項目樹立,對着module按F4,或者右鍵-》Open Module Settings,會打開如下視窗:

曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可***增加注釋,修改代碼并debug)

然後debug,就可以了:

曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可***增加注釋,修改代碼并debug)

然後,這個方案,本來昨晚嘗試的時候,是有問題的,不知道今天為啥就可以了,大家也可以試試。

另一種可行的方案

因為昨晚嘗試上面方案的時候,不知道為啥,沒生效;于是找出了下面的方法。

在helloWorld.java裡,修改如下:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.print(System.getProperty("sun.boot.class.path"));
        
        ArrayList<String> list = new ArrayList<>();
        list.add("abc");
    }
}
           

我們列印了sun.boot.class.path的值,我這邊列印出來後如下:

C:\Program Files\Java\jdk1.8.0_11\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_11\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_11\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_11\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_11\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_11\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_11\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_11\jre\classes
           

這個參數啥意思,差不多就是BootStrap加載class時候,要去查找的路徑。大家也可以參考這兩篇文章:

https://www.cnblogs.com/ahudyan-forever/p/6007458.html

https://blog.csdn.net/briblue/article/details/54973413

是以,我的最終方案就是,把我們的class路徑,放到最前面,大家根據自己的路徑進行修改就行。

大家要注意的是,這裡的路徑,要仔細,粘錯一個字元都不行:

-Dsun.boot.class.path="F:\gitee-ckl\jdk-debug\target\classes;C:\Program Files\Java\jdk1.8.0_11\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_11\jre\lib\rt.jar;C:\Program Files\Java\jdk1.8.0_11\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.8.0_11\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_11\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_11\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_11\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_11\jre\classes"
           

最好直接用下面這個代碼來拼好路徑,免得人工出錯:

public class HelloWorld {
    public static void main(String[] args) {
        String property = System.getProperty("sun.boot.class.path");
        System.setProperty("sun.boot.class.path","F:\\gitee-ckl\\jdk-debug\\target\\classes;" + property);
        System.out.println(System.getProperty("sun.boot.class.path"));
		
    }
}
           

運作方式和前面一樣:

曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可***增加注釋,修改代碼并debug)

該方案為什麼可行

稍微拓展一點,因為我也就知道這麼一點,在sun.misc.Launcher類中:

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    // 1
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
           

1處,這裡定義了一個field,就是去擷取我們前面用到的那個屬性。

這個類裡,有另一個方法來解析這個field。

sun.misc.Launcher.BootClassPathHolder
private static class BootClassPathHolder
  {
    // 0
    static final URLClassPath bcp = new URLClassPath(arrayOfURL, Launcher.factory);

    static
    {
      URL[] arrayOfURL;
      if (Launcher.bootClassPath != null) {
        arrayOfURL = (URL[])AccessController.doPrivileged(new PrivilegedAction()
        {
          public URL[] run() {
            // 1
            File[] arrayOfFile = Launcher.getClassPath(Launcher.bootClassPath);
            int i = arrayOfFile.length;
            HashSet localHashSet = new HashSet();
            for (int j = 0; j < i; j++) {
              // 2
              File localFile = arrayOfFile[j];

              if (!localFile.isDirectory()) {
                localFile = localFile.getParentFile();
              }
              if ((localFile != null) && (localHashSet.add(localFile))) {
                MetaIndex.registerDirectory(localFile);
              }
            }
            // 3
            return Launcher.pathToURLs(arrayOfFile);
          }
        });
      }
      else
        arrayOfURL = new URL[0];
    }
  }
           
  • 1處,把那個屬性,用分隔符分開,解析為一個檔案數組
  • 2處,周遊數組
  • 3處,解析為URL,指派給arrayOfURL。因為這裡是一個匿名内部類,是以第三步的return,隻是return了匿名内部類中的方法
  • 0處,使用arrayOfURL,定義了一個static變量

然後在另一個方法中,會去擷取那個bcp:

sun.misc.Launcher#getBootstrapClassPath    
public static URLClassPath getBootstrapClassPath() {
        return Launcher.BootClassPathHolder.bcp;
}
           

上面這個方法在哪被調用?

java.lang.ClassLoader#getBootstrapClassPath
// Returns the URLClassPath that is used for finding system resources.
static URLClassPath getBootstrapClassPath() {
    return sun.misc.Launcher.getBootstrapClassPath();
}
           

被同屬于ClassLoader類的下列方法調用:

java.lang.ClassLoader#getBootstrapResource    
	/**
     * Find resources from the VM's built-in classloader.
     */
    private static URL getBootstrapResource(String name) {
        URLClassPath ucp = getBootstrapClassPath();
        Resource res = ucp.getResource(name);
        return res != null ? res.getURL() : null;
    }


           

再往上找,就是:

java.lang.ClassLoader#getResource    
public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name);
        }
        if (url == null) {
            url = findResource(name);
        }
        return url;
    }
           

其他就不多分析,大家也比較熟悉了。

兩種方案的差異

第一種方案,加了-Xbootclasspath,這個是在jvm層面去做修改,因為這個-X是虛拟機參數;

第二種方案,上面大家也看到了,是在rt.jar中,java層面的classloader去做修改。

總結

希望大家調試愉快。謝謝大家。有幫助的話,點個推薦。