前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM | 基于openJDK源码深度拆解Java虚拟机

JVM | 基于openJDK源码深度拆解Java虚拟机

原创
作者头像
kfaino
发布2023-10-08 12:53:37
9942
发布2023-10-08 12:53:37
举报
文章被收录于专栏:程序员的日常

引言

在上一篇文章中,我通过探讨类的生命周期,为你详细解析了类在加载进JVM时的全过程。当然,这仅仅只是JVM虚拟机的冰山一角,像执行引擎的动态编译、垃圾回收系统的内存管理、本地方法接口的与本地库的交互,以及本地方法库的结构和功能等诸多核心内容还未涉及。

本篇文章将为你展开JVM的完整画卷,不仅深入探索上述的组成部分,还将整个系统之间的关系和交互机制进行完整梳理,让我们开始吧!


堆中的对象

在进一步讲解JVM虚拟机之前,我想继续探讨一下上篇的主角——对象,并将分析延展得更深入一些。 我们来回顾下:上篇文章中我们讨论了,在类完成初始化并开始实例化的时候,JVM会为我们分配一个Building对象。你看:

在这个过程中,除了初始化数据,还会创建对象头。对象头是什么?它包含了哪些信息?除了对象头,对象内存结构中还隐藏了哪些内容?这些内容又如何影响对象的访问和操作呢?我们来深入分析下。

对象内存结构

对象的内存结构由对象头、实例数据、对齐填充组成;我把上面的Building实例对象放大,你看:

接下来我们一个一个分析。

对象头
  • Mark Word:存储对象的锁信息、哈希码、垃圾收集状态等。
  • Klass Pointer:指向对象所属类的元数据的指针,可以访问类的方法、字段信息等。
  • 数组长度(如果是数组对象):如果对象是数组,则此字段存储数组的长度。
实例数据
  • 字段:对象的所有字段值都存储在这里,包括原始类型字段和引用类型字段。
对齐填充
  • 填充字节:添加一些额外的字节,使对象进行对齐,64位的操作系统对象大小应为8的倍数。

看到对象

我们可以用jol工具(JVM对象布局的工具)来看到它们的内存占用情况。我们来看下如何使用:

首先在pom.xml引入依赖:

代码语言:java
复制
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.14</version>
        </dependency>

执行如下代码:

代码语言:java
复制
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

以JDK8,默认开启压缩指针的情况下,我们可以看到这个结果:

代码语言:java
复制
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Disconnected from the target VM, address: '127.0.0.1:9689', transport: 'socket'

我们在上面简单的创建了一个Object对象;其中8字节为MarkWord,另外4个字节为KlassPointer,为了使其对齐为8的倍数,最后4字节为对齐填充数据。


对象与JVM的关系

对象的内存结构是JVM中的一个核心概念。它连接了许多JVM的组件,例如类加载器执行引擎垃圾收集器等,并影响了对象创建、访问和管理的性能。了解对象的内存结构有助于深入理解Java程序的行为。结合前面几篇文章,我们把对象的生命周期串起来:

类加载:当首次访问一个类时(例如通过new关键字创建实例),JVM会将该类的字节码加载到内存中。这一过程由类加载子系统完成,并包括了加载、链接(验证、准备和解析)和初始化三个主要阶段。

对象实例化:使用new关键字创建对象时,会先在堆中为该对象分配内存空间,并进行零值初始化。然后会设置对象头信息(包括类的元数据指针、哈希码等)。之后,JVM会调用对象的构造函数<init>进行字段等的初始化。

方法调用:对象的方法调用涉及执行引擎。执行引擎会解释或通过JIT编译器将字节码转换为本地代码执行。是JVM的核心部分,也是实现Java的跨平台特性的关键。

垃圾回收:当对象不再被引用时,垃圾收集器会回收这些对象的内存空间。这是JVM自动管理内存的方式,可以自动回收不再使用的内存。

本地方法调用:如果Java代码需要调用本地(例如C或C++编写的)方法,可以通过Java Native Interface(JNI)实现。这是Java与本地代码进行交互的标准机制。


JVM虚拟机全览

基于上面的完整流程,我画了一张图:

我在图中为你标注序号,接下来,我们来分析下:

① 类加载器子系统与元空间的连接

  • 箭头含义:类加载器负责将类文件加载到JVM中,类的结构信息被存储在元空间中。
  • 具体作用:元空间存储了类的元数据,如类名、访问修饰符、字段、方法等。当类加载器加载类时,它将这些信息存入元空间。

② 执行引擎与运行时数据区的连接

  • 箭头含义:执行引擎负责执行字节码,其操作涉及到运行时数据区的多个部分。
  • 具体作用:执行引擎从程序计数器中获取要执行的字节码指令地址,操作虚拟机栈来执行Java方法,与堆进行交互以操作对象实例等。

③ 执行引擎与本地方法库的连接

  • 箭头含义:执行引擎可以调用本地方法库中的本地方法。
  • 具体作用:对于使用native关键字标记的方法,执行引擎会调用本地方法库中的相应实现。

④ Java Native Interface(JNI)与本地方法库的连接

  • 箭头含义:JNI允许Java代码与本地代码进行交互。
  • 具体作用:通过JNI,Java代码可以调用本地方法库中的方法,并且本地代码也可以调用Java代码中的方法。

⑤ 垃圾回收系统与堆的连接

  • 箭头含义:垃圾回收系统负责管理和回收堆内存。
  • 具体作用:垃圾回收系统定期检查堆中的对象,确定哪些对象不再被引用并可以安全回收。

完整的画卷已经平铺其上并勾勒出路线图,我们再深入源码再进一步探索其中奥妙

基于源码分析JVM虚拟机

我所查看的openJDK源码是 jdk8-b120 分支的源码,如果想进一步探索其中结构,可以将其下载到本地。好,我们开始吧!

类加载器

类只有加载进内存中,才能工作。而揽起加载类的重任就由类加载器(ClassLoader)来完成。我用三篇文章来向你介绍,足见其中重要。理所应当的,分析JVM虚拟机源码就不能脱离类加载器。

当一个新的类加载器被创建并开始加载类时,系统会为其分配一个新的ClassLoaderData实例。来,我们源码说话:

  • 文件位置:src/hotspot/share/classfile/classLoader.cpp
  • 代码位置:
代码语言:c++
复制
InstanceKlass* ClassLoader::load_class(Symbol* name, bool search_append_only, TRAPS) {
	//...省略
	// 检查类是否需要进行字节码验证。
	stream->set_verify(ClassLoaderExt::should_verify(classpath_index));
	// 创建一个空的ClassLoaderData
	ClassLoaderData* loader_data = ClassLoaderData::the_null_class_loader_data();
	// 代码安全相关
	Handle protection_domain;
	// 准备类加载信息
	ClassLoadInfo cl_info(protection_domain);
	// 从流中创建对象,返回InstanceKlass示例类引用
	InstanceKlass* result = KlassFactory::create_from_stream(stream,
                                                           name,
                                                           loader_data,
                                                           cl_info,
                                                           CHECK_NULL);
	result->set_classpath_index(classpath_index);
	// 返回类示例
	return result;
}

我列举了一些关键代码,你可以看到,类在加载的时候确实创建了一个空的ClassLoaderData。这个结构非常重要,我们来分析下。

ClassLoaderData

这个类是在C的堆上分配的class ClassLoaderData : public CHeapObj<mtClass>,我们简单过一下头文件,发现一些有意思的结构:

代码语言:c++
复制
  // 类加载器关联的元空间
  ClassLoaderMetaspace * volatile _metaspace;
  // 类加载的对象句柄,持有管理Java对象
  OopHandle  _class_loader;
  Klass*  _class_loader_klass;
  Symbol* _name;
  // 提供一个可以用于遍历所有类加载器的结构,看来底层是使用链表来组织
  void set_next(ClassLoaderData* next);
  ClassLoaderData* next() const;

看完上面的代码以及注释,我们继续。

你可以看到元空间引用,当然,这也是情理之中。我们需要有个空间来存储类元数据。

你还记得有哪些数据被存放于元空间吗?我们接着往下看


元空间

对象创建除了和堆产生直接的联系,和元空间之间的若有若无的关系总是让人难以捉摸。我们简单的通过类加载源码发现它的踪迹。接下来,我将从源码的角度深入为你分析元空间结构,以加深对其的印象。

我们回忆一下,我在前几篇文章中提到,类加载到对象创建的过程中有一些内容要被放入元空间中, 网上的说法五花八门,我们来看看源码中是怎么定义的,既然是元空间的内容自然少不了要继承自MetaspaceObj,我们按图索骥,有如下几个结构:

代码语言:c++
复制
//类的元数据
metaData
// 常量方法,进一步解读就是不可变的方法,里面包含一些字节码等等结构。
constMethod
// 常量池缓存,可以说是常量池的进阶版了,或者说是运行时常量池。
cpCache
// 记录类型的组件
recordComponent
// 符号,一种特殊的字符串类型,用来记录一些名称,后面会讲到
symbol
// 和CDS有关,这里就不讨论了。
filemap
// 注解相关的东西
annotations
// 数组类
array

我们一一对应下:

  1. 类的元数据:类的名称、父类、实现的接口、方法信息、字段信息等,也包括 静态变量常量池
  2. 字节码
  3. 常量池:类文件中的字面量符号引用等内容,它也属于类的元数据。
  4. 运行时常量池:这是一个在类加载到内存后Java虚拟机为它们分配的一个动态结构,。

总结一下,其实元空间包括这三类:类的元数据字节码运行时常量池

好,趁热打铁,我们来分析下类的元数据


类的元数据

Klass

文件位置:src/hotspot/share/oops/klass.hpp

代码结构:

代码语言:java
复制
class Klass : public Metadata {
 protected:
  // 超类指针,非常关键;用于确认继承,具体调用哪个版本的类,类型检查(instanceof)方法等。
  Klass* _super;          
  // 类加载器数据,每个类加载器都有其自己的命名空间,这意味着不同的类加载器可以加载名字相同但内容不同的类。这个指针让JVM可以追踪哪个类加载器加载特定的Klass。
  ClassLoaderData* _class_loader_data;  
  const KlassKind _kind;
  // 符号引用名
  Symbol*     _name;
  OopHandle   _java_mirror;
  int _vtable_len;
  AccessFlags _access_flags;
  // ... (其他成员)
};

看到_class_loader_data 是不是有一种恍然大悟的感觉?我在 基于类加载器的完全实践 中提到命名空间的概念,并通过一个例子告诉你,两个类加载器加载的同名类对象obj1不等于obj2。其底层是两个类加载器拥有不同的类加载数据,或者说是不同的元空间

InstanceKlass

Klass只是一个基类,以Building类为例。它在元空间中是InstanceKlass,我们来分析下这个结构:

代码语言:c++
复制
  // 注解信息
  Annotations*    _annotations;
  // 包信息
  PackageEntry*   _package_entry;
  // 生成的数组类型
  ObjArrayKlass* volatile _array_klasses;
  // 内部类
  Array<jushort>* _inner_classes;
  // 常量池
  ConstantPool* _constants;
  // 类的状态,例如这个类初始化完成状态,或者未被初始化;
  volatile ClassState _init_state;          // state of class
  // 引用类型,软引用,弱引用等。
  u1              _reference_type;          // reference type
  // 各种标志位
  InstanceKlassFlags _misc_flags;
  // 监视器
  Monitor*             _init_monitor;       // mutual exclusion to _init_state and _init_thread.
  // 当前线程
  JavaThread* volatile _init_thread;        // Pointer to current thread doing initialization (to handle recursive initialization)

我把一些重要的结构列举出来了, 你会发现当你知道类的底层结构后,一些概念会变得非常清晰。接下来,我会把一些重要的结构详细为你讲解:

  1. _reference_type:这个成员变量实际上是用来跟踪类实例的引用类型。为垃圾回收提供依据。如果你不想要让这个对象存活时间太长,可以使用弱引用, 在下次GC时把垃圾进行回收。
  2. _init_state: 要判断这个类是否初始化完成,可以根据这个成员变量进行判断。
  3. _init_monitor:用来保证在一个类加载器下多线程不会执行多次<clint>静态初始化方法。
  4. _init_thread: 用来辅助保证静态初始化方法只能有一个线程执行一次。在注释中doing initialization (to handle recursive initialization) 也明确说明,它是为了处理递归初始化。我们考虑这样一个场景,一个类的静态初始化器调用了另一个方法,而这个方法又触发了该类的主动使用。这会再次尝试初始化同一个类。_init_thread字段可以帮助检测这种递归初始化,并确保不会尝试重新初始化同一个类。

常量池 VS 运行时常量池

有些人可能会混淆这两个概念,我在这里解释一下:

  1. 我们在表述某一特定的常量池时,往往会省略定语。我认为表述成某某类的常量池,更加洽当一些。例如:Building类的常量池。
  2. 常量池和运行时常量池在底层指的是同一种数据结构。它的区别在于省略的定语是否处于使用或者运行的状态。我们在前面已经说过。当我们的字节码文件加载链接时会产生符号引用。而在类被使用的时候则会产生直接引用。这就是简单区分两者异同所在。

虽然我在这里把它们放在一起讨论,但是在底层结构中,常量池属于元数据。而运行时常量池则属于元空间。这两个类心虽相同,但奈何职责不同。

接下来,我们通过源码来深入分析常量池。

  • 文件位置:src/share/vm/oops/constantPool.hppclass ConstantPool : public Metadata { private: // 常量池条目的数量 int _length; // 指向持有这个常量池的类的指针(属于这个实例类的常量池) InstanceKlass* _pool_holder; // 常量池缓存 ConstantPoolCache* _cache; // the cache holding interpreter runtime information // ... (其他成员) };常量池条目放在哪里呢?在JVM中常量池条目用cp_info 表示,全局搜索代码发现它只看到Java的实现。当然并不妨碍理解,部分代码如下:
  • 代码结构:
代码语言:java
复制
for(ci = 1; ci < len; ci++) {
          int cpConstType = tags.at(ci);
          // write cp_info
          // write constant type
          switch(cpConstType) {
              case JVM_CONSTANT_Utf8: {
				  // ...
                  break;
              }

              case JVM_CONSTANT_Unicode:
                  throw new IllegalArgumentException("Unicode constant!");

              case JVM_CONSTANT_Integer:
				  // ...
                  break;

              case JVM_CONSTANT_Float:
				  // ...
                  break;

              case JVM_CONSTANT_Long: {
				  // ...
                  break;
              }

              case JVM_CONSTANT_Double:
				  // ...
                  break;

              case JVM_CONSTANT_Class: {
				  // ...
                  break;
              }

              // case JVM_CONSTANT_ClassIndex:
              case JVM_CONSTANT_UnresolvedClassInError:
              case JVM_CONSTANT_UnresolvedClass: {
				  // ...
                  break;
              }

              case JVM_CONSTANT_String: {
				  // ...
                  break;
              }

              // all external, internal method/field references
              case JVM_CONSTANT_Fieldref:
              case JVM_CONSTANT_Methodref:
              case JVM_CONSTANT_InterfaceMethodref: {
				  // ...
                  break;
              }

              case JVM_CONSTANT_NameAndType: {
				  // ...
                  break;
              }

              case JVM_CONSTANT_MethodHandle: {
				  // ...
                  break;
              }

              case JVM_CONSTANT_MethodType: {
				  // ...
                  break;
              }

              case JVM_CONSTANT_InvokeDynamic: {
				  // ...
                  break;
              }

              default:
                  throw new InternalError("Unknown tag: " + cpConstType);
          } // switch
      }

这些条目也可以借助插件,例如:jclassLib来看到其中条目。我在文章后面也有介绍。接下来我们看下运行时常量池的结构。

  • 文件位置:src/hotspot/share/oops/cpCache.hpp // 条目长度 int _length; // 常量池引用 ConstantPool* _constant_pool; // 解析过的符号引用句柄 OopHandle _resolved_references; // 映射结构,用于跟踪被解析的引用 Array<u2>* _reference_map; // 对于动态类型语言的支持,显然不是为Java准备的,像Groovy和Ruby支持动态类型语言 Array<ResolvedIndyEntry>* _resolved_indy_entries; // 已经解析的字段引用条目 Array<ResolvedFieldEntry>* _resolved_field_entries;这次,我们的直接引用是存储在源码同文件中的ConstantPoolCacheEntry类结构中。
  • 代码结构:

设计常量池

符号引用延迟解析策略

符号引用解析往往比较耗时,我们可以采用懒加载机制。当类被加载,但是还未被使用的时候,可以延迟加载。符号引用在第一次使用时被解析,并缓存解析结果。

使用缓存思想:分离的符号引用和直接引用

看过源码才知道其实直接引用并不在常量池中,而是在常量池缓存cpCache中。通过结构_resolved_references 来关联其解析的引用。它是一个运行时的数据结构,可以说它是ConstantPool的“缓存”版本。但是缓存并不能让它变得更快,它只是在代码层面做的“缓存”,我们可以通过代码了解它的思想。

为了加深你理解,我画了一张图:

这是对象创建中获取方法引用的图,你可以结合源码进行体会。

看到常量池

我们可以使用javap指令和插件jclassLib看到静态的常量池。后者只需要在IDE中安装插件即可查看。效果如下:

如果你想要安装该插件可以查看网上的相关教程,这里就不赘述了。假如我想看Building类的详细信息,可以在console端,输入如下命令:

代码语言:java
复制
// 在当前目录下的Building.class
javap -verbose .\Building.class

输出内容如下:

代码语言:java
复制
	// ...省略
Constant pool:
   #1 = Methodref          #17.#54        // java/lang/Object."<init>":()V
   #2 = Fieldref           #55.#56        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #57            // 建筑蓝图已被创建!
   #4 = Methodref          #58.#59        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Fieldref           #7.#60         // org/kfaino/webTemplate/jvm/Building.floorCount:I
   #6 = Fieldref           #7.#61         // org/kfaino/webTemplate/jvm/Building.constructionYear:I
   #7 = Class              #62            // org/kfaino/webTemplate/jvm/Building
   #8 = Methodref          #7.#54         // org/kfaino/webTemplate/jvm/Building."<init>":()V
   #9 = Class              #63            // java/lang/StringBuilder
  #10 = Methodref          #9.#54         // java/lang/StringBuilder."<init>":()V
  #11 = String             #64            // Building2{floorCount=
  #12 = Methodref          #9.#65         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #13 = Methodref          #9.#66         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #14 = Methodref          #9.#67         // java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
  #15 = Methodref          #9.#68         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #16 = Methodref          #17.#69        // java/lang/Object.getClass:()Ljava/lang/Class;
  #17 = Class              #70            // java/lang/Object
  // ...省略

文中重要部分解析

元数据和元空间

"元"(Meta)在许多上下文中是一个前缀,通常意味着“超越”或“更高级别”。当我们在计算机和信息科技领域讨论“元”时,我们通常是在讨论关于数据的数据或关于结构的结构

接下来,我为你解释这两个关键名词:

  1. 元数据(Metadata)
    • 元数据是关于数据的数据。它描述了数据的结构、含义、来源和其他与数据相关的信息。例如,一张照片的元数据可能包括拍摄日期、相机型号、曝光设置等。
    • 类的元数据描述了类的结构,包括它的方法、字段、父类等。
  2. 元空间(Metaspace)
    • 在Java中,元空间是OpenJDK 8引入的,用于替代之前版本中的永久代(PermGen)。元空间的目标是存储JVM加载的类定义的元数据
    • 元空间的名字意味着这是一个“关于空间的空间”。在这个情境下,它存储的是类定义,而类定义本身定义了对象在Java堆中的布局和行为。

对象头中的klass指针

你会发现我在介绍对象结构的时候有提到 Klass Pointer ,其中有何玄机?很简单,告诉JVM这个对象是哪个类加载器加载,元数据从哪里取,用于快速关联的埋点。

站在设计者的角度,我们思考它的优点:

  • 效率:JVM可以迅速知道这个对象是哪个类的实例,对方法调用类型检查反射等操作非常之关键。
  • 节省空间: 相同类的实例共享同一个Klass结构。而不是挤在堆内存中。

弱引用的应用

弱引用的目的是在内存紧张的情况下。不希望一些对象的存活时间过长,而在下一次垃圾回收时被回收。我们看下如何使用:

代码语言:java
复制
Map<WeakReference<Key>, Value> cache = new HashMap<>();

上面只是一个简单的示例,我们想象一下这样的场景: 当有一个资源被释放后,需要在释放动作之后做一些清理工作。你可能会想到用finalize 。但是通常并不建议你这么做。因为可能会导致不可预测的延迟。我们可以借助ReferenceQueue 来实现,代码如下:

代码语言:java
复制
class Resource {
    private String id;

    public Resource(String id) {
        this.id = id;
    }
}

public class WeakReferenceWithQueueDemo {
    public static void main(String[] args) throws InterruptedException {
//        WeakHashMap<Object, Object> objectObjectWeakHashMap = new WeakHashMap<>();
        ReferenceQueue<Resource> referenceQueue = new ReferenceQueue<>();
        Map<WeakReference<Resource>, String> weakReferences = new HashMap<>();

        Resource resource = new Resource("RESOURCE_1");
        WeakReference<Resource> weakRef = new WeakReference<>(resource, referenceQueue);
        weakReferences.put(weakRef, "RESOURCE_1");
        // 清空强引用,只保留弱引用(试试把这里注释,你就看不到后面的打印语句了)
        resource = null;
        System.gc();

        Thread.sleep(1000);

        Reference<? extends Resource> removed;
        // 检查ReferenceQueue
        while ((removed = referenceQueue.poll()) != null) {
            String id = weakReferences.remove(removed);
            if (id != null) {
                System.out.println("Resource with ID: " + id + " 被垃圾回收了,我们来做一些额外的清理工作....");
            }
        }
    }
}

执行结果如下:

代码语言:java
复制
Resource with ID: RESOURCE_1 被垃圾回收了,我们来做一些额外的清理工作....
Process finished with exit code 0

这个引用队列确实捕获到资源被释放的事件。


常见面试题

详细描述Java对象在堆中的内存结构,包括对象头和实例数据的内容
你了解JVM虚拟机吗,它包含哪些部分?
描述Java的常量池。它存储了哪些信息?
什么是弱引用,以及它的用途是什么?

总结

本篇完毕,我们来回顾下:在Java中,一切皆为对象。所以我们从对象出发,探索对象的内存结构。通过其设计的结构关联到JVM虚拟机的其它组件。一步步的解构这个JVM系统,最终掌握完整的JVM虚拟机。希望以上文章对你有所启发,感谢阅读。

参考文献

  1. 《深入解析java虚拟机hotspot》
  2. 《揭秘Java虚拟机-JVM设计原理与实现》
  3. 《深入理解Java虚拟机:JVM高级特性与最佳实践》
  4. jvm中类和对象定义存储基础知识
  5. 消失的死锁:从 JSF 线程池满到 JVM 初始化原理剖析
  6. 图解 JVM 内存模型及 JAVA 程序运行原理

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 堆中的对象
    • 对象内存结构
      • 看到对象
        • 对象与JVM的关系
        • JVM虚拟机全览
          • ① 类加载器子系统与元空间的连接
            • ② 执行引擎与运行时数据区的连接
              • ③ 执行引擎与本地方法库的连接
                • ④ Java Native Interface(JNI)与本地方法库的连接
                  • ⑤ 垃圾回收系统与堆的连接
                  • 基于源码分析JVM虚拟机
                    • 类加载器
                      • ClassLoaderData
                    • 元空间
                      • 类的元数据
                      • 常量池 VS 运行时常量池
                      • 设计常量池
                      • 看到常量池
                      • 元数据和元空间
                      • 对象头中的klass指针
                      • 弱引用的应用
                  • 文中重要部分解析
                  • 常见面试题
                  • 总结
                  • 参考文献
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档