筆者博客地址:https://charpty.com
本文代碼委託在:https://github.com/charpty/cjvm
許多同學看了不少關於JVM和GC相關的書,很多概念都熟悉了,但本着經歷過才能身入其境的原則,我覺得必須要自己寫一寫,體會下前人的思想和辛苦,才能對所學JVM和GC相關知識進行實踐性總結。
業餘時間的樂趣型項目,使用C語言實現的一個可高效運行的Java虛擬機,包括解釋執行實現和CS|IP方式實現。使用C99編寫,僅在類unix系統上運行,包含類加載子系統、執行子系統(常用字節碼指令實現)、運行時數據區、GC、JIT等組件的實現。最終的目標是能夠使用該虛擬機運行筆者網站的Java代碼。
01-搜索class文件
這基本上就是一個簡單的文件搜索並讀取的操作,代碼也比較好理解,只是有幾個注意事項:
- 要遵循規範中ClassLoader的雙親委託模型,總是嘗試在父ClassLoader中找尋文件
- 要加載的不僅僅是直接的class文件,還有壓縮在jar包、war包中的class文件
雙親委託加載方式
在虛擬機中有3種類加載器,分別是:BootstrpLoader
、ExtClassLoader
、AppClassLoader
。
對應的我們稱被這3個類加載器加載的class文件路徑爲:bootStrapPath
、extPath
、userPath
,其中AppClassLoader也稱爲SystemClassLoader,它用於加載系統(用戶的項目)裏的class文件,所以稱這些class的路徑爲userPath更加形象。
classpath.c
typedef struct ClassPath
{
char *bootStrapPath;
char *extPath;
char *userPath;
char *(*readClass)(ClassPath *classPath, char *classname);
} ClassPath;
SClass *readClass(ClassPath *classPath, char *classname)
{
SClass *r;
if ((r = readBootStrap(classPath, classname)) != NULL)
return r;
else if ((r = readExt(classPath, classname)) != NULL)
return r;
else
return readUser(classPath, classname);
}
從jar包中加載
jar包本質上就是zip壓縮包,所以我們使用libzip來讀取它。
classpath.c
SClass *readClassInJar(char *jarPath, char *classname)
{
int err;
struct zip *z = zip_open(jarPath, 0, &err);
// TODO I can't find err code >39 means in zip.h
if (err != 0 && err < 39)
{
LOG_ERROR(__FILE__, __LINE__, "open jar file %s failed, error code is: %d", jarPath, err);
return NULL;
}
const char *name = classname;
struct zip_stat st;
zip_stat_init(&st);
zip_stat(z, name, 0, &st);
if (st.size <= 0)
return NULL;
char *contents = malloc(st.size);
struct zip_file *f = zip_fopen(z, name, 0);
zip_fread(f, contents, st.size);
zip_fclose(f);
zip_close(z);
struct SClass *r = (SClass *)malloc(sizeof(struct SClass));
r->len = st.size;
r->bytes = contents;
r->name = classname;
return r;
}
02-解析class文件的內容
這裏也就是將class文件的裏的字節內容,解析成語言可識別的數據結構,這裏我們將其解析成稱爲ClassFile
的結構體。ClassFile
將單個字節碼文件的內容解析成C語言的結構體,方便後續能夠被ClassLoader
加載爲Class
結構體。
僅看第一層內容,ClassFile
並不複雜。
classfile.c
// 屬性命名和oracle虛擬機規範儘量保持一直(規範中屬性名都使用下劃線,但結構體我習慣用駝峯形式)
// https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
ClassFile *readAsClassFile(ClassReader *r)
{
ClassFile *rs = (ClassFile *)malloc(sizeof(struct ClassFile));
// 讀取版本信息
rs->magic = readUint32(r);
checkMagic(rs->magic);
rs->minor_version = readUint16(r);
rs->major_version = readUint16(r);
checkClassVersion(rs->major_version, rs->minor_version);
// 讀取常量池,動長
struct CP *csp = readConstantPool(r);
rs->constant_pool = csp;
// 訪問標誌,是一個位圖標記,記錄了類的訪問級別,類是否爲final,是否是註解類型等等
rs->access_flags = readUint16(r);
// 當前類名在常量池中的索引
rs->this_class = readUint16(r);
// 當前類父類名在常量池中的索引
rs->super_class = readUint16(r);
// 讀取該類實現的所有的接口
rs->interfaces = readUint16s(r, &(rs->interfaces_count));
// 讀取當前類的屬性,包括靜態屬性
rs->fields = readMembers(r, csp);
// 讀取當前類的方法信息,包括靜態方法
rs->methods = readMembers(r, csp);
// 讀取剩餘的不包含在方法或者字段裏的其它屬性表信息
rs->attributes = readAttributes(r, csp);
return rs;
}
我們第一步要做的就是將class文件裏的內容解析爲這麼一個不太複雜的結構體,僅有這麼一個結構體還不夠,爲了統一表示對一個class文件的讀取操作,我們使用一個叫ClassReader
的結構體表示該操作。
classreader.h
typedef struct ClassReader
{
// 逐個字節讀下去
uint32_t position;
uint32_t len;
unsigned char *data;
} ClassReader;
// 提供了以下幾種讀取方式
static uint8_t readUint8(ClassReader *r);
static uint16_t readUint16(ClassReader *r);
static uint32_t readUint32(ClassReader *r);
static uint64_t readUint64(ClassReader *r);
static uint16_t *readUint16s(ClassReader *r, u_int16_t *size);
static char *readBytes(ClassReader *r, u_int32_t n);
可以看出,同時對應該結構體也準備了一系列讀取方法,幾個典型實現如下:
classreader.h
static uint16_t readUint16(ClassReader *r)
{
return (uint16_t)r->data[r->position++] << 8 | (uint16_t)r->data[r->position++];
}
static uint32_t readUint32(ClassReader *r)
{
u_int8_t x1 = r->data[r->position++];
u_int8_t x2 = r->data[r->position++];
u_int8_t x3 = r->data[r->position++];
u_int8_t x4 = r->data[r->position++];
// *(uint32_t *)(r->data + r->position);
return (uint32_t)x1 << 24 | (uint32_t)x2 << 16 | (uint32_t)x3 << 8 | (uint32_t)x4;
}
... 其它函數
static uint16_t *readUint16s(ClassReader *r, u_int16_t *size)
{
uint16_t *rs = (uint16_t *)malloc((*size = readUint16(r)) * sizeof(u_int16_t));
for (int i = 0; i < (*size); i++)
{
rs[i] = readUint16(r);
}
return rs;
}
有了這兩個基礎,剩下的事情就是按字節和規律一個個讀取了。
classfile.c
ClassFile *readAsClassFile(ClassReader *r)
{
ClassFile *rs = (ClassFile *)malloc(sizeof(struct ClassFile));
// 讀取版本信息
rs->magic = readUint32(r);
checkMagic(rs->magic);
rs->minor_version = readUint16(r);
rs->major_version = readUint16(r);
checkClassVersion(rs->major_version, rs->minor_version);
// 讀取常量池,動長
struct CP *csp = readConstantPool(r);
rs->constant_pool = csp;
// 訪問標誌,是一個位圖標記,記錄了類的訪問級別,類是否爲final,是否是註解類型等等
rs->access_flags = readUint16(r);
// 當前類名在常量池中的索引
rs->this_class = readUint16(r);
// 當前類父類名在常量池中的索引
rs->super_class = readUint16(r);
// 讀取該類實現的所有的接口
rs->interfaces = readUint16s(r, &(rs->interfaces_count));
// 讀取當前類的屬性,包括靜態屬性
rs->fields = readMembers(r, csp);
// 讀取當前類的方法信息,包括靜態方法
rs->methods = readMembers(r, csp);
// 讀取剩餘的不包含在方法或者字段裏的其它屬性表信息
rs->attributes = readAttributes(r, csp);
return rs;
}
接下來比較複雜的就是常量池、方法和屬性簽名、屬性表這3個了。
Class中的常量池
當前這個常量池和後面運行時數據區的常量池不同,它僅是當前這個class文件裏使用的。
constant_pool.h
typedef struct CPInfo
{
uint8_t tag;
// 常量池裏存着各種各樣類型的信息
void *v1;
void *v2;
} CPInfo;
// 用CP表示class裏的常量池,運行期的常量池則用GCP來表示,更親切
typedef struct CP
{
uint32_t len;
CpInfo **infos;
} CP;
接下來的任務是將class字節碼的常量池部分解析成常量池對應結構體。
constant_pool.h
static CP *readConstantPool(ClassReader *r)
{
CP *rs = (CP *)malloc(sizeof(struct CP));
int cpCount = readUint16(r);
rs->len = cpCount;
rs->infos = (CPInfo **)malloc(cpCount * sizeof(CPInfo *));
// 常量池從下標1開始
for (int i = 1; i < cpCount; i++)
{
rs->infos[i] = readConstantInfo(r, rs);
// http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.5
// 這就是個數的特殊情況,讀到long和double時,必須下一個元素是個空,以兼容老版本
// 這是由於一個byte佔常量池2個位置
if (rs->infos[i]->tag == CONSTANT_Long || (rs->infos[i]->tag == CONSTANT_Double))
{
++i;
continue;
}
}
return rs;
}
常量池裏存着各種類型的信息,但最多的也就兩個屬性,所以這裏就用兩個void*
指針表示了。常量信息使用tag
表示屬性類型,有14種類型。
constant_pool.h
#define CONSTANT_Class 7
#define CONSTANT_Fieldref 9
#define CONSTANT_Methodref 10
#define CONSTANT_InterfaceMethodref 11
#define CONSTANT_String 8
#define CONSTANT_Integer 3
#define CONSTANT_Float 4
#define CONSTANT_Long 5
#define CONSTANT_Double 6
#define CONSTANT_NameAndType 12
#define CONSTANT_Utf8 1
#define CONSTANT_MethodHandle 15
#define CONSTANT_MethodType 16
#define CONSTANT_InvokeDynamic 18
根據不同的類型,我們需要不同的方式,列舉一部分。
constant_pool.h
static CPInfo *readConstantInfo(ClassReader *r, CP *cp)
{
CPInfo *rs = (CPInfo *)malloc(sizeof(struct CPInfo));
uint8_t tag = rs->tag = readUint8(r);
if (tag == CONSTANT_Class)
{
// nameIndex
// 存儲class存儲的位置索引
rs->v1 = malloc(sizeof(uint16_t));
*(uint16_t *)rs->v1 = readUint16(r);
}
else if (tag == CONSTANT_Fieldref)
{
// classIndex and nameAndTypeIndex
rs->v1 = malloc(sizeof(uint16_t));
rs->v2 = malloc(sizeof(uint16_t));
*(uint16_t *)rs->v1 = readUint16(r);
*(uint16_t *)rs->v2 = readUint16(r);
}
... 各種類似解析
else if (tag == CONSTANT_InvokeDynamic)
{
// bootstrapMethodAttrIndex and nameAndTypeIndex
rs->v1 = malloc(sizeof(uint16_t));
rs->v2 = malloc(sizeof(uint16_t));
*(uint16_t *)rs->v1 = readUint16(r);
*(uint16_t *)rs->v2 = readUint16(r);
}
方法和屬性簽名
方法和屬性簽名帶的幾個屬性是相同的,所以都用同一個結構體表示了。
member_info.h
typedef struct MemberInfo
{
// 訪問控制符,是否靜態,是否公開等
uint16_t accessFlags;
// 方法名|字段名在常量池中索引
uint16_t nameIndex;
// 描述符字符串
// https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
uint16_t descriptorIndex;
// 屬性表,方法代碼存在屬性表中
AttributeInfos *attributes;
} MemberInfo;
可以看到,在這個結構體中外層只有一些簽名信息。
就方法而言,包括方法的訪問控制信息和特性信息、方法的名稱信息、方法的描述信息3部分,其中方法的描述符也是一串字符串,如下:
(IDLjava/lang/Thread;)Ljava/lang/Object;
實際就是方法
Object m(int i, double d, Thread t) {...}
那麼方法中的具體實現代碼存在哪裏呢?答案是屬性表中,屬性表可以說是最複雜多樣的一個結構了,基本上什麼都有。
屬性表
我們使用一個簡單的結構體來表示屬性表
attribute_info.h
typedef struct AttributeInfo
{
// 保留文件常量池的指針,後續不用每次傳遞了
CP *cp;
// 一共23中屬性表,CJVM中僅解析需要用到的部分
// https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7
void *info;
} AttributeInfo;
typedef struct AttributeInfos
{
uint32_t size;
AttributeInfo **infos;
} AttributeInfos;
和常量池信息類似,屬性表中的信息也有多種類型,類型很多,我們就不一一解析了,僅解析我們需要用到的幾個。
attribute_info.h
typedef struct ExceptionTableEntry
{
// PC計數器起,可以理解爲代碼起,包括
uint16_t startPc;
// try-catch代碼行止,不包括
uint16_t endPc;
// catch時處理行起,必須指向有效的code數組某一個下標
uint16_t handlerPc;
// catch異常類型類名
uint16_t catchType;
} ExceptionTableEntry;
typedef struct ExceptionTable
{
uint32_t size;
ExceptionTableEntry **entrys;
} ExceptionTable;
/*
* 實際的代碼(指令)存儲在屬性表中
*/
typedef struct AttrCode
{
uint16_t maxStack;
uint16_t maxLocals;
uint32_t codeLen;
char *code;
ExceptionTable *exceptionTable;
AttributeInfos *attributes;
} AttrCode;
// Deprecated過期、內部生成字段等標記位
typedef struct MarkerAttribute
{
} MarkerAttribute;
// 定長屬性
typedef struct ConstantValueAttribute
{
uint16_t constantValueIndex;
} ConstantValueAttribute;
// 方法表示
typedef struct EnclosingMethodAttribute
{
uint16_t classIndex;
uint16_t methodIndex;
} EnclosingMethodAttribute;
// 指向異常表
typedef struct ExceptionsAttribute
{
uint32_t len;
uint16_t *exceptionIndexTable[];
} ExceptionsAttribute;
// 內部類
typedef struct InnerClassInfo
{
uint16_t innerClassInfoIndex;
uint16_t outerClassInfoIndex;
uint16_t innerNameIndex;
uint16_t innerClassAccessFlags;
} InnerClassInfo;
// 代碼行數信息,方便在出錯時定位問題,但不完全準確
typedef struct LineNumberTableEntry
{
uint16_t startPc;
uint16_t lineNumber;
} LineNumberTableEntry;
// 棧幀本地變量表
typedef struct LocalVariableTableEntry
{
uint16_t startPc;
uint16_t length;
uint16_t nameIndex;
uint16_t descriptorIndex;
uint16_t index;
} LocalVariableTableEntry;
typedef struct MethodParameter
{
uint16_t nameIndex;
// 參數、方法、屬性、類都有權限控制標記
uint16_t accessFlags;
} MethodParameter;
// JDK8以後可以指定編譯器保留形參的名稱
typedef struct MethodParameters
{
uint8_t len;
MethodParameter **parameters;
} MethodParameters;
// 從哪編譯而來
typedef struct SourceFileAttribute
{
uint16_t signatureIndex;
} SourceFileAttribute;
// 後續再解析的屬性
typedef struct UnparsedAttribute
{
uint32_t nameLen;
char *name;
uint32_t length;
uint32_t infoLen;
char *info;
} UnparsedAttribute;
只展示了一部分解析後的結構體,對於我們不想解析的或者後續再解析的,我們統一使用UnparsedAttribute
表示。
同樣的,我們也是按照類型逐個解析這些屬性表
attribute_info.h
static AttributeInfo *readAttribute(ClassReader *r, CP *cp)
{
uint16_t attrNameIndex = readUint16(r);
char *attrName = getUtf8(cp, attrNameIndex);
u_int32_t attrLen = readUint32(r);
struct AttributeInfo *rs = (AttributeInfo *)malloc(sizeof(struct AttributeInfo));
rs->cp = cp;
if (strcmp(attrName, "Code") == 0)
{
struct AttrCode *attr = (AttrCode *)malloc(sizeof(struct AttrCode));
attr->maxStack = readUint16(r);
attr->maxLocals = readUint16(r);
attr->codeLen = readUint32(r);
attr->code = readBytes(r, attr->codeLen);
uint16_t exceptionTableLength = readUint16(r);
ExceptionTable *exceptionTable = malloc(sizeof(ExceptionTable));
exceptionTable->size = exceptionTableLength;
exceptionTable->entrys = malloc(sizeof(ExceptionTableEntry *) * exceptionTableLength);
for (int i = 0; i < exceptionTableLength; i++)
{
exceptionTable->entrys[i] = malloc(sizeof(ExceptionTableEntry));
exceptionTable->entrys[i]->startPc = readUint16(r);
exceptionTable->entrys[i]->endPc = readUint16(r);
exceptionTable->entrys[i]->handlerPc = readUint16(r);
exceptionTable->entrys[i]->catchType = readUint16(r);
}
attr->attributes = readAttributes(r, cp);
rs->info = attr;
}
else if (strcmp(attrName, "ConstantValue") == 0)
{
struct ConstantValueAttribute *attr = (ConstantValueAttribute *)malloc(sizeof(struct ConstantValueAttribute));
attr->constantValueIndex = readUint16(r);
rs->info = attr;
}
...其他類型的解析
else
{
struct UnparsedAttribute *attr = (UnparsedAttribute *)malloc(sizeof(struct UnparsedAttribute));
attr->name = attrName;
attr->infoLen = attrLen;
attr->info = readBytes(r, attrLen);
}
}
至此,我們已經將class文件解析爲ClassFile
結構體,接下來可以把它交給ClassLoader
加載爲運行時的Class
結構體。