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

前言


这篇博客主要来说说类与对象在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.dll文件到jre\bin目录下,然后进入jdk\lib目录下,使用java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB,即可启动HSDB:


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


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


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


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


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


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


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


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


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


查看类


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


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


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


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




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


到这里我们已经可以看到,我们所创建的类,其在内存中的存在形式,实际上是使用一个名为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中是怎样的:


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


找到创建的对象:


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



可以看到在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 compiledICHolderOopDesc* compiledICHolderOop;


为了简化变量名,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对象:


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


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


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


而左侧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[]aArray = new int[10];
    //引用数据类型数组
    private Integer[] bArray = new Integer[10];
    //普通对象
    private Map<String,Object> mapObj = new HashMap<>(16);
    public static void main(String[] args) {
        Blog blog = new Blog();
        int[] typeArray = new int[10];
        Integer[] objArray = new Integer[10];
        while(true){}
    }
}


HSDB:


  1. 基本数据类型数组:


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


观察HSDB,不难看出我们在Blog类中创建的三种不同类型的成员属性:基本数据类型数组引用数据类型数组普通对象,都由不同的Oop-Klass模型进行表示,表示方式大致可以用下图进行描述:


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


Oop-Klass模型的简易理解


在JVM中,使用Oop-Klass模型这种一分为二的模型区描述Java类,但是笔者认为这种叫法并不是特别容易让人理解,对于初学者来说,什么是Oop,什么是Klass?并没有一种可以顾名思义的解读,实际上,无非就是元数据和实例数据进行分离,所以初学者看到这里,不妨可以把他直接理解为data-meta模型,data即oop、而meta即klass,这样就可以很好地理解Oop-Klass这个概念了。


而实际上,在JVM中,Klass保存元数据这个概念会更好理解一些,如果你看过JVM源码,你会发现,实际上在JVM源码中Klass正是继承Metadata类的。


结语


本文带大家了解了Java的类与对象在JVM中的存在形式,JVM将其一分为二,分为Oop-Klass,分别存储对象示例信息及类的元信息,在整个证明过程中,我们使用了HSDB这个强大的工具,对这一结构进行窥探及证明。


当然,Oop-Klass模型内部是一个庞大的体系,本文只是抓取了日常使用频次比较高的类以及比较有特点的一些类进行验证,感兴趣的同学可以在线下根据这套方法,自己去验证其他的一些类型的表示形式。


这是整个JVM专题的第一篇文章,关于JVM的更多内容将会在之后的JVM文章中进行分享。


如果需要提问,欢迎评论区留言~


另外欢迎大家访问:http://blog.objectspace.cn

上一篇:树莓派 之 备份树莓派


下一篇:Jupyter notebook快速入门教程