欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Golang實(shí)現(xiàn)Java虛擬機(jī)之解析class文件詳解

 更新時(shí)間:2024年01月11日 09:18:45   作者:橡皮筋兒  
這篇文章主要為大家詳細(xì)介紹了Golang實(shí)現(xiàn)Java虛擬機(jī)之解析class文件的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

前言

所需前置知識(shí)為:JAVA語言、JVM知識(shí)

對(duì)應(yīng)項(xiàng)目:jvmgo

一、準(zhǔn)備環(huán)境

操作系統(tǒng):Windows 11

1.1 JDK版本

openjdk version "1.8.0_382"

1.2 Go版本

go version go1.21.0 windows/amd64

1.3 配置Go工作空間

1.4 java命令指示

Java虛擬機(jī)的工作是運(yùn)行Java應(yīng)用程序。和其他類型的應(yīng)用程序一樣,Java應(yīng)用程序也需要一個(gè)入口點(diǎn),這個(gè)入口點(diǎn)就是我們熟知的main()方法。最簡單的Java程序是 只有一個(gè)main()方法的類,如著名的HelloWorld程序。

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, world!");
  }
}

JVM如何知道從哪個(gè)類啟動(dòng)呢,虛擬機(jī)規(guī)范并沒有明確,而是需要虛擬機(jī)實(shí)現(xiàn)。比如Oracle的JVM就是通過java命令啟動(dòng)的,主類名由命令行參數(shù)決定。

java命令有如下4種形式:

java [-options] class [args]
java [-options] -jar jarfile [args]
javaw [-options] class [args]
javaw [-options] -jar jarfile [args]

可以向java命令傳遞三組參數(shù):選項(xiàng)、主類名(或者JAR文件名) 和main()方法參數(shù)。選項(xiàng)由減號(hào)(–)開頭。通常,第一個(gè)非選項(xiàng)參數(shù) 給出主類的完全限定名(fully qualified class name)。但是如果用戶提供了–jar選項(xiàng),則第一個(gè)非選項(xiàng)參數(shù)表示JAR文件名,java命令必須從這個(gè)JAR文件中尋找主類。javaw命令和java命令幾乎一樣,唯 一的差別在于,javaw命令不顯示命令行窗口,因此特別適合用于啟 動(dòng)GUI(圖形用戶界面)應(yīng)用程序。

選項(xiàng)可以分為兩類:標(biāo)準(zhǔn)選項(xiàng)和非標(biāo)準(zhǔn)選項(xiàng)。標(biāo)準(zhǔn)選項(xiàng)比較穩(wěn)定,不會(huì)輕易變動(dòng)。非標(biāo)準(zhǔn)選項(xiàng)以-X開頭,

選項(xiàng)用途
-version輸出版本信息,然后退出
-? / -help輸出幫助信息,然后退出
-cp / -classpath指定用戶類路徑
-Dproperty=value設(shè)置Java系統(tǒng)屬性
-Xms<size>設(shè)置初始堆空間大小
-Xmx<size>設(shè)置最大堆空間大小
-Xss<size>設(shè)置線程??臻g大小

二、編寫命令行工具

環(huán)境準(zhǔn)備完畢,接下來實(shí)現(xiàn)java命令的的第一種用法。

2.1 創(chuàng)建目錄

創(chuàng)建cmd.go

Go源文件一般以.go作為后綴,文件名全部小寫,多個(gè)單詞之間使用下劃線分隔。Go語言規(guī)范要求Go源文件必須使用UTF-8編碼,詳見https://golang.org/ref/spec

2.2 結(jié)構(gòu)體存儲(chǔ)cmd選項(xiàng)

在文件中定義cmd中java命令需要的選項(xiàng)和參數(shù)

package ch01

// author:郜宇博
type Cmd struct {
	// 標(biāo)注是否為 --help
	helpFlag bool
	//標(biāo)注是否為 --version
	versionFlag bool
	//選項(xiàng)
	cpOption string
	//主類名,或者是jar文件
	class string
	//參數(shù)
	args []string
}

Go語言標(biāo)準(zhǔn)庫包

由于要處理的命令行,因此將使用到flag()函數(shù),此函數(shù)為Go的標(biāo)準(zhǔn)庫包之一。

Go語言的標(biāo)準(zhǔn)庫以包的方式提供支持,下表列出了Go語言標(biāo)準(zhǔn)庫中常見的包及其功能。

Go語言標(biāo)準(zhǔn)庫包名功 能
bufio帶緩沖的 I/O 操作
bytes實(shí)現(xiàn)字節(jié)操作
container封裝堆、列表和環(huán)形列表等容器
crypto加密算法
database數(shù)據(jù)庫驅(qū)動(dòng)和接口
debug各種調(diào)試文件格式訪問及調(diào)試功能
encoding常見算法如 JSON、XML、Base64 等
flag命令行解析
fmt格式化操作
goGo語言的詞法、語法樹、類型等??赏ㄟ^這個(gè)包進(jìn)行代碼信息提取和修改
htmlHTML 轉(zhuǎn)義及模板系統(tǒng)
image常見圖形格式的訪問及生成
io實(shí)現(xiàn) I/O 原始訪問接口及訪問封裝
math數(shù)學(xué)庫
net網(wǎng)絡(luò)庫,支持 Socket、HTTP、郵件、RPC、SMTP 等
os操作系統(tǒng)平臺(tái)不依賴平臺(tái)操作封裝
path兼容各操作系統(tǒng)的路徑操作實(shí)用函數(shù)
pluginGo 1.7 加入的插件系統(tǒng)。支持將代碼編譯為插件,按需加載
reflect語言反射支持??梢詣?dòng)態(tài)獲得代碼中的類型信息,獲取和修改變量的值
regexp正則表達(dá)式封裝
runtime運(yùn)行時(shí)接口
sort排序接口
strings字符串轉(zhuǎn)換、解析及實(shí)用函數(shù)
time時(shí)間接口
text文本模板及 Token 詞法器

flag()函數(shù)

[]: https://studygolang.com/pkgdoc

eg:flag.TypeVar()

基本格式如下: flag.TypeVar(Type指針, flag名, 默認(rèn)值, 幫助信息) 例如我們要定義姓名、年齡、婚否三個(gè)命令行參數(shù),我們可以按如下方式定義:

var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "張三", "姓名")
flag.IntVar(&age, "age", 18, "年齡")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "時(shí)間間隔")

2.3 接收處理用戶輸入的命令行指令

創(chuàng)建parseCmd()函數(shù),實(shí)現(xiàn)接受處理用戶輸入的命令行指令

func parseCmd() *Cmd {
	cmd := &Cmd{}

	flag.Usage = printUsage
	flag.BoolVar(&cmd.helpFlag, "help", false, "print help message")
	flag.BoolVar(&cmd.helpFlag, "?", false, "print help message")
	flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit")
	flag.StringVar(&cmd.cpOption, "classpath", "", "classpath")
	flag.StringVar(&cmd.cpOption, "cp", "", "classpath")
	flag.Parse()

	args := flag.Args()
	if len(args) > 0 {
		cmd.class = args[0]
		cmd.args = args[1:]
	}

	return cmd
}

func printUsage() {
	fmt.Printf("Usage: %s [-options] class [args...]\n", os.Args[0])
	//flag.PrintDefaults()
}

首先設(shè)置flag.Usage變量,把printUsage()函數(shù)賦值給它;

然后調(diào) 用flag包提供的各種Var函數(shù)設(shè)置需要解析的選項(xiàng);

接著調(diào)用 Parse()函數(shù)解析選項(xiàng)。

如果Parse()函數(shù)解析失敗,它就調(diào)用printUsage()函數(shù)把命令的用法打印到控制臺(tái)。

如果解析成功,調(diào)用flag.Args()函數(shù)可以捕獲其他沒有被解析 的參數(shù)。其中第一個(gè)參數(shù)就是主類名,剩下的是要傳遞給主類的參數(shù)。

2.4 測試

與cmd.go文件一樣,main.go文件的包名也是main。在Go 語言中,main是一個(gè)特殊的包,這個(gè)包所在的目錄(可以叫作任何 名字)會(huì)被編譯為可執(zhí)行文件。Go程序的入口也是main()函數(shù),但 是不接收任何參數(shù),也不能有返回值。

測試代碼如下:

package main

import "fmt"

func main() {
	cmd := parseCmd()
	if cmd.versionFlag {
		//模擬輸出版本
		fmt.Println("version 0.0.1")
	} else if cmd.helpFlag || cmd.class == "" {
		printUsage()
	} else {
		startJVM(cmd)
	}
}

// 模擬啟動(dòng)JVM
func startJVM(cmd *Cmd) {
	fmt.Printf("classpath:%s class:%s args:%v\n",
		cmd.cpOption, cmd.class, cmd.args)
}

main()函數(shù)先調(diào)用ParseCommand()函數(shù)解析命令行參數(shù),如 果一切正常,則調(diào)用startJVM()函數(shù)啟動(dòng)Java虛擬機(jī)。如果解析出現(xiàn)錯(cuò)誤,或者用戶輸入了-help選項(xiàng),則調(diào)用PrintUsage()函數(shù)打印出幫助信息。如果用戶輸入了-version選項(xiàng),則輸版本信息。因?yàn)槲覀冞€沒有真正開始編寫Java虛擬機(jī),所以startJVM()函數(shù)暫時(shí)只是打印一些信息而已。

在終端:

go install jvmgo\ch0

此時(shí)在工作空間的bin目錄中會(huì)生成ch01.exe的文件,運(yùn)行:結(jié)果如下

三、獲取類路徑

已經(jīng)完成了JAVA應(yīng)用程序如何啟動(dòng):命令行啟動(dòng),并獲取到了啟動(dòng)時(shí)需要的選項(xiàng)和參數(shù)。

但是,如果要啟動(dòng)一個(gè)最簡單的“Hello World”程序(如下),也需要加載很多所需的類進(jìn)入JVM

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, world!");
  }
}

加載HelloWorld類之前,需要加載該類的父類(超類),也就是java.lang.Object,main函數(shù)的參數(shù)為String[]類型,因此也需要將java.lang.String類和java.lang.String[]加載,輸出字符串又需要加載java.lang.System類,等等。接下來就來解決如何獲取這些類的路徑。

3.1類路徑介紹

Java虛擬機(jī)規(guī)范并沒有規(guī)定虛擬機(jī)應(yīng)該從哪里尋找類,因此不同的虛擬機(jī)實(shí)現(xiàn)可以采用不同的方法。

Oracle的Java虛擬機(jī)實(shí)現(xiàn)根據(jù)類路徑(class path)來搜索類。

按照搜索的先后順序,類路徑可以 分為以下3個(gè)部分:

  • 啟動(dòng)類路徑(bootstrap classpath)
  • 擴(kuò)展類路徑(extension classpath)
  • 用戶類路徑(user classpath)

啟動(dòng)類路徑默認(rèn)對(duì)應(yīng)jre\lib目錄,Java標(biāo)準(zhǔn)庫(大部分在rt.jar里) 位于該路徑。

擴(kuò)展類路徑默認(rèn)對(duì)應(yīng)jre\lib\ext目錄,使用Java擴(kuò)展機(jī)制的類位于這個(gè)路徑。

用戶類路徑為自己實(shí)現(xiàn)的類,以及第三方類庫的路徑??梢酝ㄟ^-Xbootclasspath選項(xiàng)修改啟動(dòng)類路徑,不過一般不需要這樣做。

用戶類路徑的默認(rèn)值是當(dāng)前目錄,也就是. ??梢栽O(shè)置 CLASSPATH環(huán)境變量來修改用戶類路徑,但是這樣做不夠靈活,所以不推薦使用。

更好的辦法是給java命令傳遞-classpath(或簡寫為-cp)選項(xiàng)。-classpath/-cp選項(xiàng)的優(yōu)先級(jí)更高,可以覆蓋CLASSPATH環(huán)境變量設(shè)置。如下:

java -cp path\to\classes ...
java -cp path\to\lib1.jar ...
java -cp path\to\lib2.zip ...

3.2解析用戶類路徑

該功能建立在命令行工具上,因此復(fù)制上次的代碼,并創(chuàng)建classpath子目錄。

Java虛擬機(jī)將使用JDK的啟動(dòng)類路徑來尋找和加載Java 標(biāo)準(zhǔn)庫中的類,因此需要某種方式指定jre目錄的位置。

命令行選項(xiàng)可以獲取,所以增加一個(gè)非標(biāo)準(zhǔn)選項(xiàng)-Xjre。

修改Cmd結(jié)構(gòu)體,添加XjreOption字段;parseCmd()函數(shù)也要相應(yīng)修改:

type Cmd struct {
	// 標(biāo)注是否為 --help
	helpFlag bool
	//標(biāo)注是否為 --version
	versionFlag bool
	//選項(xiàng)
	cpOption string
	//主類名,或者是jar文件
	class string
	//參數(shù)
	args []string
	// jre路徑
	XjreOption string
}

func parseCmd() *Cmd {
	cmd := &Cmd{}

	flag.Usage = printUsage
	flag.BoolVar(&cmd.helpFlag, "help", false, "print help message")
	flag.BoolVar(&cmd.helpFlag, "?", false, "print help message")
	flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit")
	flag.StringVar(&cmd.cpOption, "classpath", "", "classpath")
	flag.StringVar(&cmd.cpOption, "cp", "", "classpath")
	flag.StringVar(&cmd.XjreOption, "Xjre", "", "path to jre")
	flag.Parse()

	args := flag.Args()
	if len(args) > 0 {
		//第一個(gè)參數(shù)是主類名
		cmd.class = args[0]
		cmd.args = args[1:]
	}

	return cmd
}

3.3獲取用戶類路徑

可以把類路徑想象成一個(gè)大的整體,它由啟動(dòng)類路徑、擴(kuò)展類路徑和用戶類路徑三個(gè)小路徑構(gòu)成。

三個(gè)小路徑又分別由更小的路徑構(gòu)成。是不是很像組合模式(composite pattern)

接下來將使用組合模式來設(shè)計(jì)和實(shí)現(xiàn)類路徑。

1.Entry接口

定義一個(gè)Entry接口,作為所有類的基準(zhǔn)。

package classpath

import "os"

// :(linux/unix) or ;(windows)
const pathListSeparator = string(os.PathListSeparator)

type Entry interface {
    // className: fully/qualified/ClassName.class
    readClass(classpath string) ([]byte, Entry, error)
    String() string
}

常量pathListSeparator是string類型,存放路徑分隔符,后面會(huì)用到。

Entry接口中有個(gè)兩方法。

readClass()方法:負(fù)責(zé)尋找和加載class 文件。

參數(shù)是class文件的相對(duì)路徑,路徑之間用斜線/分隔,文件名有.class后綴。比如要讀取java.lang.Object類,傳 入的參數(shù)應(yīng)該是java/lang/Object.class。返回值是讀取到的字節(jié)數(shù)據(jù)、最終定位到class文件的Entry,以及錯(cuò)誤信息。

String()方法:作用相當(dāng)于Java中的toString(),用于返回變量 的字符串表示。

Go的函數(shù)或方法允許返回多個(gè)值,按照慣例,可以使用最后一個(gè)返回值作為錯(cuò)誤信息。

還需要一個(gè)類似于JAVA構(gòu)造函數(shù)的函數(shù),但在Go語言中沒有構(gòu)造函數(shù)的概念,對(duì)象的創(chuàng)建通常交由一個(gè)全局的創(chuàng)建函數(shù)來完成,以NewXXX來命令,表示"構(gòu)造函數(shù)"

newEntry()函數(shù)根據(jù)參數(shù)創(chuàng)建不同類型的Entry實(shí)例,代碼如下:

func newEntry(path string) Entry {
	////如果路徑包含分隔符 表示有多個(gè)文件 
	if strings.Contains(path, pathListSeparator) {
		return newCompositeEntry(path)
	}
	//包含*,則說明要將相應(yīng)目錄下的所有class文件加載
	if strings.HasSuffix(path, "*") {
		return newWildcardEntry(path)
	}
	//包含.jar,則說明是jar文件,通過zip方式加載
	if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") ||
		strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") {

		return newZipEntry(path)
	}

	return newDirEntry(path)
}

2.實(shí)現(xiàn)類

存在四種類路徑指定方式:

  • 普通路徑形式:gyb/gyb
  • jar/zip形式:/gyb/gyb.jar
  • 通配符形式:gyb/*
  • 多個(gè)路徑形式:gyb/1:/gyb/2

DirEntry(普通形式)

創(chuàng)建entry_dir.go,定義DirEntry結(jié)構(gòu)體:

package classpath

import "io/ioutil"
import "path/filepath"

type DirEntry struct {
	absDir string
}

func newDirEntry(path string) *DirEntry {
	//轉(zhuǎn)化為絕對(duì)路徑
	absDir, err := filepath.Abs(path)
	if err != nil {
		panic(err)
	}
	return &DirEntry{absDir}
}

func (self *DirEntry) readClass(className string) ([]byte, Entry, error) {
	//拼接類文件目錄 和 類文件名  
	// '/gyb/xxx/' + 'helloworld.class' = '/gyb/xxx/helloworld.class'
	fileName := filepath.Join(self.absDir, className)
	data, err := ioutil.ReadFile(fileName)
	return data, self, err
}

func (self *DirEntry) String() string {
	return self.absDir
}

DirEntry只有一個(gè)字段,用于存放目錄的絕對(duì)路徑。

和Java語言不同,Go結(jié)構(gòu)體不需要顯示實(shí)現(xiàn)接口,只要方法匹配即可。

ZipEntry(jar/zip形式)

package classpath

import "archive/zip"
import "errors"
import "io/ioutil"
import "path/filepath"

type ZipEntry struct {
	absPath string
}

func newZipEntry(path string) *ZipEntry {
	absPath, err := filepath.Abs(path)
	if err != nil {
		panic(err)
	}
	return &ZipEntry{absPath}
}

func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) {
	r, err := zip.OpenReader(self.absPath)
	if err != nil {
		return nil, nil, err
	}

	defer r.Close()
	for _, f := range r.File {
		if f.Name == className {
			rc, err := f.Open()
			if err != nil {
				return nil, nil, err
			}

			defer rc.Close()
			data, err := ioutil.ReadAll(rc)
			if err != nil {
				return nil, nil, err
			}

			return data, self, nil
		}
	}

	return nil, nil, errors.New("class not found: " + className)
}

func (self *ZipEntry) String() string {
	return self.absPath
}

首先打開ZIP文件,如果這一步出錯(cuò)的話,直接返回。然后遍歷 ZIP壓縮包里的文件,看能否找到class文件。如果能找到,則打開 class文件,把內(nèi)容讀取出來,并返回。如果找不到,或者出現(xiàn)其他錯(cuò) 誤,則返回錯(cuò)誤信息。有兩處使用了defer語句來確保打開的文件得 以關(guān)閉。

CompositeEntry(多路徑形式)

CompositeEntry由更小的Entry組成,正好可以表示成[]Entry。

在Go語言中,數(shù)組屬于比較低層的數(shù)據(jù)結(jié)構(gòu),很少直接使用。大部分情況下,使用更便利的slice類型。

構(gòu)造函數(shù)把參數(shù)(路徑列表)按分隔符分成小路徑,然后把每個(gè)小路徑都轉(zhuǎn)換成具體的 Entry實(shí)例。

package classpath

import "errors"
import "strings"

type CompositeEntry []Entry

func newCompositeEntry(pathList string) CompositeEntry {
	compositeEntry := []Entry{}

	for _, path := range strings.Split(pathList, pathListSeparator) {
		//去判斷 path 屬于哪其他三種哪一種情況 生成對(duì)應(yīng)的 ClassDirEntry類目錄對(duì)象
		entry := newEntry(path)
		compositeEntry = append(compositeEntry, entry)
	}

	return compositeEntry
}

func (self CompositeEntry) readClass(className string) ([]byte, Entry, error) {
	//遍歷切片 中的 類目錄對(duì)象
	for _, entry := range self {
		//如果找到了 對(duì)應(yīng)的 類 直接返回
		data, from, err := entry.readClass(className)
		if err == nil {
			return data, from, nil
		}

	}
	//沒找到 返回錯(cuò)誤
	return nil, nil, errors.New("class not found: " + className)
}

func (self CompositeEntry) String() string {
	strs := make([]string, len(self))

	for i, entry := range self {
		strs[i] = entry.String()
	}

	return strings.Join(strs, pathListSeparator)
}

WildcardEntry(通配符形式)

WildcardEntry實(shí)際上也是CompositeEntry,所以就不再定義新的類型了。

首先把路徑末尾的星號(hào)去掉,得到baseDir,然后調(diào)用filepath包的Walk()函數(shù)遍歷baseDir創(chuàng)建ZipEntry。Walk()函數(shù)的第二個(gè)參數(shù) 也是一個(gè)函數(shù)。

walkFn中,根據(jù)后綴名選出JAR文件,并且返回SkipDir跳過子目錄(通配符類路徑不能遞歸匹配子目錄下的JAR文件)。

package classpath

import "os"
import "path/filepath"
import "strings"

func newWildcardEntry(path string) CompositeEntry {
	//截取通用匹配符 /gyb/* 截取掉 *
	baseDir := path[:len(path)-1] // remove *
	//多個(gè) 類目錄對(duì)象
	compositeEntry := []Entry{}

	walkFn := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		//如果為空
		if info.IsDir() && path != baseDir {
			return filepath.SkipDir
		}
		//如果是 .jar  或者 .JAR 結(jié)尾的文件
		if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") {
			jarEntry := newZipEntry(path)
			compositeEntry = append(compositeEntry, jarEntry)
		}
		return nil
	}
	//遍歷 目錄下所有 .jar .JAR 文件 生成ZipEntry目錄對(duì)象 放在切片中返回
	//walFn為函數(shù)
	filepath.Walk(baseDir, walkFn)

	return compositeEntry
}

3.4實(shí)現(xiàn)類目錄

前面提到了java 虛擬機(jī)默認(rèn) 會(huì)先從啟動(dòng)路徑--->擴(kuò)展類路徑 --->用戶類路徑

按順序依次去尋找,加載類。

那么就會(huì)有3個(gè)類目錄對(duì)象,所以就要定義一個(gè)結(jié)構(gòu)體去存放它。

type Classpath struct {
	BootClasspath Entry
	ExtClasspath  Entry
	UserClasspath Entry
}

啟動(dòng)類路徑

啟動(dòng)路徑,其實(shí)對(duì)應(yīng)Jre目錄下``lib` 也就是運(yùn)行java 程序必須可少的基本運(yùn)行庫。

通過 -Xjre 指定 如果不指定 會(huì)在當(dāng)前路徑下尋找jre 如果找不到 就會(huì)從我們?cè)谘bjava是配置的JAVA_HOME環(huán)境變量 中去尋找。

所以獲取驗(yàn)證環(huán)境變量的方法如下:

func getJreDir(jreOption string) string {
	//如果 從cmd  -Xjre 獲取到目錄 并且存在
	if jreOption != "" && exists(jreOption) {
	//返回目錄
		return jreOption 
	}
	//如果 當(dāng)前路徑下 有 jre 返回目錄
	if exists("./jre") { 
		return "./jre"
	}
	//如果 上面都找不到 到系統(tǒng)環(huán)境 變量中尋找 
	if jh := os.Getenv("JAVA_HOME"); jh != "" {
		//存在 就返回
		return filepath.Join(jh, "jre") 
	}
	//都找不到 就報(bào)錯(cuò)
	panic("Can not find jre folder!") 
}
//判斷 目錄是否存在
func exists(path string) bool {
	if _, err := os.Stat(path); err != nil {
	if os.IsNotExist(err) { return false
} }
return true }

擴(kuò)展類路徑

擴(kuò)展類 路徑一般 在啟動(dòng)路徑 的子目錄下jre/lib/ext

func (self *Classpath) parseBootAndExtClasspath(jreOption string) {
	jreDir := getJreDir(jreOption)

	// 拼接成jre 的路徑
	// jre/lib/*
	jreLibPath := filepath.Join(jreDir, "lib", "*")
	//加載 所有底下的 jar包
	self.BootClasspath = newWildcardEntry(jreLibPath)

	// 拼接 擴(kuò)展類 的路徑
	// jre/lib/ext/*
	jreExtPath := filepath.Join(jreDir, "lib", "ext", "*")
	//加載 所有底下的jar包
	self.ExtClasspath = newWildcardEntry(jreExtPath)
}

用戶類路徑

用戶類路徑通過前面提到的 -classpath 來指定 ,如果沒有指定 就默認(rèn)為當(dāng)前路徑就好

func (self *Classpath) parseUserClasspath(cpOption string) {
	//如果沒有指定
	if cpOption == "" {
	// . 作為當(dāng)前路徑
	cpOption = "." 
	}
	//創(chuàng)建 類目錄對(duì)象
	self.UserClasspath = newEntry(cpOption)
}

實(shí)現(xiàn)類的加載

對(duì)于指定文件類名取查找 我們是按前面提到的(啟動(dòng)路徑--->擴(kuò)展類路徑 --->用戶類路徑
按順序依次去尋找,加載類),沒找到就挨個(gè)查找下去。

如果用戶沒有提供-classpath/-cp選項(xiàng),則使用當(dāng)前目錄作為用 戶類路徑。ReadClass()方法依次從啟動(dòng)類路徑、擴(kuò)展類路徑和用戶 類路徑中搜索class文件,

//根據(jù)類名 分別從 bootClasspath,extClasspath,userClasspath 依次加載類目錄
func (self *Classpath) ReadClass(className string) ([]byte, ClassDirEntry, error) {
	className = className + ".class"
	if data, entry, err := self.BootClasspath.readClass(className); err == nil{ return data, entry, err }
	if data, entry, err := self.ExtClasspath.readClass(className); err == nil { return data, entry, err }
	return self.UserClasspath.readClass(className)
}

初始化類加載目錄

定義一個(gè)初始化函數(shù),來作為初始函數(shù),執(zhí)行后生成一個(gè) Classpath對(duì)象。

//jreOption 為啟動(dòng)類目錄  cpOption 為 用戶指定類目錄 從cmd 命令行 中解析獲取
func InitClassPath(jreOption, cpOption string) *Classpath {
	cp := &Classpath{}
	//初始化 啟動(dòng)類目錄
	cp.parseBootAndExtClasspath(jreOption)
	//初始化 用戶類目錄
	cp.parseUserClasspath(cpOption)
	return cp
}

注意,傳遞給ReadClass()方法的類名不包含“.class”后綴。

3.5總結(jié)

3.6測試

成功獲取到class文件!

四、解析Class文件

4.1 class文件介紹

詳細(xì)class文件分析

作為類/接口信息的載體,每一個(gè)class文件都完整的定義了一個(gè)類,為了使Java程序可以實(shí)現(xiàn)“編寫一次,處處運(yùn)行”,java虛擬機(jī)對(duì)class文件的格式進(jìn)行了嚴(yán)格的規(guī)范。

但是對(duì)于從哪里加載class文件,給予了高度自由空間:第三節(jié)中說過,可以從文件系統(tǒng)讀取jar/zip文件中的class文件,除此之外,也可以從網(wǎng)絡(luò)下載,甚至是直接在運(yùn)行中生成class文件。

構(gòu)成class文件的基本數(shù)據(jù)單位是字節(jié),可以把整個(gè)class文件當(dāng) 成一個(gè)字節(jié)流來處理。稍大一些的數(shù)據(jù)由連續(xù)多個(gè)字節(jié)構(gòu)成,這些數(shù)據(jù)在class文件中以大端(big-endian)方式存儲(chǔ)。

為了描述class文件格式,Java虛擬機(jī)規(guī)范定義了u1、u2u4三種數(shù)據(jù)類型來表示1、 2和4字節(jié)無符號(hào)整數(shù),分別對(duì)應(yīng)Go語言的uint8、uint16uint32類型。

相同類型的多條數(shù)據(jù)一般按表(table)的形式存儲(chǔ)在class文件中。表由表頭表項(xiàng)(item)構(gòu)成,表頭是u2或u4整數(shù)。假設(shè)表頭是 n,后面就緊跟著n個(gè)表項(xiàng)數(shù)據(jù)。

Java虛擬機(jī)規(guī)范使用一種類似C語言的結(jié)構(gòu)體語法來描述class 文件格式。整個(gè)class文件被描述為一個(gè)ClassFile結(jié)構(gòu),代碼如下:

ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

!表示大小不定。

4.2解析class文件

Go語言內(nèi)置了豐富的數(shù)據(jù)類型,非常適合處理class文件。

如下為Go和Java語言基本數(shù)據(jù)類型對(duì)照關(guān)系:

Go語言類型Java語言類型說明
int8byte8比特有符號(hào)整數(shù)
uint8(別名byte)N/A8比特?zé)o符號(hào)整數(shù)
int16short16比特有符號(hào)整數(shù)
uint16char16比特?zé)o符號(hào)整數(shù)
int32(別名rune)int32比特有符號(hào)整數(shù)
uint32N/A32比特?zé)o符號(hào)整數(shù)
int64long64比特有符號(hào)整數(shù)
uint64N/A64比特?zé)o符號(hào)整數(shù)
float32float32比特IEEE-754浮點(diǎn)數(shù)
float64double64比特IEEE-754浮點(diǎn)數(shù)

4.2.1讀取數(shù)據(jù)

解析class文件的第一步是從里面讀取數(shù)據(jù)。雖然可以把class文件當(dāng)成字節(jié)流來處理,但是直接操作字節(jié)很不方便,所以先定義一個(gè)結(jié)構(gòu)體ClassReader來幫助讀取數(shù)據(jù),創(chuàng)建class_reader.go。

package classfile
import "encoding/binary"
type ClassReader struct {
	data []byte
}
func (self *ClassReader) readUint8() uint8 {...} // u1
func (self *ClassReader) readUint16() uint16 {...} // u2
func (self *ClassReader) readUint32() uint32 {...} // u4
func (self *ClassReader) readUint64() uint64 {...}
func (self *ClassReader) readUint16s() []uint16 {...}
func (self *ClassReader) readBytes(length uint32) []byte {...}

ClassReader只是[]byte類型的包裝而已。readUint8()讀取u1類型數(shù)據(jù)。

ClassReader并沒有使用索引記錄數(shù)據(jù)位置,而是使用Go 語言的reslice語法跳過已經(jīng)讀取的數(shù)據(jù)

實(shí)現(xiàn)代碼如下:

// u1
func (self *ClassReader) readUint8() uint8 {
    val := self.data[0]
    self.data = self.data[1:]
    return val
}

// u2
func (self *ClassReader) readUint16() uint16 {
    val := binary.BigEndian.Uint16(self.data)
    self.data = self.data[2:]
    return val
}

// u4
func (self *ClassReader) readUint32() uint32 {
    val := binary.BigEndian.Uint32(self.data)
    self.data = self.data[4:]
    return val
}

func (self *ClassReader) readUint64() uint64 {
    val := binary.BigEndian.Uint64(self.data)
    self.data = self.data[8:]
    return val
}

func (self *ClassReader) readUint16s() []uint16 {
    n := self.readUint16()
    s := make([]uint16, n)
    for i := range s {
       s[i] = self.readUint16()
    }
    return s
}

func (self *ClassReader) readBytes(n uint32) []byte {
    bytes := self.data[:n]
    self.data = self.data[n:]
    return bytes
}

Go標(biāo)準(zhǔn)庫encoding/binary包中定義了一個(gè)變量BigEndian,可以從[]byte中解碼多字節(jié)數(shù)據(jù)。

4.2.2解析整體結(jié)構(gòu)

有了ClassReader,可以開始解析class文件了。創(chuàng)建class_file.go文件,在其中定義ClassFile結(jié)構(gòu)體,與4.1中的class文件中字段對(duì)應(yīng)。

package classfile
import "fmt"

type ClassFile struct {
    //magic uint32
    minorVersion uint16
    majorVersion uint16
    constantPool ConstantPool
    accessFlags uint16
    thisClass uint16
    superClass uint16
    interfaces []uint16
    fields []*MemberInfo
    methods []*MemberInfo
    attributes []AttributeInfo
}

在class_file.go文件中實(shí)現(xiàn)一系列函數(shù)和方法。

func Parse(classData []byte) (cf *ClassFile, err error) {...}
func (self *ClassFile) read(reader *ClassReader) {...}
func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {...}
func (self *ClassFile) readAndCheckVersion(reader *ClassReader) {...}
func (self *ClassFile) MinorVersion() uint16 {...} // getter
func (self *ClassFile) MajorVersion() uint16 {...} // getter
func (self *ClassFile) ConstantPool() ConstantPool {...} // getter
func (self *ClassFile) AccessFlags() uint16 {...} // getter
func (self *ClassFile) Fields() []*MemberInfo {...} // getter
func (self *ClassFile) Methods() []*MemberInfo {...} // getter
func (self *ClassFile) ClassName() string {...}
func (self *ClassFile) SuperClassName() string {...}
func (self *ClassFile) InterfaceNames() []string {...}

相比Java語言,Go的訪問控制非常簡單:只有公開和私有兩種。

所有首字母大寫的類型、結(jié)構(gòu)體、字段、變量、函數(shù)、方法等都是公開的,可供其他包使用。
首字母小寫則是私有的,只能在包內(nèi)部使用。

解析[]byte

Parse()函數(shù)把[]byte解析成ClassFile結(jié)構(gòu)體。

func Parse(classData []byte) (cf *ClassFile, err error) {
    defer func() {
       //嘗試捕獲 panic,并將其存儲(chǔ)在變量 r 中。如果沒有發(fā)生 panic,r 將為 nil。
       if r := recover(); r != nil {
          var ok bool
          //判斷 r 是否是一個(gè) error 類型
          err, ok = r.(error)
          if !ok {
             err = fmt.Errorf("%v", r)
          }
       }
    }()

    cr := &ClassReader{classData}
    cf = &ClassFile{}
    cf.read(cr)
    return
}

順序解析

read() 方法依次調(diào)用其他方法解析class文件,順序一定要保證正確,與class文件相對(duì)應(yīng)。

func (self *ClassFile) read(reader *ClassReader) {
  	//讀取并檢查類文件的魔數(shù)。 
    self.readAndCheckMagic(reader)
	//讀取并檢查類文件的版本號(hào)。
    self.readAndCheckVersion(reader)
    //解析常量池,常量池類還沒寫
    self.constantPool = readConstantPool(reader)
    //讀取類的訪問標(biāo)志
    self.accessFlags = reader.readUint16()
    //讀取指向當(dāng)前類在常量池中的索引
    self.thisClass = reader.readUint16()
    //父類在常量池中的索引
    self.superClass = reader.readUint16()
    //讀取接口表的數(shù)據(jù)
    self.interfaces = reader.readUint16s()
    //讀取類的字段信息
    self.fields = readMembers(reader, self.constantPool)
    //讀取類的方法信息
    self.methods = readMembers(reader, self.constantPool)
    //讀取類的屬性信息(類級(jí)別的注解、源碼文件等)
    self.attributes = readAttributes(reader, self.constantPool)
}
  • self.readAndCheckMagic(reader): 這是一個(gè) ClassFile 結(jié)構(gòu)的方法,用于讀取并檢查類文件的魔數(shù)。魔數(shù)是類文件的標(biāo)識(shí)符,用于確定文件是否為有效的類文件。
  • self.readAndCheckVersion(reader): 這個(gè)方法用于讀取并檢查類文件的版本號(hào)。Java類文件具有版本號(hào),標(biāo)識(shí)了它們的Java編譯器版本。這里會(huì)對(duì)版本號(hào)進(jìn)行檢查。
  • self.constantPool = readConstantPool(reader): 這一行代碼調(diào)用 readConstantPool 函數(shù)來讀取常量池部分的數(shù)據(jù),并將其存儲(chǔ)在 ClassFile 結(jié)構(gòu)的 constantPool 字段中。常量池是一個(gè)包含各種常量信息的表格,用于支持類文件中的各種符號(hào)引用。
  • self.accessFlags = reader.readUint16(): 這一行代碼讀取類的訪問標(biāo)志,它標(biāo)識(shí)類的訪問權(quán)限,例如 public、private 等。
  • self.thisClass = reader.readUint16(): 這行代碼讀取指向當(dāng)前類在常量池中的索引,表示當(dāng)前類的類名。
  • self.superClass = reader.readUint16(): 這行代碼讀取指向父類在常量池中的索引,表示當(dāng)前類的父類名。
  • self.interfaces = reader.readUint16s(): 這行代碼讀取接口表的數(shù)據(jù),表示當(dāng)前類實(shí)現(xiàn)的接口。
  • self.fields = readMembers(reader, self.constantPool): 這行代碼調(diào)用 readMembers 函數(shù),以讀取類的字段信息,并將它們存儲(chǔ)在 fields 字段中。字段包括類的成員變量。
  • self.methods = readMembers(reader, self.constantPool): 這行代碼類似于上一行,但它讀取類的方法信息,并將它們存儲(chǔ)在 methods 字段中。
  • self.attributes = readAttributes(reader, self.constantPool): 最后,這行代碼調(diào)用 readAttributes 函數(shù),以讀取類的屬性信息,并將它們存儲(chǔ)在 attributes 字段中。屬性包括類級(jí)別的注解、源碼文件等信息。

以下均為類似于Java的getter方法,以后將不再贅述。

func (self *ClassFile) MinorVersion() uint16 {
	return self.minorVersion
}
func (self *ClassFile) MajorVersion() uint16 {
	return self.majorVersion
}
func (self *ClassFile) ConstantPool() ConstantPool {
	return self.constantPool
}
func (self *ClassFile) AccessFlags() uint16 {
	return self.accessFlags
}
func (self *ClassFile) Fields() []*MemberInfo {
	return self.fields
}
func (self *ClassFile) Methods() []*MemberInfo {
	return self.methods
}

ClassName從常量池中獲取,SuperClassName同理,常量池還未實(shí)現(xiàn)。

所有類的超類(父類),Object是java中唯一沒有父類的類,一個(gè)類可以不是Object的直接子類,但一定是繼承于Object并拓展于Object。

func (self *ClassFile) ClassName() string {
    return self.constantPool.getClassName(self.thisClass)
}
func (self *ClassFile) SuperClassName() string {
	if self.superClass > 0 {
		return self.constantPool.getClassName(self.superClass)
	}
    //Object類
	return ""
}

Java的類是單繼承,多實(shí)現(xiàn)的,因此獲取接口應(yīng)該使用循環(huán),也從常量池中獲取。

func (self *ClassFile) InterfaceNames() []string {
    interfaceNames := make([]string, len(self.interfaces))
    for i, cpIndex := range self.interfaces {
       interfaceNames[i] = self.constantPool.getClassName(cpIndex)
    }
    return interfaceNames
}

解析魔數(shù)

很多文件格式都會(huì)規(guī)定滿足該格式的文件必須以某幾個(gè)固定字節(jié)開頭,這幾個(gè)字節(jié)主要起標(biāo)識(shí)作用,叫作魔數(shù)(magic number)。

  • PDF文件以4字節(jié)“%PDF”(0x25、0x50、0x44、0x46)開頭
  • ZIP 文件以2字節(jié)“PK”(0x50、0x4B)開頭
  • class文件的魔數(shù) 是“0xCAFEBABE” 。

因此readAndCheckMagic()方法的代碼如下。

func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {
    magic := reader.readUint32()
    if magic != 0xCAFEBABE {
       panic("java.lang.ClassFormatError: magic!")
    }
}

Java虛擬機(jī)規(guī)范規(guī)定,如果加載的class文件不符合要求的格式,Java虛擬機(jī)實(shí)現(xiàn)就拋出java.lang.ClassFormatError異常。

但是因?yàn)槲覀儾艅倓傞_始編寫虛擬機(jī),還無法拋出異常,所以暫時(shí)先調(diào)用 panic()方法終止程序執(zhí)行。

版本號(hào)

解析版本號(hào)

魔數(shù)之后是class文件的次版本號(hào)和主版本號(hào),都是u2類型。

假設(shè)某class文件的主版本號(hào)是M,次版本號(hào)是m,那么完整的版本號(hào) 可以表示成M.m的形式。

次版本號(hào)只在J2SE 1.2之前用過,從1.2 開始基本上就沒什么用了(都是0)。

主版本號(hào)在J2SE 1.2之前是45, 從1.2開始,每次有大的Java版本發(fā)布,都會(huì)加1。

Java 版本類文件版本號(hào)
Java 1.145.3
Java 1.246.0
Java 1.347.0
Java 1.448.0
Java 549.0
Java 650.0
Java 751.0
Java 852.0

特定的Java虛擬機(jī)實(shí)現(xiàn)只能支持版本號(hào)在某個(gè)范圍內(nèi)的class文 件。

Oracle的實(shí)現(xiàn)是完全向后兼容的,比如Java SE 8支持版本號(hào)為 45.0~52.0的class文件。

如果版本號(hào)不在支持的范圍內(nèi),Java虛擬機(jī) 實(shí)現(xiàn)就拋出java.lang.UnsupportedClassVersionError異常。參考 Java 8,支持版本號(hào)為45.0~52.0的class文件。如果遇到其他版本號(hào), 調(diào)用panic()方法終止程序執(zhí)行。

如下為檢查版本號(hào)代碼:

func (self *ClassFile) readAndCheckVersion(reader *ClassReader) {
    self.minorVersion = reader.readUint16()
    self.majorVersion = reader.readUint16()
    switch self.majorVersion {
    case 45:
       return
    case 46, 47, 48, 49, 50, 51, 52:
       if self.minorVersion == 0 {
          return
       }
    }
    panic("java.lang.UnsupportedClassVersionError!")
}

解析類訪問標(biāo)識(shí)

版本號(hào)之后是常量池,但是由于常量池比較復(fù)雜,所以放到4.3 節(jié)介紹。

常量池之后是類訪問標(biāo)志,這是一個(gè)16位的bitmask,指出class文件定義的是類還是接口,訪問級(jí)別是public還是private,等等。

本章只對(duì)class文件進(jìn)行初步解析,并不做完整驗(yàn)證,所以只是讀取類訪問標(biāo)志以備后用。

ClassFileTest的類訪問標(biāo)志為:0X21:

解析類和父類索引

類訪問標(biāo)志之后是兩個(gè)u2類型的常量池索引,分別給出類名和超類名。

class文件存儲(chǔ)的類名類似完全限定名,但是把點(diǎn)換成了 斜線,Java語言規(guī)范把這種名字叫作二進(jìn)制名binary names。

因?yàn)槊總€(gè)類都有名字,所以thisClass必須是有效的常量池索引。
除 java.lang.Object之外,其他類都有超類,所以superClass只在 Object.class中是0,在其他class文件中必須是有效的常量池索引。如下,ClassFileTest的類索引是5,超類索引是6。

解析接口索引表

類和超類索引后面是接口索引表,表中存放的也是常量池索引,給出該類實(shí)現(xiàn)的所有接口的名字。ClassFileTest沒有實(shí)現(xiàn)接口, 所以接口表是空的

解析字段和方法表

接口索引表之后是字段表和方法表,分別存儲(chǔ)字段和方法信息。

字段和方法的基本結(jié)構(gòu)大致相同,差別僅在于屬性表。

下面是 Java虛擬機(jī)規(guī)范給出的字段結(jié)構(gòu)定義

field_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

和類一樣,字段和方法也有自己的訪問標(biāo)志。訪問標(biāo)志之后是一個(gè)常量池索引,給出字段名或方法名,然后又是一個(gè)常量池索引,給出字段或方法的描述符,最后是屬性表。

為了避免重復(fù)代 碼,用一個(gè)結(jié)構(gòu)體統(tǒng)一表示字段和方法。

package classfile
type MemberInfo struct {
    cp ConstantPool
    accessFlags uint16
    nameIndex uint16
    descriptorIndex uint16
    attributes []AttributeInfo
}
func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo {...}
func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo {...}
func (self *MemberInfo) AccessFlags() uint16 {...} // getter
func (self *MemberInfo) Name() string {...}
func (self *MemberInfo) Descriptor() string {...}

cp字段保存常量池指針,后面會(huì)用到它。readMembers()讀取字段表或方法表,代碼如下:

func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo {
    memberCount := reader.readUint16()
    members := make([]*MemberInfo, memberCount)
    for i := range members {
       members[i] = readMember(reader, cp)
    }
    return members
}

readMember()函數(shù)讀取字段或方法數(shù)據(jù)。

func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo {
    return &MemberInfo{
       cp:              cp,
       accessFlags:     reader.readUint16(),
       nameIndex:       reader.readUint16(),
       descriptorIndex: reader.readUint16(),
       attributes:      readAttributes(reader, cp),
    }
}

Name()從常量 池查找字段或方法名,Descriptor()從常量池查找字段或方法描述 符

func (self *MemberInfo) Name() string {
    return self.cp.getUtf8(self.nameIndex)
}
func (self *MemberInfo) Descriptor() string {
    return self.cp.getUtf8(self.descriptorIndex)
}

4.2.3解析常量池

常量池占據(jù)了class文件很大一部分?jǐn)?shù)據(jù),里面存放著各式各樣的常量信息,包括數(shù)字和字符串常量、類和接口名、字段和方法名,等等

創(chuàng)建constant_pool.go文件,里面定義 ConstantPool類型。

package classfile

type ConstantPool []ConstantInfo
func readConstantPool(reader *ClassReader) ConstantPool {...}
func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {...}
func (self ConstantPool) getNameAndType(index uint16) (string, string) {...}
func (self ConstantPool) getClassName(index uint16) string {...}
func (self ConstantPool) getUtf8(index uint16) string {...}

常量池實(shí)際上也是一個(gè)表,但是有三點(diǎn)需要特別注意。

表頭給出的常量池大小比實(shí)際大1。假設(shè)表頭給出的值是n,那么常量池的實(shí)際大小是n–1。

有效的常量池索引是1~n–1。0是無效索引,表示不指向任何常量。

CONSTANT_Long_infoCONSTANT_Double_info各占兩個(gè)位置。也就是說,如果常量池中存在這兩種常量,實(shí)際的常量數(shù)量比n–1還要少,而且1~n–1的某些 數(shù)也會(huì)變成無效索引。

常量池由readConstantPool()函數(shù)讀取,代碼如下:

func readConstantPool(reader *ClassReader) ConstantPool {
    cpCount := int(reader.readUint16())
    cp := make([]ConstantInfo, cpCount)

    // 索引從1開始
    for i := 1; i < cpCount; i++ {
       cp[i] = readConstantInfo(reader, cp)
       switch cp[i].(type) {
       //占兩個(gè)位置
       case *ConstantLongInfo, *ConstantDoubleInfo:
          i++
       }
    }

    return cp
}

getConstantInfo()方法按索引查找常量

func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {
    if cpInfo := self[index]; cpInfo != nil {
       return cpInfo
    }
    panic(fmt.Errorf("Invalid constant pool index: %v!", index))
}

getNameAndType()方法從常量池查找字段或方法的名字和描述符

func (self ConstantPool) getNameAndType(index uint16) (string, string) {
    ntInfo := self.getConstantInfo(index).(*ConstantNameAndTypeInfo)
    name := self.getUtf8(ntInfo.nameIndex)
    _type := self.getUtf8(ntInfo.descriptorIndex)
    return name, _type
}

getClassName()方法從常量池查找類名

func (self ConstantPool) getClassName(index uint16) string {
    classInfo := self.getConstantInfo(index).(*ConstantClassInfo)
    return self.getUtf8(classInfo.nameIndex)
}

getUtf8()方法從常量池查找UTF-8字符串

func (self ConstantPool) getUtf8(index uint16) string {
    utf8Info := self.getConstantInfo(index).(*ConstantUtf8Info)
    return utf8Info.str
}

ConstPool接口

由于常量池中存放的信息各不相同,所以每種常量的格式也不同。

常量數(shù)據(jù)的第一字節(jié)是tag,用來區(qū)分常量類型。

下面是Java 虛擬機(jī)規(guī)范給出的常量結(jié)構(gòu)

cp_info {
    u1 tag;
    u1 info[];
}

Java虛擬機(jī)規(guī)范一共定義了14種常量。創(chuàng)建constant_info.go文件,在其中定義tag常量值,代碼如下:

package classfile

// Constant pool tags
const (
    CONSTANT_Class              = 7
    CONSTANT_Fieldref           = 9
    CONSTANT_Methodref          = 10
    CONSTANT_InterfaceMethodref = 11
    CONSTANT_String             = 8
    CONSTANT_Integer            = 3
    CONSTANT_Float              = 4
    CONSTANT_Long               = 5
    CONSTANT_Double             = 6
    CONSTANT_NameAndType        = 12
    CONSTANT_Utf8               = 1
    CONSTANT_MethodHandle       = 15
    CONSTANT_MethodType         = 16
    CONSTANT_InvokeDynamic      = 18
)

定義ConstantInfo接口來表示常量信息

type ConstantInfo interface {
	readInfo(reader *ClassReader)
}
//讀取常量信息
func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {...}
func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {...}

readInfo()方法讀取常量信息,需要由具體的常量結(jié)構(gòu)體實(shí)現(xiàn)。 readConstantInfo()函數(shù)先讀出tag值,然后調(diào)用newConstantInfo()函數(shù)創(chuàng)建具體的常量,最后調(diào)用常量的readInfo()方法讀取常量信息, 代碼如下:

func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {
    tag := reader.readUint8()
    c := newConstantInfo(tag, cp)
    c.readInfo(reader)
    return c
}

newConstantInfo()根據(jù)tag值創(chuàng)建具體的常量,代碼如下:

func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {
    switch tag {
    case CONSTANT_Integer:
       return &ConstantIntegerInfo{}
    case CONSTANT_Float:
       return &ConstantFloatInfo{}
    case CONSTANT_Long:
       return &ConstantLongInfo{}
    case CONSTANT_Double:
       return &ConstantDoubleInfo{}
    case CONSTANT_Utf8:
       return &ConstantUtf8Info{}
    case CONSTANT_String:
       return &ConstantStringInfo{cp: cp}
    case CONSTANT_Class:
       return &ConstantClassInfo{cp: cp}
    case CONSTANT_Fieldref:
       return &ConstantFieldrefInfo{ConstantMemberrefInfo{cp: cp}}
    case CONSTANT_Methodref:
       return &ConstantMethodrefInfo{ConstantMemberrefInfo{cp: cp}}
    case CONSTANT_InterfaceMethodref:
       return &ConstantInterfaceMethodrefInfo{ConstantMemberrefInfo{cp: cp}}
    case CONSTANT_NameAndType:
       return &ConstantNameAndTypeInfo{}
    case CONSTANT_MethodType:
       return &ConstantMethodTypeInfo{}
    case CONSTANT_MethodHandle:
       return &ConstantMethodHandleInfo{}
    case CONSTANT_InvokeDynamic:
       return &ConstantInvokeDynamicInfo{}
    default:
       panic("java.lang.ClassFormatError: constant pool tag!")
    }
}

CONSTANT_Integer_info

CONSTANT_Integer_info使用4字節(jié)存儲(chǔ)整數(shù)常量,其JVM結(jié)構(gòu)定義如下:

CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;
}

CONSTANT_Integer_info和后面將要介紹的其他三種數(shù)字常量無論是結(jié)構(gòu),還是實(shí)現(xiàn),都非常相似,所以把它們定義在同一個(gè)文件中。創(chuàng)建cp_numeric.go文件,在其中定義 ConstantIntegerInfo結(jié)構(gòu)體,代碼如下:

package classfile
import "math"
type ConstantIntegerInfo struct {
	val int32
}
func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {...}

readInfo()先讀取一個(gè)uint32數(shù)據(jù),然后把它轉(zhuǎn)型成int32類型, 代碼如下

func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {
    bytes := reader.readUint32()
    self.val = int32(bytes)
}

CONSTANT_Float_info

CONSTANT_Float_info使用4字節(jié)存儲(chǔ)IEEE754單精度浮點(diǎn)數(shù)常量,JVM結(jié)構(gòu)如下:

CONSTANT_Float_info {
    u1 tag;
    u4 bytes;
}

cp_numeric.go文件中定義ConstantFloatInfo結(jié)構(gòu)體,代碼如下:

type ConstantFloatInfo struct {
	val float32
}

func (self *ConstantFloatInfo) readInfo(reader *ClassReader) {
	bytes := reader.readUint32()
	self.val = math.Float32frombits(bytes)
}

CONSTANT_Long_info

CONSTANT_Long_info使用8字節(jié)存儲(chǔ)整數(shù)常量,結(jié)構(gòu)如下:

CONSTANT_Long_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

cp_numeric.go文件中定義ConstantLongInfo結(jié)構(gòu)體,代碼如下:

type ConstantLongInfo struct {
    val int64
}

func (self *ConstantLongInfo) readInfo(reader *ClassReader) {
    bytes := reader.readUint64()
    self.val = int64(bytes)
}

CONSTANT_Double_info

最后一個(gè)數(shù)字常量是CONSTANT_Double_info,使用8字節(jié)存儲(chǔ)IEEE754雙精度浮點(diǎn)數(shù),結(jié)構(gòu)如下:

CONSTANT_Double_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

cp_numeric.go文件中定義ConstantDoubleInfo結(jié)構(gòu)體,代碼如下:

type ConstantDoubleInfo struct {
    val float64
}

func (self *ConstantDoubleInfo) readInfo(reader *ClassReader) {
    bytes := reader.readUint64()
    self.val = math.Float64frombits(bytes)
}

CONSTANT_Utf8_info

CONSTANT_Utf8_info常量里放的是MUTF-8編碼的字符串, 結(jié)構(gòu)如下:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

Java類文件中使用MUTF-8(Modified UTF-8)編碼而不是標(biāo)準(zhǔn)的UTF-8,是因?yàn)镸UTF-8在某些方面更適合于在Java虛擬機(jī)內(nèi)部處理字符串。以下是一些原因:

空字符的表示: 在標(biāo)準(zhǔn)的UTF-8編碼中,空字符(U+0000)會(huì)使用單個(gè)字節(jié)0x00表示,這與C字符串中的字符串終止符相同,可能引起混淆。在MUTF-8中,空字符會(huì)使用0xC0 0x80來表示,避免了混淆。

編碼長度: MUTF-8編碼中的每個(gè)字符都使用1至3個(gè)字節(jié)來表示,這與UTF-8編碼相比更緊湊。對(duì)于大多數(shù)常見的字符集,這可以減少存儲(chǔ)和傳輸開銷。

字符的編碼范圍: MUTF-8編碼對(duì)字符的范圍進(jìn)行了限制,只包含Unicode BMP(基本多文種平面)范圍內(nèi)的字符。這些字符通常足夠用于表示Java標(biāo)識(shí)符和字符串文字。

兼容性: 早期版本的Java使用的是MUTF-8編碼,因此為了保持與早期版本的兼容性,后續(xù)版本也繼續(xù)使用MUTF-8。這有助于確保Java類文件的可互操作性。

創(chuàng)建cp_utf8.go文件,在其中定義 ConstantUtf8Info結(jié)構(gòu)體,代碼如下:

type ConstantUtf8Info struct {
    str string
}

func (self *ConstantUtf8Info) readInfo(reader *ClassReader) {
    length := uint32(reader.readUint16())
    bytes := reader.readBytes(length)
    self.str = decodeMUTF8(bytes)
}

Java序列化機(jī)制也使用了MUTF-8編碼。

java.io.DataInput和 java.io.DataOutput接口分別定義了readUTF()writeUTF()方法,可以讀寫MUTF-8編碼的字符串。

如下為簡化版的java.io.DataInputStream.readUTF()

// mutf8 -> utf16 -> utf32 -> string
func decodeMUTF8(bytearr []byte) string {
    utflen := len(bytearr)
    chararr := make([]uint16, utflen)

    var c, char2, char3 uint16
    count := 0
    chararr_count := 0

    for count < utflen {
       c = uint16(bytearr[count])
       if c > 127 {
          break
       }
       count++
       chararr[chararr_count] = c
       chararr_count++
    }

    for count < utflen {
       c = uint16(bytearr[count])
       switch c >> 4 {
       case 0, 1, 2, 3, 4, 5, 6, 7:
          /* 0xxxxxxx*/
          count++
          chararr[chararr_count] = c
          chararr_count++
       case 12, 13:
          /* 110x xxxx   10xx xxxx*/
          count += 2
          if count > utflen {
             panic("malformed input: partial character at end")
          }
          char2 = uint16(bytearr[count-1])
          if char2&0xC0 != 0x80 {
             panic(fmt.Errorf("malformed input around byte %v", count))
          }
          chararr[chararr_count] = c&0x1F<<6 | char2&0x3F
          chararr_count++
       case 14:
          /* 1110 xxxx  10xx xxxx  10xx xxxx*/
          count += 3
          if count > utflen {
             panic("malformed input: partial character at end")
          }
          char2 = uint16(bytearr[count-2])
          char3 = uint16(bytearr[count-1])
          if char2&0xC0 != 0x80 || char3&0xC0 != 0x80 {
             panic(fmt.Errorf("malformed input around byte %v", (count - 1)))
          }
          chararr[chararr_count] = c&0x0F<<12 | char2&0x3F<<6 | char3&0x3F<<0
          chararr_count++
       default:
          /* 10xx xxxx,  1111 xxxx */
          panic(fmt.Errorf("malformed input around byte %v", count))
       }
    }
    // The number of chars produced may be less than utflen
    chararr = chararr[0:chararr_count]
    runes := utf16.Decode(chararr)
    return string(runes)
}
  • 初始化 chararr 數(shù)組,用于存儲(chǔ)UTF-16字符。
  • 遍歷MUTF-8字節(jié)數(shù)組中的字節(jié),根據(jù)字節(jié)的值來判斷字符的編碼方式。
  • 如果字節(jié)值小于128,表示ASCII字符,直接轉(zhuǎn)換為UTF-16并存儲(chǔ)。
  • 如果字節(jié)值在特定范圍內(nèi),表示多字節(jié)字符,需要根據(jù)UTF-8編碼規(guī)則進(jìn)行解碼。
  • 如果遇到不符合規(guī)則的字節(jié),拋出異常來處理錯(cuò)誤情況。
  • 最后,將解碼后的UTF-16字符轉(zhuǎn)換為Go字符串。

CONSTANT_String_info

CONSTANT_String_info常量表示java.lang.String字面量,結(jié)構(gòu)如下:

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

可以看到,CONSTANT_String_info本身并不存放字符串?dāng)?shù)據(jù)

只存了常量池索引,這個(gè)索引指向一個(gè)CONSTANT_Utf8_info常量。

下創(chuàng)建cp_string.go文件,在其中定義 ConstantStringInfo結(jié)構(gòu)體

type ConstantStringInfo struct {
    cp ConstantPool
    stringIndex uint16
}

func (self *ConstantStringInfo) readInfo(reader *ClassReader) {
    self.stringIndex = reader.readUint16()
}

String()方法按索引從常量池中查找字符串:

func (self *ConstantStringInfo) String() string {
    return self.cp.getUtf8(self.stringIndex)
}

CONSTANT_Class_info

CONSTANT_Class_info常量表示類或者接口的符號(hào)引用

他是對(duì)類或者接口的符號(hào)引用。它描述的可以是當(dāng)前類型的信息,也可以描述對(duì)當(dāng)前類的引用,還可以描述對(duì)其他類的引用。JVM結(jié)構(gòu)如下:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

CONSTANT_String_info類似,name_index是常量池索引,指向CONSTANT_Utf8_info常量。
創(chuàng)建 cp_class.go文件,定義ConstantClassInfo結(jié)構(gòu)體

type ConstantClassInfo struct {
    cp        ConstantPool
    nameIndex uint16
}

func (self *ConstantClassInfo) readInfo(reader *ClassReader) {
    self.nameIndex = reader.readUint16()
}
func (self *ConstantClassInfo) Name() string {
    return self.cp.getUtf8(self.nameIndex)
}

CONSTANT_NameAndType_info

CONSTANT_NameAndType_info給出字段或方法的名稱和描述符。

CONSTANT_Class_infoCONSTANT_NameAndType_info加在 一起可以唯一確定一個(gè)字段或者方法。其結(jié)構(gòu)如下:

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

字段或方法名由name_index給出,字段或方法的描述符由descriptor_index給出。

name_indexdescriptor_index都是常量池索引,指向CONSTANT_Utf8_info常量。

Java虛擬機(jī)規(guī)范定義了一種簡單的語法來描述字段和方法,可以根據(jù)下面的規(guī)則生成描述符。

一、類型描述符

  • 基本類型byte、short、char、int、long、float和double的描述符是單個(gè)字母,分別對(duì)應(yīng)B、S、C、I、J、F和D。注意,long的描述符是J 而不是L。
  • 引用類型的描述符是L+類的完全限定名+分號(hào)。
  • 數(shù)組類型的描述符是[+數(shù)組元素類型描述符

二、字段描述符

? 字段類型的描述符

三、方法描述符

? 分號(hào)分隔的參數(shù)類型描述符+返回值類型描述符,其中void返回值由單個(gè)字母V表示。

Java語言支持方法重載(override),不同的方法可 以有相同的名字,只要參數(shù)列表不同即可。

這就是為什么 CONSTANT_NameAndType_info結(jié)構(gòu)要同時(shí)包含名稱和描述符的原因。

創(chuàng)建cp_name_and_type.go文件,在其中定義ConstantName-AndTypeInfo結(jié)構(gòu)體,代碼如下:

type ConstantNameAndTypeInfo struct {
    nameIndex       uint16
    descriptorIndex uint16
}

func (self *ConstantNameAndTypeInfo) readInfo(reader *ClassReader) {
    self.nameIndex = reader.readUint16()
    self.descriptorIndex = reader.readUint16()
}

CONSTANT_Fieldref_info、 CONSTANT_Methodref_info和 CONSTANT_InterfaceMethodref_info

CONSTANT_Fieldref_info表示字段符號(hào)引用,CONSTANT_Methodref_info表示普通(非接口)方法符號(hào)引用, CONSTANT_InterfaceMethodref_info表示接口方法符號(hào)引用。這三種常量結(jié)構(gòu)一模一樣。

其中CONSTANT_Fieldref_info的結(jié)構(gòu)如下:

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

class_indexname_and_type_index都是常量池索引,分別指向CONSTANT_Class_infoCONSTANT_NameAndType_info常量。

創(chuàng)建cp_member_ref.go文件,定義一個(gè)統(tǒng)一的結(jié)構(gòu)體ConstantMemberrefInfo來表示這3種常量,然后定義三個(gè)結(jié)構(gòu)體“繼承”ConstantMemberrefInfo。

Go語言并沒有“繼承”這個(gè)概念,但是可以通過結(jié)構(gòu)體嵌套來模擬

type ConstantFieldrefInfo struct{ ConstantMemberrefInfo }
type ConstantMethodrefInfo struct{ ConstantMemberrefInfo }
type ConstantInterfaceMethodrefInfo struct{ ConstantMemberrefInfo }

type ConstantMemberrefInfo struct {
    cp               ConstantPool
    classIndex       uint16
    nameAndTypeIndex uint16
}

func (self *ConstantMemberrefInfo) readInfo(reader *ClassReader) {
    self.classIndex = reader.readUint16()
    self.nameAndTypeIndex = reader.readUint16()
}
func (self *ConstantMemberrefInfo) ClassName() string {
	return self.cp.getClassName(self.classIndex)
}
func (self *ConstantMemberrefInfo) NameAndDescriptor() (string, string) {
	return self.cp.getNameAndType(self.nameAndTypeIndex)
}

還有三個(gè)常量沒有介紹:CONSTANT_MethodType_info、 CONSTANT_MethodHandle_info和 CONSTANT_InvokeDynamic_info。它們是Java SE 7才添加到class文件中的,目的是支持新增的invokedynamic指令。本次暫不實(shí)現(xiàn)。

總結(jié)

可以把常量池中的常量分為兩類:字面量(literal)符號(hào)引用 (symbolic reference)。

字面量包括數(shù)字常量字符串常量符號(hào)引用包括接口名、字段方法信息等。

除了字面量,其他常量都是通過索引直接或間接指向CONSTANT_Utf8_info常量,以 CONSTANT_Fieldref_info為例,如下所示。

4.2.4解析屬性表

一些重要的信息沒有出現(xiàn),如方法的字節(jié)碼等。那么這些信息存在哪里呢?答案是屬性表。

AttributeInfo接口

和常量池類似,各種屬性表達(dá)的信息也各不相同,因此無法用統(tǒng)一的結(jié)構(gòu)來定義。不同之處在于,常量是由Java虛擬機(jī)規(guī)范嚴(yán)格 定義的,共有14種。

但屬性是可以擴(kuò)展的,不同的虛擬機(jī)實(shí)現(xiàn)可以定義自己的屬性類型。

由于這個(gè)原因,Java虛擬機(jī)規(guī)范沒有使用tag,而是使用屬性名來區(qū)別不同的屬性。

屬性數(shù)據(jù)放在屬性名之后的u1表中,這樣Java虛擬機(jī)實(shí)現(xiàn)就可以跳過自己無法識(shí)別的屬性。 屬性的結(jié)構(gòu)定義如下:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

屬性表中存放的屬性名實(shí)際上并不是編碼后的字符串, 而是常量池索引,指向常量池中的CONSTANT_Utf8_info常量。

創(chuàng)建attribute_info.go文件,在其中定義 AttributeInfo接口

package classfile
type AttributeInfo interface {
	readInfo(reader *ClassReader)
}
func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo {...}
func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo {...}
func newAttributeInfo(attrName string, attrLen uint32,cp ConstantPool) AttributeInfo {...}

ConstantInfo接口一樣,AttributeInfo接口也只定義了一個(gè)readInfo()方法,需要由具體的屬性實(shí)現(xiàn)。readAttributes()函數(shù)讀取屬性表。

func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo {
    attributesCount := reader.readUint16()
    attributes := make([]AttributeInfo, attributesCount)
    for i := range attributes {
       attributes[i] = readAttribute(reader, cp)
    }
    return attributes
}

讀取單個(gè)屬性函數(shù):

func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo {
    attrNameIndex := reader.readUint16()
    attrName := cp.getUtf8(attrNameIndex)
    attrLen := reader.readUint32()
    attrInfo := newAttributeInfo(attrName, attrLen, cp)
    attrInfo.readInfo(reader)
    return attrInfo
}

readAttribute()先讀取屬性名索引,根據(jù)它從常量池中找到屬性名,然后讀取屬性長度,接著調(diào)用newAttributeInfo()函數(shù)創(chuàng)建具體的屬性實(shí)例。

Java虛擬機(jī)規(guī)范預(yù)定義了23種屬性,先解析其中的8種。newAttributeInfo()函數(shù)的代碼如下

func newAttributeInfo(attrName string, attrLen uint32, cp ConstantPool) AttributeInfo {
    switch attrName {
    case "Code":
       return &CodeAttribute{cp: cp}
    case "ConstantValue":
       return &ConstantValueAttribute{}
    case "Deprecated":
       return &DeprecatedAttribute{}
    case "Exceptions":
       return &ExceptionsAttribute{}
    case "LineNumberTable":
       return &LineNumberTableAttribute{}
    case "LocalVariableTable":
       return &LocalVariableTableAttribute{}
    case "SourceFile":
       return &SourceFileAttribute{cp: cp}
    case "Synthetic":
       return &SyntheticAttribute{}
    default:
       return &UnparsedAttribute{attrName, attrLen, nil}
    }
}

創(chuàng)建attr_unparsed.go文件中,定義UnparsedAttribute結(jié)構(gòu)體

package classfile

/*
attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}
*/
type UnparsedAttribute struct {
    name   string
    length uint32
    info   []byte
}

func (self *UnparsedAttribute) readInfo(reader *ClassReader) {
    self.info = reader.readBytes(self.length)
}

func (self *UnparsedAttribute) Info() []byte {
    return self.info
}

按照用途,23種預(yù)定義屬性可以分為三組。

  • 第一組屬性是實(shí)現(xiàn) Java虛擬機(jī)所必需的,共有5種;
  • 第二組屬性是Java類庫所必需的,共有12種;
  • 第三組屬性主要提供給工具使用,共有6種。

第三組屬性是可選的,也就是說可以不出現(xiàn)在class文件中。如果class文件中存在第三組屬性,Java虛擬機(jī)實(shí)現(xiàn)或者Java類庫也是可以利用它們 的,比如使用LineNumberTable屬性在異常堆棧中顯示行號(hào)。

如下給出了這23 種屬性出現(xiàn)的Java版本、分組以及它們?cè)赾lass文件中的位置。

Deprecated和Synthetic屬性

DeprecatedSynthetic是最簡單的兩種屬性,僅起標(biāo)記作用,不包含任何數(shù)據(jù)。

這兩種屬性都是JDK1.1引入的,可以出現(xiàn)在 ClassFile、field_info和method_info結(jié)構(gòu)中,它們的結(jié)構(gòu)定義如下:

Deprecated_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
}
Synthetic_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
}

由于不包含任何數(shù)據(jù),所以attribute_length的值必須是0。

Deprecated屬性用于指出類、接口、字段或方法已經(jīng)不建議使用,編譯器等工具可以根據(jù)Deprecated屬性輸出警告信息。

J2SE 5.0之前 可以使用Javadoc提供的@deprecated標(biāo)簽指示編譯器給類、接口、字段或方法添加Deprecated屬性,語法格式如下:

/** @deprecated */
public void oldMethod() {...}

J2SE 5.0開始,也可以使用@Deprecated注解,語法格式如下:

@Deprecated
public void oldMethod() {}

在Java中,編譯器可能會(huì)生成一些額外的方法、字段或類,用于支持內(nèi)部的匿名內(nèi)部類、枚舉、泛型等特性。這些生成的元素可能會(huì)被標(biāo)記為 Synthetic。

創(chuàng)建attr_markers.go文件,在其中定義 DeprecatedAttributeSyntheticAttribute結(jié)構(gòu)體,代碼如下:

package classfile
type DeprecatedAttribute struct { MarkerAttribute }
type SyntheticAttribute struct { MarkerAttribute }
type MarkerAttribute struct{}
func (self *MarkerAttribute) readInfo(reader *ClassReader) {
// read nothing
}

SourceFile屬性

SourceFile 屬性是Java類文件中的一個(gè)屬性,它用于指定源文件的名稱,即生成該類文件的源代碼文件的名稱。這個(gè)屬性并不直接影響類的運(yùn)行時(shí)行為。其結(jié)構(gòu)定義如下:

SourceFile_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 sourcefile_index;
}

attribute_length的值必須是2。sourcefile_index是常量池索引, 指向CONSTANT_Utf8_info常量

創(chuàng)建 attr_source_file.go文件,在其中定義SourceFileAttribute結(jié)構(gòu)體,代碼如下:

package classfile
type SourceFileAttribute struct {
    cp ConstantPool
    sourceFileIndex uint16
}
func (self *SourceFileAttribute) readInfo(reader *ClassReader) {
	self.sourceFileIndex = reader.readUint16()
}
func (self *SourceFileAttribute) FileName() string {
	return self.cp.getUtf8(self.sourceFileIndex)
}

例如,如果有一個(gè)名為 MyClass.java 的源代碼文件,它包含以下類:

public class MyClass {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

當(dāng)編譯 MyClass.java 文件時(shí),會(huì)生成一個(gè)名為 MyClass.class 的類文件,并在其中添加一個(gè) SourceFile 屬性,將其值設(shè)置為 MyClass.java

ConstantValue屬性

ConstantValue 屬性是Java類文件中的一個(gè)屬性,通常與字段(field)相關(guān)聯(lián)。這個(gè)屬性的作用是為字段提供一個(gè)常量初始值。這意味著,如果您在類中聲明一個(gè)字段,并為其分配了 ConstantValue 屬性,那么該字段的初始值將在類加載時(shí)被設(shè)置為 ConstantValue 中指定的常量。

ConstantValue_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 constantvalue_index;
}

例如,假設(shè)有以下Java代碼:

public class MyClass {
    public final int myField = 42;
}

在對(duì)應(yīng)的類文件中,將包含一個(gè) ConstantValue 屬性,指定了常量值 42,并與 myField 字段相關(guān)聯(lián)。當(dāng)類加載時(shí),myField 將被初始化為 42。

constantvalue_index是常量池索引,具體指向哪種常量因字段類型而異,如下為對(duì)照表

創(chuàng)建attr_constant_value.go文件,在其中定義ConstantValueAttribute結(jié)構(gòu)體,代碼如下:

package classfile
type ConstantValueAttribute struct {
	constantValueIndex uint16
}
func (self *ConstantValueAttribute) readInfo(reader *ClassReader) {
	self.constantValueIndex = reader.readUint16()
}
func (self *ConstantValueAttribute) ConstantValueIndex() uint16 {
	return self.constantValueIndex
}

Code屬性

Code 屬性是Java類文件中的一個(gè)屬性,通常與方法(Method)相關(guān)聯(lián)。

它包含了方法的字節(jié)碼指令,即實(shí)際的可執(zhí)行代碼。Code 屬性是Java類文件中最重要的屬性之一,因?yàn)樗朔椒ǖ膶?shí)際執(zhí)行邏輯。

以下是關(guān)于 Code 屬性的一些重要信息:

屬性結(jié)構(gòu)Code 屬性通常包含以下信息:

  • 最大堆棧深度(max_stack):方法執(zhí)行時(shí)所需的最大堆棧深度。
  • 局部變量表的大?。?code>max_locals):方法內(nèi)部局部變量表的大小。
  • 字節(jié)碼指令(code):實(shí)際的字節(jié)碼指令序列,即方法的執(zhí)行代碼。
  • 異常處理器列表(exception_table):用于捕獲和處理異常的信息。
  • 方法屬性(attributes):其他與方法相關(guān)的屬性,例如局部變量表、行號(hào)映射表等。

字節(jié)碼指令Code 屬性中的 code 部分包含了方法的實(shí)際字節(jié)碼指令,這些指令由Java虛擬機(jī)執(zhí)行。每個(gè)指令執(zhí)行一些特定的操作,例如加載、存儲(chǔ)、算術(shù)操作、分支、方法調(diào)用等。

異常處理Code 屬性中的 exception_table 部分包含了異常處理器的信息,指定了哪些字節(jié)碼范圍可以拋出哪些異常,并且指定了如何處理這些異常。

局部變量表Code 屬性中的局部變量表(max_locals)用于存儲(chǔ)方法執(zhí)行期間的局部變量,例如方法參數(shù)和臨時(shí)變量。

屬性Code 屬性中還可以包含其他屬性,如局部變量表、行號(hào)映射表等,這些屬性提供了更多的調(diào)試和運(yùn)行時(shí)信息。

Code 屬性是Java虛擬機(jī)實(shí)際執(zhí)行方法的關(guān)鍵部分,它描述了方法的行為和操作,包括如何處理輸入和生成輸出。編譯器將源代碼編譯為字節(jié)碼,然后將字節(jié)碼填充到 Code 屬性中,這使得Java程序可以在虛擬機(jī)上執(zhí)行。

創(chuàng)建attr_code.go文件,定義CodeAttribute結(jié)構(gòu)體ExceptionTableEntry結(jié)構(gòu)體,代碼如下:

type CodeAttribute struct {
	cp             ConstantPool
	maxStack       uint16
	maxLocals      uint16
	code           []byte
	exceptionTable []*ExceptionTableEntry
	attributes     []AttributeInfo
}

func (self *CodeAttribute) readInfo(reader *ClassReader) {
	self.maxStack = reader.readUint16()
	self.maxLocals = reader.readUint16()
	codeLength := reader.readUint32()
	self.code = reader.readBytes(codeLength)
	self.exceptionTable = readExceptionTable(reader)
	self.attributes = readAttributes(reader, self.cp)
}
type ExceptionTableEntry struct {
    startPc   uint16
    endPc     uint16
    handlerPc uint16
    catchType uint16
}

func readExceptionTable(reader *ClassReader) []*ExceptionTableEntry {
    exceptionTableLength := reader.readUint16()
    exceptionTable := make([]*ExceptionTableEntry, exceptionTableLength)
    for i := range exceptionTable {
       exceptionTable[i] = &ExceptionTableEntry{
          startPc:   reader.readUint16(),
          endPc:     reader.readUint16(),
          handlerPc: reader.readUint16(),
          catchType: reader.readUint16(),
       }
    }
    return exceptionTable
}

Exceptions屬性

Exceptions屬性通常與方法(Method)相關(guān)聯(lián),用于指定方法可能拋出的受檢查異常(checked exceptions)的列表。

Exceptions_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_exceptions;
    u2 exception_index_table[number_of_exceptions];
}

創(chuàng)建attr_exceptions.go文件,在其中定義ExceptionsAttribute結(jié)構(gòu)體

type ExceptionsAttribute struct {
    exceptionIndexTable []uint16
}

func (self *ExceptionsAttribute) readInfo(reader *ClassReader) {
    self.exceptionIndexTable = reader.readUint16s()
}

func (self *ExceptionsAttribute) ExceptionIndexTable() []uint16 {
    return self.exceptionIndexTable
}

LineNumberTable和LocalVariableTable屬性

LineNumberTable 和 LocalVariableTable 屬性是Java類文件中的兩個(gè)用于調(diào)試和運(yùn)行時(shí)跟蹤的屬性,它們包含了與源代碼中行號(hào)和局部變量相關(guān)的信息。

LineNumberTable 屬性:用于建立源代碼行號(hào)和字節(jié)碼指令之間的映射。它允許開發(fā)工具在調(diào)試時(shí)將異常棧軌跡映射到源代碼的特定行,以便開發(fā)者可以更容易地定位和修復(fù)代碼中的問題。結(jié)構(gòu)如下:

LineNumberTable {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {
        u2 start_pc;
        u2 line_number;
    } line_number_table[line_number_table_length];
}

LocalVariableTable 屬性:用于跟蹤局部變量的信息,包括局部變量的名稱、數(shù)據(jù)類型、作用域范圍和字節(jié)碼偏移。

創(chuàng)建attr_line_number_table.go文件,定義LineNumberTableAttribute結(jié)構(gòu)體,代碼如下:

type LineNumberTableAttribute struct {
    lineNumberTable []*LineNumberTableEntry
}

type LineNumberTableEntry struct {
    startPc    uint16
    lineNumber uint16
}

func (self *LineNumberTableAttribute) readInfo(reader *ClassReader) {
    lineNumberTableLength := reader.readUint16()
    self.lineNumberTable = make([]*LineNumberTableEntry, lineNumberTableLength)
    for i := range self.lineNumberTable {
       self.lineNumberTable[i] = &LineNumberTableEntry{
          startPc:    reader.readUint16(),
          lineNumber: reader.readUint16(),
       }
    }
}

func (self *LineNumberTableAttribute) GetLineNumber(pc int) int {
    for i := len(self.lineNumberTable) - 1; i >= 0; i-- {
       entry := self.lineNumberTable[i]
       if pc >= int(entry.startPc) {
          return int(entry.lineNumber)
       }
    }
    return -1
}

創(chuàng)建attr_local_variable_table.go文件,定義LocalVariableTableAttribute,代碼如下:

type LocalVariableTableAttribute struct {
    localVariableTable []*LocalVariableTableEntry
}

type LocalVariableTableEntry struct {
    startPc         uint16
    length          uint16
    nameIndex       uint16
    descriptorIndex uint16
    index           uint16
}

func (self *LocalVariableTableAttribute) readInfo(reader *ClassReader) {
    localVariableTableLength := reader.readUint16()
    self.localVariableTable = make([]*LocalVariableTableEntry, localVariableTableLength)
    for i := range self.localVariableTable {
       self.localVariableTable[i] = &LocalVariableTableEntry{
          startPc:         reader.readUint16(),
          length:          reader.readUint16(),
          nameIndex:       reader.readUint16(),
          descriptorIndex: reader.readUint16(),
          index:           reader.readUint16(),
       }
    }
}

4.3測試

打開ch03\main.go文件,修改import語句和startJVM()函數(shù),代碼如下:

package main

import "fmt"
import "strings"
import "jvmgo/ch03/classfile"
import "jvmgo/ch03/classpath"

func main() {
    cmd := parseCmd()

    if cmd.versionFlag {
       fmt.Println("version 0.0.1")
    } else if cmd.helpFlag || cmd.class == "" {
       printUsage()
    } else {
       startJVM(cmd)
    }
}

func startJVM(cmd *Cmd) {
    cp := classpath.Parse(cmd.XjreOption, cmd.cpOption)
    className := strings.Replace(cmd.class, ".", "/", -1)
    cf := loadClass(className, cp)
    fmt.Println(cmd.class)
    printClassInfo(cf)
}

func loadClass(className string, cp *classpath.Classpath) *classfile.ClassFile {
    classData, _, err := cp.ReadClass(className)
    if err != nil {
       panic(err)
    }

    cf, err := classfile.Parse(classData)
    if err != nil {
       panic(err)
    }

    return cf
}

func printClassInfo(cf *classfile.ClassFile) {
    fmt.Printf("version: %v.%v\n", cf.MajorVersion(), cf.MinorVersion())
    fmt.Printf("constants count: %v\n", len(cf.ConstantPool()))
    fmt.Printf("access flags: 0x%x\n", cf.AccessFlags())
    fmt.Printf("this class: %v\n", cf.ClassName())
    fmt.Printf("super class: %v\n", cf.SuperClassName())
    fmt.Printf("interfaces: %v\n", cf.InterfaceNames())
    fmt.Printf("fields count: %v\n", len(cf.Fields()))
    for _, f := range cf.Fields() {
       fmt.Printf("  %s\n", f.Name())
    }
    fmt.Printf("methods count: %v\n", len(cf.Methods()))
    for _, m := range cf.Methods() {
       fmt.Printf("  %s\n", m.Name())
    }
}

首先go install jvmgo\ch03 生產(chǎn)ch03.exe

然后執(zhí)行,并輸入命令行語句,得到結(jié)果如下:

  • version: 52.0:這表示 java.lang.String 類的類文件版本為 52.0。類文件版本號(hào)與Java版本號(hào)有關(guān),52.0 對(duì)應(yīng)于Java 8。
  • constants count: 548:這表示常量池中包含 548 個(gè)常量。常量池包含了類的常量、方法、字段等信息。
  • access flags: 0x31:這表示類的訪問標(biāo)志,0x31 是十六進(jìn)制表示,對(duì)應(yīng)于二進(jìn)制 00110001。這些標(biāo)志描述類的訪問權(quán)限和特性。
  • this class: java/lang/String:這表示類的名稱,即 java.lang.String
  • super class: java/lang/Object:這表示 java.lang.String 類繼承自 java.lang.Object 類。
  • interfaces: [java/io/Serializable java/lang/Comparable java/lang/CharSequence]:這表示 java.lang.String 類實(shí)現(xiàn)了三個(gè)接口,分別是 Serializable、Comparable 和 CharSequence
  • fields count: 5:這表示 java.lang.String 類包含 5 個(gè)字段。
  • methods count: 94:這表示 java.lang.String 類包含 94 個(gè)方法。其中一些是構(gòu)造方法(<init>),其他是實(shí)例方法。

以上就是Golang實(shí)現(xiàn)Java虛擬機(jī)之解析class文件詳解的詳細(xì)內(nèi)容,更多關(guān)于Go實(shí)現(xiàn)Java虛擬機(jī)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Golang基于內(nèi)存的鍵值存儲(chǔ)緩存庫go-cache

    Golang基于內(nèi)存的鍵值存儲(chǔ)緩存庫go-cache

    go-cache是一個(gè)內(nèi)存中的key:value store/cache庫,適用于單機(jī)應(yīng)用程序,本文主要介紹了Golang基于內(nèi)存的鍵值存儲(chǔ)緩存庫go-cache,具有一定的參考價(jià)值,感興趣的可以了解一下
    2025-03-03
  • go的切片擴(kuò)容機(jī)制詳解

    go的切片擴(kuò)容機(jī)制詳解

    本文主要介紹了go的切片擴(kuò)容機(jī)制詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04
  • 手把手帶你走進(jìn)Go語言之運(yùn)算符解析

    手把手帶你走進(jìn)Go語言之運(yùn)算符解析

    這篇文章主要介紹了手Go語言之運(yùn)算符解析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-09-09
  • Go語言集成開發(fā)環(huán)境之VS Code安裝使用

    Go語言集成開發(fā)環(huán)境之VS Code安裝使用

    VS Code是微軟開源的一款編輯器,插件系統(tǒng)十分的豐富,下面介紹如何用VS Code搭建go語言開發(fā)環(huán)境,需要的朋友可以參考下
    2021-10-10
  • Go語言實(shí)現(xiàn)定時(shí)器的方法

    Go語言實(shí)現(xiàn)定時(shí)器的方法

    這篇文章主要介紹了Go語言實(shí)現(xiàn)定時(shí)器的方法,涉及Go語言時(shí)間操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-02-02
  • CGO編程基礎(chǔ)快速入門

    CGO編程基礎(chǔ)快速入門

    這篇文章主要為大家介紹了CGO編程基礎(chǔ)快速入門示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • Golang分布式注冊(cè)中心實(shí)現(xiàn)流程講解

    Golang分布式注冊(cè)中心實(shí)現(xiàn)流程講解

    這篇文章主要介紹了Golang分布式注冊(cè)中心實(shí)現(xiàn)流程,注冊(cè)中心可以用于服務(wù)發(fā)現(xiàn),服務(wù)注冊(cè),配置管理等方面,在分布式系統(tǒng)中,服務(wù)的發(fā)現(xiàn)和注冊(cè)是非常重要的組成部分,需要的朋友可以參考下
    2023-05-05
  • go語言實(shí)現(xiàn)兩個(gè)協(xié)程交替打印

    go語言實(shí)現(xiàn)兩個(gè)協(xié)程交替打印

    這篇文章主要介紹了go語言實(shí)現(xiàn)兩個(gè)協(xié)程交替打印,文章主要分享了兩種方法使用兩個(gè)channel和使用一個(gè)channel,內(nèi)容介紹詳細(xì)具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-03-03
  • Go一站式配置管理工具Viper的使用教程

    Go一站式配置管理工具Viper的使用教程

    Viper是一個(gè)方便Go語言應(yīng)用程序處理配置信息的庫,它可以處理多種格式的配置,這篇文章主要為大家介紹了它的具體使用教程,需要的可以參考下
    2023-08-08
  • Go調(diào)用opencv實(shí)現(xiàn)圖片矯正的代碼示例

    Go調(diào)用opencv實(shí)現(xiàn)圖片矯正的代碼示例

    這篇文章主要為大家詳細(xì)介紹了Go調(diào)用opencv實(shí)現(xiàn)圖片矯正的代碼示例,文中的示例代碼簡潔易懂,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-09-09

最新評(píng)論