专栏名称: Java知音
专注于Java,推送技术文章,热门开源项目等。致力打造一个有实用,有情怀的Java技术公众号!
目录
相关文章推荐
新闻广角  ·  正式批复!厦门新机场名字定了! ·  18 小时前  
新闻广角  ·  刚果(金)一监狱囚犯纵火致多人死亡,约440 ... ·  19 小时前  
新闻广角  ·  海底捞回应“招聘985/211学历外送员” ·  21 小时前  
51好读  ›  专栏  ›  Java知音

类和对象在JVM中是如何存储的,竟然有一半人回答不上来!

Java知音  · 公众号  ·  · 2020-11-16 09:45

正文

前言

这篇博客主要来说说类与对象在JVM中是如何存储的,由于JVM是个非常庞大的课题,所以我会把他分成很多章节来细细阐述,具体的数量还没有决定,当然这不重要,重点在于是否可以在文章中学到东西,是否对JVM可以有一些更深的理解,当然这也是笔者自己写文章的初衷。

问题提出

我们在日常工作学习中所使用的Java语言,其最大的特点就是“跨平台”,我们不用在不同的平台上编译两套不同的机器码,而可以做到“ 一次编译,到处运行 ”,其跨平台最重要的一个因素就在于,Java语言并不直接运行在真实机器上,而是有一个虚拟机(即 Java Virtual Machine ,JVM )来承载其运行,我们通过 javac 命令,将 .java 文件编译成为 .class 文件,然后通过虚拟机来编译/解释执行成对应的平台硬编码并执行,使得只要安装了该虚拟机的平台,就可以运行java程序。

实际上,现在不光Java可以运行在Java虚拟机上,还有例如Kotlin、Scala、Groovy、Clojure等语言,都采用了这种模式,编译成为class文件后,放在Java虚拟机上运行,所以笔者预计在很长的一段时间内,即使Java会过时,但是Java虚拟机也会存在较长的一段时间。

那么就从最开始说起,我们写程序时,最先进行的操作一定是新建一个类,然后新建一个对象, 那么类与对象在JVM中是如何存储的呢

如何窥探?

在研究这个问题之前,我们必须要看到类和对象在JVM中是以何种状态存在的,在笔者经过一段时间的学习后,了解了JDK自带的一款“神器”— HSDB ,下面来介绍其基本的一些使用方式。

启动

首先需要需要复制 jdk\jre\bin 目录下的 sawindbg.dl l文件到 jre\bin 目录下,然后进入 jdk\lib 目录下,使用 java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB ,即可启动HSDB:

启动HSDB

然后我们启动一个Java项目,让其保持启动状态:

  public class Blog {
      public static void main(String[] args) {
          System.out.println("Hello JVM");
  
          while(true){}
      }
  }

在终端中使用 jps -l 命令,查看运行起来的Java进程的进程号。

jps查看进程

我这里的进程号是720,获取到进程号之后,点击HSDB上的File->Attach to HotSpot Process,并输入进程号:

HSDBAttach

点击【OK】,即可绑定进程,下图中是这个Java进程中的所有线程。

绑定进程成功

查看类

我们可以通过这个工具,来看一下我们刚才运行的这个类究竟是以何种形式,存在于JVM中的。

点击 Tools -> Class Browser ,然后可以找到Main方法所在类的内存地址,可以看到我创建的类的内存地址是 0x7c0060828

查看类

然后点击 Tools -> Inspector ,在右上方输入内存地址,就可以看到这个类的数据了。

查看类数据

到这里我们已经可以看到,我们所创建的类,其在内存中的存在形式,实际上是使用一个名为 InstanceKlass 的类的实例进行存储的。我们可以得到一个并不是太准确的结论,也算是到目前为止的一个认知, 类在JVM中,是被InstanceKlass所描述的,InstanceKlass中包含类的元数据和方法信息 ,例如:Java类的 继承信息 成员变量 静态变量 成员方法 构造函数 等,JVM可以通过InstanceKlass来反射出Java类的全部结构信息。

查看对象

在HSDB中,我们找到类的内存地址后,通过Inspector可以清楚地看到类在JVM中的一种存在形式。实际上在我们第一次学Java的时候,就听过一句话: 在Java中,万物皆对象 ,在JVM看来,不仅Java对象是对象,Java类也是对象,Java方法也是对象,字节码常量池皆为对象。

由于JVM是由C++编写,所以我们在Java中声明的所有东西,都可以在由C++编写的JVM中以一个对象的方式存在,正如一个Java类是以InstanceKlass的一个实例对象来表示一样,Java对象也可以使用一个C++对象来表示,我们可以来重复一次上述的过程,来看看Java对象是如何在JVM中进行存储的。

首先我们需要修改刚才的测试代码:

  public class Blog {
      public static void main(String[] args) {
          //在Main方法中新建一个对象
          Blog blog = new Blog();
  
          while(true){}
      }
  }

我们在Main方法中新建了一个Blog对象,然后在HSDB中查看这个对象在JVM中是怎样的:

找到创建的对象:

Main线程堆栈内容
找到线程堆栈中对象

可以看到在JVM中,对象是以一个名为Oop的对象来描述的,在Oop对象中,有一个_metadata,代表这个对象的类元数据,其中有一个compressed_klass指针, 指向的正是我们上文中说的,描述类的元信息的InstanceKlass

相信在上面一些小小的测试中,我们应该都有了一些基本的认知。无论是Java中的类,还是对象,在JVM中都是以 对象 的形式存在的,存放类的InstanceKlass对象,保存了类的元数据,例如 父类、方法、成员变量、静态变量 等等,而Oop对象中保存了对象的一些信息,了解过对象的内存分布的同学应该知道一个Java对象中存放有哪些结构,但是这里先卖个关子,这部分内容会在后期文章中单独叙述,还有一个指向类元数据InstanceKlass的指针。现在应该可以理解 万物皆对象 这句话真正的含义了,但如果觉得这就是全部,那就太早了,这其实只是冰山一角,只是开始。

Oop-Klass模型

在上文中我们对Oop和Klass都有了最基本的认识,Oop用于描述对象,Klass用于描述类,而经过笔者更深入的学习中发现,在JVM中,情况绝不止第一节中提到的这么简单。

在JVM中,并没有根据Java实例对象直接通过虚拟机映射到新建的C++对象,而是定义了各种Oop-Klass:

  • Oop(ordinary  object  pointer),用来描述对象实例信息。
  • Klass,用来描述 Java 类,是虚拟机内部Java类型结构的对等体 。

而刚才我们看到的InstanceKlass,实际上只是Klass的一种。

Oop体系

看到Oop,大家第一反应一定是Object-oriented programming(面向对象程序设计),但是这里的Oop,是值Ordinary Object Pointer,即 标准对象指针 ,它用来表示对象的实例信息。

在JVM源码里, oopsHierarchy.hpp 中定义了oop和klass各自的体系,这个是Oop的体系:

  typedef  class oopDesc*                               oop;//所有oops共同基类
  typedef  class   instanceOopDesc*              instanceOop;//Java类实例对象
  typedef  class methodOopDesc*        methodOop;//Java方法对象
  typedef  class constMethodOopDesc*     constMethodOop;//方法中的只读信息对象
  typedef  class methodDataOopDesc*      methodDataOop;//方法性能统计对象
  typedef  class   arrayOopDesc*                    arrayOop;//描述数组
  typedef  class   objArrayOopDesc*              objArrayOop;//描述引用数据类型数组
  typedef  class   typeArrayOopDesc*             typeArrayOop;//描述基本数据类型数组
  typedef  class constantPoolOopDesc*   constantPoolOop;//class文件中的常量池
  typedef  class constantPoolCacheOopDesc*  constantPoolCacheOop;//常量池缓存
  typedef  class klassOopDesc*            klassOop;//指向klass实例
  typedef  class markOopDesc*       markOop;//对象头
  typedef  class compiledICHolderOopDesccompiledICHolderOop;

为了简化变量名,JVM统一将结尾的Desc去掉,以Oop为结尾命名。

在Oop体系中,分别使用不同的Oop来表示不同的对象,在代码的注释中,笔者已经注明了每一种oop分别用于表示什么对象。HotSpot认为用这些模型,便足以描述Java程序的全部内容。

Klass体系

在JVM源码里, oopsHierarchy.hpp 中定义了oop和klass各自的体系,这个是Klass的体系:

  class                        Klass;//klass家族的基类
  class                InstanceKlass;//虚拟机层面与Java类对等的数据结构
  class          InstanceMirrorKlass;//描述java.lang.Class的实例
  class     InstanceClassLoaderKlass;//描述类加载器的实例
  class             InstanceRefKlass;//描述java.lang.Reference的子类
  class                  MethodKlass;//表示Java类中的方法
  class          ConstantMethodKlass;//描述Java类方法所对应的字节码指令信息的固有属性
  class                   KlassKlass;//Klass链路的末端,在Jdk8已不存在
  class               ConstPoolKlass;//描述字节码文件中常量池的属性
  class                   ArrayKlass;//描述数组的信息,是抽象类。
  class                ObjArrayKlass;//ArrayKlass的子类,描述引用类型的数组类元信息
  class               TypeArrayKlass;//ArrayKlass的子类,描述普通配型的数组类元信息

Klass主要提供一下两种能力:

  • klass提供一个与 Java 类对等的 C++类型描述。
  • klass提供虚拟机内部的函数分发机制 。

由于在JVM中,Java类是以Oop和Klass分别进行表示的,所以Klass体系基本和Oop体系相互对应。

或许将两个维度分开,对于我们真正理解这个体系并不是一件好事,因为毕竟这两个体系息息相关,所以笔者在这里只是浅尝辄止地介绍了一下两个体系的成员,接下来我们就以一个最简单的案例来一步步了解Oop-Klass体系,顺便验证我们上文中所说的一些内容。根据上文提到的Oop体系和Klass体系内容,我们分别在Main方法中创建几个对象:

public class Blog {
    private int a = 10;
    private int b = 20;
    public static void main(String[] args) {
        Blog blog = new Blog();
        int[] typeArray = new int[10];
        Integer[] objArray = new Integer[10];
        while(true){}
    }
}

按照我们上文的说法,Klass存储类的元信息,Oop用于描述对象的实例信息,而我们都知道创建一个对象JVM一般分为三步,首先是在 堆中先分配一片内存空间 ,第二步需要完成 对象的初始化 ,最后 将对象的引用指向该内存空间 ,当然这只是比较宏观的一种说法,而落实到细节中,大概是这样一个流程:

1.将Java类 加载到方法区 ,加载到方法区的时候实际上就是创建了一个Klass,Klass中保存了这个Java类的所有信息,例如: 变量、方法、父类、接口、构造方法、属性 等。

2.而在完成对象的 初始化 时,JVM会在堆分配的空间中,创建一个Oop,这个Oop便是我们这个对象实例在内存中的对等体,主要 存储这个对象实例的成员变量 ,其中这个Oop中存在一个指针,指向Klass,通过这个指针,JVM可以在运行期间,获取这个对象的所有类元信息。

看到这里可能有人会说,“哎呀这些不过是你说的,但是我们并没有真正看过啊,你怎么知道你说的这些就是对的呢?”。不急,我们依旧可以使用HSDB来验证我们的说法。

还是上文的代码,打开HSDB后,找到我们创建的Blog对象:

验证Oop内部

可以看到,我们创建的这个对象,其是由Oop所描述,而Oop对象中存在一个指向Klass的指针,指向Klass,并且Oop对象中主要存放了对象实例的成员变量,说明刚才我们的结论是正确的,而在“宏观说法”中,对象的引用指向该内存空间,实际上就是指向这个Oop对象。那么就可以根据这个操作结果,用一张图来描述出Oop-Klass模型基本的样子:

Oop-Klass模型图

而左侧Oop对象图,实际上就是我们平常经常背的一道面试题的来源, Java对象由什么组成 :对象头、实例数据、对齐填充,在这部分内容中,指向klass的指针还存在是否指针压缩的概念。当然,这不是今天的重点,这部分内容我会在之后的JVM内容中作为单独一篇文章来描述。

我们接着往下说,刚才我们只是证明了Oop和Klass模型的内部结构,以及Oop-Klass存在的联系,是通过一个指针关联的,还有一个东西并没有得以证明,就是在最初介绍Oop模型和Klass模型时,我们说过其家族的庞大,对于每一种不同类型的类和对象,都由不同的Oop及Klass进行描述,首先修改一下刚才的代码,使用HSDB来分别查看不同的类和对象,观察其区别:

public class Blog {
    //基本数据类型
    private int a = 10;
    private int b = 20;
    //基本数据类型数组
    private int






请到「今天看啥」查看全文