前言

这篇博客主要来说说类与对象在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,下面来介绍其基本的一些使用方式。

启动

首先需要需要复制jdkjrebin目录下的sawindbg.dll文件到jrebin目录下,然后进入jdklib目录下,使用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 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对象:

验证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[]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. 基本数据类型数组:

    基本数据类型数组

  2. 引用数据类型数组:

    引用数据类型数组

  3. 对象:

    对象

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

OopKlass表示类型

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文章中进行分享。

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

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

  1. 重磅硬核 | 一文聊透对象在 JVM 中的内存布局,以及内存对齐和压缩指针的原理及应用

    欢迎关注公众号:bin的技术小屋 大家好,我是bin,又到了每周我们见面的时刻了,我的公众号在1月10号那天发布了第一篇文章<从内核角度看IO模型的演变>,在这篇文章中我们通过图解的方式以 ...

  2. Java对象在JVM中的生命周期

          当你通过new语句创建一个java对象时,JVM就会为这个对象分配一块内存空间,只要这个对象被引用变量引用了,那么这个对象就会一直驻留在内存中,否则,它就会结束生命周期,JVM会在合适的时 ...

  3. 类、对象以及jvm运行内存解析

    一.JVM内存的分析: 第一步:存放在硬盘上的程序首先要被加载到内存空间中. 第二步:内存中的jvm找到程序中main函数作为入口,然后开始执行. 第三步:执行过程中的内存管理:内存分为四个部分: 栈 ...

  4. 《深入Java虚拟机学习笔记》- 第7章 类型的生命周期/对象在JVM中的生命周期

    一.类型生命周期的开始 如图所示 初始化时机 所有Java虚拟机实现必须在每个类或接口首次主动使用时初始化: 以下几种情形符合主动使用的要求: 当创建某个类的新实例时(或者通过在字节码中执行new指令 ...

  5. Hibernate 系列 06 - 对象在JVM中的生命周期

    引导目录: Hibernate 系列教程 目录 Java对象通过new命令进行创建,Java虚拟机(Java Virtual Machine,JVM)会为新的Java对象在内存中开辟一个新空间以存放次 ...

  6. jvm详情——2、Java对象在jvm中的大小

    Java对象的大小 基本数据的类型的大小是固定的,这里就不多说了.对于非基本类型的Java对象,其大小就值得商榷.在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任 ...

  7. 【Java】运行时Java对象在内存中是如何存储的?

    翻译自这一篇文章 我们知道函数在内存中实现为一个活动记录的栈.我们也知道Java方法在JVM栈区中实现为一个帧栈而Java对象是在堆区进行分配的. Java对象在堆内存中是怎样的呢?一旦对象保存在内存 ...

  8. python学习之【第十七篇】:Python中的面向对象(类和对象)

    1.什么是类和类的对象? 类是一种数据结构,我们可以用它来定义对象,后者把数据值和行为特性融合在一起,类是现实世界的抽象的实体以编程形式出现.实例是这些对象的具体化.类是用来描述一类事物,类的对象指的 ...

  9. oc语言学习之基础知识点介绍(二):类和对象的进一步介绍

    一.类.对象在内存中的存储 /* 内存分区: 栈:局部变量 堆:程序员自己写代码申请开辟的 程序员自己维护,编译器现在帮我们自动优化了,它在合适的给我们加上了释放空间的语句,所以我们现在写的对象不会造 ...

随机推荐

  1. Netty之网络编程数据编码

    一.概况 我们在进行网络编程中会把各种数据转换为byte数据以便能在网络上传输,最常见的网络字节序--Little-Endian和Big-Endian,也让好多初进网络编程的新手摸不着头脑,还有按位或 ...

  2. centos8平台使用xfs文件系统

    一,xfs文件系统的特点 XFS是一种高性能的日志文件系统, 它是由SGI公司设计的,被称为业界最先进的.最具可升级性的文件系统技术. 最初是从unix(irix)移植到linux系统上的. 从cen ...

  3. HTML <del> 标签

    HTML <del> 标签 什么是<del> 标签? 定义文档中已被删除的文本. 实例 a month  is <del>25</del> 30 day ...

  4. APP脱壳方法三

    第一步 手机启动frida服务 第二步 手机打开要脱壳的app 第三步编辑hook代码 agent.js /* * Author: hluwa <hluwa888@gmail.com> * ...

  5. JAVA 基于Jusup爬虫

    java爬虫核心:httpclient slf4j jsoup slf4j 配置文件log4j.properties log4j.rootlogger=DEBUG,A1log4j.logger.cn. ...

  6. .net core autofac asyncinterceptor 异步拦截器帮助包

    autofac使用拦截器实现AOP,是基于Castle.Core的.然而Castle.Core并未提供原生异步支持.所以需要使用帮助类实现,这在autofac官方文档的已知问题中有详细说明: http ...

  7. 三种方式获取SSMS连接密码

    内网渗透是有的时候会遇到对方SSMS没断开连接正连着别的机器的mssql此时有两种方法可以获取sa密码 当密码强度较弱时可以使用第一只方式,第一种方式解不开的情况下可以使用后面二种方式 1.直接查询s ...

  8. LruCache缓存bitmap(三)

    应用在网络连接上,onrestart后不会重新联网获取图片,省去了流量, public class MainActivity extends AppCompatActivity { ImageView ...

  9. vue生命钩子函数

    vue的生命钩子函数在使用Vue开发中是非常重要的一环,可以说,生命钩子函数使开发变得更加便捷. 下图是Vue的生命周期图: 具体钩子如下: beforeCreate created beforeMo ...

  10. 支持向量机(SVM)必备概念(凸集和凸函数,凸优化问题,软间隔,核函数,拉格朗日乘子法,对偶问题,slater条件、KKT条件)

    SVM目前被认为是最好的现成的分类器,SVM整个原理的推导过程也很是复杂啊,其中涉及到很多概念,如:凸集和凸函数,凸优化问题,软间隔,核函数,拉格朗日乘子法,对偶问题,slater条件.KKT条件还有 ...