我們在平時開發過程中,一定定義過無數個千奇百怪的類,但是大家有想過,一個 Java 文件中的 Class,在虛擬機中的真實形態是什麼麼?
這篇文章就帶大家探討一下在 Android ART 裏,類的真實形態,以及類加載的過程。
視頻鏈接:
Android虛擬機Java對象初始化原理,Class之間的關係
鏈接: https://pan.baidu.com/s/1AisNpQGWVrCHWaN-gkJwjg 提取碼: d4gf更多面試內容,面試專題,flutter視頻 全套,音視頻從0到高手開發。
關注GitHub:https://github.com/xiangjiana/Android-MS
免費獲取面試PDF合集
免費提供簡歷修改建議,獲取大廠面試PDF和視頻教程
預備知識
- 瞭解 Java 基本開發
- 瞭解 ClassLoader 基本使用
看完本文可以達到什麼程度
- 瞭解 Android ART 中類的存在形式
- 瞭解 Android ART 中類加載的過程
閱讀前準備工作
- 下載 ART 源碼 作爲參照
文章概覽
一、在 Java 中如何定義一個類
對於如何在 Java 代碼中定義一個類,我們一定非常熟悉了,代碼如下:
class MInterface {
void imethod() {
}
}
class Parent {
}
class Child extends Parent implements MInterface {
}
二、ART 中如何表示一個 Java 類
那麼對於一個 Java 類,在 ART 中是如何表示的呢?
在 ART 中,也定義了一個 Class 類,用來表示 Java 世界中的類。
當然,這個類是 c++ 定義的,畢竟 ART 就是 c++ 實現的。
下面這張圖展示了 ART 中類的重要部分。
下面我們就看看這個 Class 的具體定義:
2.1 類的定義
// C++ mirror of java.lang.Class
class MANAGED Class FINAL : public Object {
private:
// 指向定義 Class 的 ClassLoader,如果爲 null,說明是 bootstrap system loader
HeapReference<ClassLoader> class_loader_;
// 對於數組類型有用,保存了數組的原始類型,比如 對於 String[],這裏指向的是 String
// 對非數組類型,值爲 null
HeapReference<Class> component_type_;
// 指向 DexCache,如果是運行時生成的 Class,值爲 null
HeapReference<DexCache> dex_cache_;
HeapReference<ClassExt> ext_data_;
// interface table,接口方法表,IfTable 中保存了接口類指針和方法表指針
HeapReference<IfTable> iftable_;
// Descriptor for the class such as "java.lang.Class" or "[C". Lazily initialized by ComputeName
// 類描述符 eg: java.lang.Class 或者 [C
HeapReference<String> name_;
// 父類,如果是 java.lang.Object 值爲 null
HeapReference<Class> super_class_;
// 虛方法表,"invoke-virtual" 指令會用到,用來保存父類虛方法以及自身虛方法
HeapReference<PointerArray> vtable_;
// 保存類屬性,只保存自身屬性
uint64_t ifields_;
// 指向 ArtMethod 數組,保存了所有的方法,包括私有方法,靜態方法,final 方法,虛方法和繼承的方法
uint64_t methods_;
// 保存靜態屬性
uint64_t sfields_;
// 訪問修飾符
uint32_t access_flags_;
uint32_t class_flags_;
// 類實例大小,GC 時使用
uint32_t class_size_;
// 線程 id,類加載時加鎖使用
pid_t clinit_thread_id_;
// ClassDex 在 DEX 文件中的 index
int32_t dex_class_def_idx_;
// DEX 文件中的類型 id
int32_t dex_type_idx_;
// 實例屬性數量
uint32_t num_reference_instance_fields_;
// 靜態變量數量
uint32_t num_reference_static_fields_;
// 對象大小,GC 時使用
uint32_t object_size_;
uint32_t object_size_alloc_fast_path_;
uint32_t primitive_type_;
// ifields 的偏移量
uint32_t reference_instance_offsets_;
// 類初始化狀態
Status status_;
// methods_ 中第一個從接口中複製的虛方法的偏移
uint16_t copied_methods_offset_;
// methods_ 中第一個自身定義的虛方法的偏移
uint16_t virtual_methods_offset_;
// java.lang.Class
static GcRoot<Class> java_lang_Class_;
};
上面的類就是 Java 類在 ART 中的真實形態,各個屬性在上面做了註釋。
這裏對幾個比較重要的屬性再做一下解釋。
和 Java 類方法有關的兩個屬性是 iftable_,vtable_
和 methods_
。
其中 iftable_
保存的是接口中的方法,vtable_
保存的是虛方法,methods_
保存的是所有方法。
什麼是虛方法呢?虛方法其實是 C++ 中的概念,在 C++ 中,被 virtual 關鍵字修飾的方法就是虛方法。
而在 Java 中,我們可以理解爲所有子類複寫的方法都是虛方法。
和 Java 類屬性有關的兩個屬性是 ifields_
和 sfields_
。分別保存的是類的實例屬性和靜態屬性。
從上面的我們可以看到,Java 類的屬性就都保存在 ART 中定義的 Class 裏了。
其中方法最終會指向 ArtMethod
實例上,屬性,最終會指向 ArtField
實例上。
2.2 類方法的定義
在 ART 中,一個 Java 的類方法是用 ArtMethod
實例來表示的。ArtMethod
結構如下:
class ArtMethod FINAL {
protected:
// 定義此方法的類
GcRoot<mirror::Class> declaring_class_;
// 訪問修飾符
std::atomic<std::uint32_t> access_flags_;
// 方法 code 在 dex 中的偏移
uint32_t dex_code_item_offset_;
// 方法在 dex 中的 index
uint32_t dex_method_index_;
// 方法 index,對於虛方法,指的是 vtable 中的 index,對於接口方法,指的是 ifTable 中的 index
uint16_t method_index_;
// 方法的熱度計數,Jit 會根據此變量決定是否將方法進行編譯
uint16_t hotness_count_;
struct PtrSizedFields {
ArtMethod** dex_cache_resolved_methods_;
void* data_;
// 方法的入口
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
}
2.3 類屬性的定義
在 ART 中,一個 Java 類屬性是用 ArtField 實例來表示的。ArtField
結構如下:
class ArtField FINAL {
private:
// 定義此屬性的類
GcRoot<mirror::Class> declaring_class_;
// 訪問修飾符
uint32_t access_flags_ = 0;
// 變量在 dex 中的 id
uint32_t field_dex_idx_ = 0;
// 此變量在類或者類實例中的偏移
uint32_t offset_ = 0;
}
三、ART 中加載類的過程
3.1 類加載的本質
在 Java 中定義好一個類之後,還需要通過 ClassLoader 進行加載。
我們經常會說到類加載,但是類加載的本質是什麼呢?
在我們上面瞭解了一個 Java 類在 ART 中的真實形態以後,我們就比較容易理解類加載的本質了。
我們都知道,Java 文件編譯完成的產物是 .class
文件,在 Android 中是 .dex
文件,類加載的本質就是解析.class / .dex
文件,並根據對應的信息生成 ArtField,ArtMethod
,最後生成 Class 實例。
再簡單點來說,類加載的本質就是根據 .dex
文件內容創建 Class 實例。
3.2 ART 中類加載的入口 -- ClassLinker#DefineClass
在 Android 中,常見的兩個 ClassLoader
就是 PathClassLoader
和 DexClassLoader
,都是繼承了 BaseDexClassLoader
,我們就從 BaseDexClassLoader#findClass
開始看一下整個加載的流程。
// BaseDexClassLoader#findClass
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
// ...
return c;
}
// DexPathList#findClass
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
// ...
return null;
}
// Element#findCLass
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
從上面的代碼來看,BaseDexClassLoader#findClass
一路調用,調用到 DexFile#loadClassBinaryName
,我們再繼續往下看。
// DexFile
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
在 DexFile
裏,最終調用到 defineClassNative
方法去加載 Class,對應到 JNI 中的方法是 DexFile_defineClassNative
,位於 runtime/native/dalvik_system_DexFile.cc
文件中。
static jclass DexFile_defineClassNative(JNIEnv* env,
jclass,
jstring javaName,
jobject javaLoader,
jobject cookie,
jobject dexFile) {
// 調用
for (auto& dex_file : dex_files) {
ObjPtr<mirror::Class> result = class_linker->DefineClass(soa.Self(),
descriptor.c_str(),
hash,
class_loader,
*dex_file,
*dex_class_def);
}
}
而在 defineClassNative
中,又是調用 ClassLinker#DefineClass
去加載類的。
所以我們可以說,ClassLinker#DefineClass
就是 ART 中類加載的入口。
入口已經出現,我們就進去探索一番,看看類加載的時候,是如何創建 Class 實例的~
DefineClass
本身代碼比較多,我們這裏把代碼簡化一下,看其主要流程。
mirror::Class* ClassLinker::DefineClass(Thread* self,
const char* descriptor,
size_t hash,
Handle<mirror::ClassLoader> class_loader,
const DexFile& dex_file,
const DexFile::ClassDef& dex_class_def) {
auto klass = hs.NewHandle<mirror::Class>(nullptr);
// 一些常用的,並且類大小可以確定的,會提前構造好對應的 Class,所以這裏直接使用
if (UNLIKELY(!init_done_)) {
// finish up init of hand crafted class_roots_
if (strcmp(descriptor, "Ljava/lang/Object;") == 0) {
klass.Assign(GetClassRoot(kJavaLangObject));
} else if (strcmp(descriptor, "Ljava/lang/Class;") == 0) {
klass.Assign(GetClassRoot(kJavaLangClass));
} else if (strcmp(descriptor, "Ljava/lang/String;") == 0) {
klass.Assign(GetClassRoot(kJavaLangString));
} else if (strcmp(descriptor, "Ljava/lang/ref/Reference;") == 0) {
klass.Assign(GetClassRoot(kJavaLangRefReference));
} else if (strcmp(descriptor, "Ljava/lang/DexCache;") == 0) {
klass.Assign(GetClassRoot(kJavaLangDexCache));
} else if (strcmp(descriptor, "Ldalvik/system/ClassExt;") == 0) {
klass.Assign(GetClassRoot(kDalvikSystemClassExt));
}
}
if (klass == nullptr) {
// 創建其他類實例
klass.Assign(AllocClass(self, SizeOfClassWithoutEmbeddedTables(dex_file, dex_class_def)));
}
// 設置對應的 DEX 緩存
klass->SetDexCache(dex_cache);
// 設置 Class 的一些屬性,包括 ClassLoader,訪問修飾符,Class 在 DEX 中對應的 index 等等
SetupClass(*new_dex_file, *new_class_def, klass, class_loader.Get());
// 把 Class 插入 ClassLoader 的 class_table 中做一個緩存
ObjPtr<mirror::Class> existing = InsertClass(descriptor, klass.Get(), hash);
// 加載類屬性
LoadClass(self, *new_dex_file, *new_class_def, klass);
// 加載父類
if (!LoadSuperAndInterfaces(klass, *new_dex_file)) {
// 加載失敗的處理
}
if (!LinkClass(self, descriptor, klass, interfaces, &h_new_class)) {
// 連接失敗的處理
}
// ...
return h_new_class.Get();
}
從上面 DefineClass 的代碼裏我們可以看到,加載分爲幾個步驟:
1.創建類實例
2.設置 Class 訪問修飾符,ClassLoader
等一些屬性
3.加載類成員LoadClass
4.加載父類和接口LoadSuperAndInterfaces
5.連接LinkClass
下面我們主要看下後面加載類成員,加載父類,連接這三個步驟。
3.3 加載類成員 -- LoadClass
加載類成員這一過程,主要有下面幾個步驟:
加載靜態變量
加載實例變量
加載方法,分爲虛方法和非虛方法
由於這裏代碼比較長,我們分段來看。
3.3.1 加載靜態變量
// class_linker.cc
void ClassLinker::LoadClassMembers(Thread* self,
const DexFile& dex_file,
const uint8_t* class_data,
Handle<mirror::Class> klass) {
{
// Load static fields.
// 獲取 DEX 文件中的變量迭代器
ClassDataItemIterator it(dex_file, class_data);
LengthPrefixedArray<ArtField>* sfields = AllocArtFieldArray(self,
allocator,
it.NumStaticFields());
// ...
// 遍歷靜態變量
for (; it.HasNextStaticField(); it.Next()) {
// ...
LoadField(it, klass, &sfields->At(num_sfields));
}
// ...
klass->SetSFieldsPtr(sfields);
}
}
// 加載變量,設置變量 Class 以及訪問修飾符
void ClassLinker::LoadField(const ClassDataItemIterator& it,
Handle<mirror::Class> klass,
ArtField* dst) {
const uint32_t field_idx = it.GetMemberIndex();
dst->SetDexFieldIndex(field_idx);
dst->SetDeclaringClass(klass.Get());
dst->SetAccessFlags(it.GetFieldAccessFlags());
}
加載靜態變量時,取出 DEX 文件中對應的 Class 數據,遍歷其中的靜態變量,設置給 Class#sfield_
變量。
3.3.2 加載實例變量
加載實例變量和加載靜態變量是類似的,這裏不做過多的解讀了
void ClassLinker::LoadClassMembers(Thread* self,
const DexFile& dex_file,
const uint8_t* class_data,
Handle<mirror::Class> klass) {
{
// Load instance fields.
LengthPrefixedArray<ArtField>* ifields = AllocArtFieldArray(self,
allocator,
it.NumInstanceFields());
for (; it.HasNextInstanceField(); it.Next()) {
LoadField(it, klass, &ifields->At(num_ifields));
}
// ...
klass->SetIFieldsPtr(ifields);
}
}
3.3.3 加載方法
void ClassLinker::LoadClassMembers(Thread* self,
const DexFile& dex_file,
const uint8_t* class_data,
Handle<mirror::Class> klass) {
{
for (size_t i = 0; it.HasNextDirectMethod(); i++, it.Next()) {
ArtMethod* method = klass->GetDirectMethodUnchecked(i, image_pointer_size_);
LoadMethod(dex_file, it, klass, method);
LinkCode(this, method, oat_class_ptr, class_def_method_index);
// ...
}
for (size_t i = 0; it.HasNextVirtualMethod(); i++, it.Next()) {
ArtMethod* method = klass->GetVirtualMethodUnchecked(i, image_pointer_size_);
LoadMethod(dex_file, it, klass, method);
LinkCode(this, method, oat_class_ptr, class_def_method_index);
// ...
}
}
}
加載方法時分爲兩個步驟,LoadMethod
和 LinkCode
。
void ClassLinker::LoadMethod(const DexFile& dex_file,
const ClassDataItemIterator& it,
Handle<mirror::Class> klass,
ArtMethod* dst) {
// ...
dst->SetDexMethodIndex(dex_method_idx);
dst->SetDeclaringClass(klass.Get());
dst->SetCodeItemOffset(it.GetMethodCodeItemOffset());
dst->SetDexCacheResolvedMethods(klass->GetDexCache()->GetResolvedMethods(), image_pointer_size_);
uint32_t access_flags = it.GetMethodAccessFlags();
// ...
dst->SetAccessFlags(access_flags);
}
LoadMethod 主要是給 ArtMethod 設置訪問修飾符等屬性。
LinkCode 這一步驟,可以理解爲是給 ArtMethod 設置方法入口,即從其他方法如何跳轉到此方法進行執行。這裏也分爲了幾種情況:
- 如果此方法已經通過 OAT 編譯成了本地機器指令,那麼這裏會將入口設置爲跳轉到本地機器指令執行
- 如果是靜態方法,設置跳板方法,此時不會具體指定方法如何執行,後面會在
ClassLinker::InitializeClass
裏被ClassLinker::FixupStaticTrampolines
替換掉 - 如果是 Native 方法,入口設置爲跳轉到 JNI 動態連接的方法中
- 如果是解釋模式,入口設置爲跳轉到解釋器中
static void LinkCode(ClassLinker* class_linker,
ArtMethod* method,
const OatFile::OatClass* oat_class,
uint32_t class_def_method_index) REQUIRES_SHARED(Locks::mutator_lock_) {
if (oat_class != nullptr) {
// 判斷方法是否已經被 OAT
const OatFile::OatMethod oat_method = oat_class->GetOatMethod(class_def_method_index);
oat_method.LinkMethod(method);
}
// Install entry point from interpreter.
const void* quick_code = method->GetEntryPointFromQuickCompiledCode();
bool enter_interpreter = class_linker->ShouldUseInterpreterEntrypoint(method, quick_code);
if (method->IsStatic() && !method->IsConstructor()) {
// 對於靜態方法,後面會在 ClassLinker::InitializeClass 裏被 ClassLinker::FixupStaticTrampolines 替換掉
method->SetEntryPointFromQuickCompiledCode(GetQuickResolutionStub());
} else if (quick_code == nullptr && method->IsNative()) {
// Native 方法跳轉到 JNI
method->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());
} else if (enter_interpreter) {
// 解釋模式,跳轉到解釋器
method->SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());
}
// ...
}
這就是解析方法的主要過程,關於方法的調用,其實還比較複雜,如果大家感興趣,後面可以再專門說說。
3.4 加載父類和接口 -- LoadSuperAndInterfaces
自身類成員加載完成後,就去加載父類。加載父類調用的是 LoadSuperAndInterfaces
,主要代碼如下:
bool ClassLinker::LoadSuperAndInterfaces(Handle<mirror::Class> klass, const DexFile& dex_file) {
// 加載父類
ObjPtr<mirror::Class> super_class = ResolveType(dex_file, super_class_idx, klass.Get());
// 檢查父類可見性
if (!klass->CanAccess(super_class)) {
// ...
}
// 設置父類
klass->SetSuperClass(super_class);
// 加載接口
const DexFile::TypeList* interfaces = dex_file.GetInterfacesList(class_def);
for (size_t i = 0; i < interfaces->Size(); i++) {
ObjPtr<mirror::Class> interface = ResolveType(dex_file, idx, klass.Get());
// ...
// 檢查接口可見性
if (!klass->CanAccess(interface)) {
}
}
// 此時說明類已經加載完畢了
mirror::Class::SetStatus(klass, mirror::Class::kStatusLoaded, nullptr);
}
加載父類和接口都是通過 ResolveType
來的,ResolveType
中又是調用了 ClassLinker#FindClass -> ClassLinker#DefineClass
來的,於是加載父類的流程又回到了我們本小結開頭。
就這樣遞歸加載下去,直到父類全部加載完成,也就標識着類自身也加載完成了。
3.5 連接 -- LinkClass
之後就是 LinkClass
,這裏步驟比較清晰,我們先看一下主要代碼:
bool ClassLinker::LinkClass(Thread* self,
const char* descriptor,
Handle<mirror::Class> klass,
Handle<mirror::ObjectArray<mirror::Class>> interfaces,
MutableHandle<mirror::Class>* h_new_class_out) {
if (!LinkSuperClass(klass)) {
return false;
}
// ...
if (!LinkMethods(self, klass, interfaces, &new_conflict, imt_data)) {
return false;
}
if (!LinkInstanceFields(self, klass)) {
return false;
}
size_t class_size;
if (!LinkStaticFields(self, klass, &class_size)) {
return false;
}
// ...
return true;
}
從主要代碼中可以看到,主要有四個步驟:
1.LinkSuperClass
2.LinkMethods
3.LinkInstanceFields
4.LinkStaticFields
3.5.1 LinkSuperClass
這裏主要是對父類權限做了一下檢查,包括是否是 final,是否對子類可見(父類爲 public 或者同包名),以及繼承父類一些屬性(包括是否有 finalize 方法,ClassFlags 等等)。
bool ClassLinker::LinkSuperClass(Handle<mirror::Class> klass) {
ObjPtr<mirror::Class> super = klass->GetSuperClass();
//
// Verify
if (super->IsFinal() || super->IsInterface()) {
}
if (!klass->CanAccess(super)) {
}
if (super->IsFinalizable()) {
klass->SetFinalizable();
}
if (super->IsClassLoaderClass()) {
klass->SetClassLoaderClass();
}
uint32_t reference_flags = (super->GetClassFlags() & mirror::kClassFlagReference);
klass->SetClassFlags(klass->GetClassFlags() | reference_flags);
return true;
}
3.5.2 LinkMethods
LinkMethods
主要做的事情是填充 vtable
和 itable
。主要通過 SetupInterfaceLookupTable,LinkVirtualMethods,LinkInterfaceMethods
三個方法來進行的.
bool ClassLinker::LinkMethods(Thread* self,
Handle<mirror::Class> klass,
Handle<mirror::ObjectArray<mirror::Class>> interfaces,
bool* out_new_conflict,
ArtMethod** out_imt) {
// ...
return SetupInterfaceLookupTable(self, klass, interfaces)
&& LinkVirtualMethods(self, klass, /*out*/ &default_translations)
&& LinkInterfaceMethods(self, klass, default_translations, out_new_conflict, out_imt);
}
SetupInterfaceLookupTable
用來填充 iftable_
,就是上面說到保存接口的地方。iftable_
對應的是 IfTable
類。IfTable
類結構如下:
class MANAGED IfTable FINAL : public ObjectArray<Object> {
enum {
// Points to the interface class.
kInterface = 0,
// Method pointers into the vtable, allow fast map from interface method index to concrete
// instance method.
kMethodArray = 1,
kMax = 2,
};
}
其中 kInterface
指向 Interface 的 Class 對象,kMethodArray
指向的是 vtable
,通過此變量可以方便的找到接口方法的實現。
LinkVirtualMethods
和 LinkInterfaceMethods
會填充 vtable_
,這裏具體的代碼很長,我們暫且不分析(這裏具體流程對於理解本文主旨其實影響不大),有兩個重要的過程是:
1.首先會拷貝父類的
vtable
到當前類的vtable
2.如果類中覆蓋了父類的抽象方法,就在vtable
中替換掉父類的方法
通過上面兩個過程,我們可以知道,vtable
中保存的就是真正方法的實現,也就是 Java 中多態的實現原理。
3.5.3 LinkInstanceFields & LinkStaticFields
這裏的兩個方法最終都調用了 LinkFields
方法裏做了兩件事情:
1.爲了對齊內存,對 fields 進行排序
2.計算 Class 大小
其中 fields 排序規則如下:
引用類型 -> long (64-bit) -> double (64-bit) -> int (32-bit) -> float (32-bit) -> char (16-bit) -> short (16-bit) -> boolean (8-bit) -> byte (8-bit)
總結
通過上面的分析,我們知道了一個 Java 類在 Android ART 中的真實形態,也對 ART 中類加載的過程做了一些簡單的分析。
其實在寫這篇文章的時候,裏面有一些知識點也會有些疑問,如果大家有任何想法,歡迎討論~
最後用文章開始的圖總結一下,回顧一下 ART 中類的全貌。
視頻鏈接:
Android虛擬機Java對象初始化原理,Class之間的關係
鏈接: https://pan.baidu.com/s/1AisNpQGWVrCHWaN-gkJwjg 提取碼: d4gf更多面試內容,面試專題,flutter視頻 全套,音視頻從0到高手開發。
關注GitHub:https://github.com/xiangjiana/Android-MS
免費獲取面試PDF合集
免費提供簡歷修改建議,獲取大廠面試PDF和視頻教程