本文主要讲解一下在 JVM 中如何保存 Java 对象以及 Java 对象指针压缩相关的东西
JVM 体系结构
JVM 规范中定义的体系结构(这个只是定义的规范,实际的 JVM 实现中可能与这个结构会有差异)
堆和方法区是所有类共享的,其中堆主要存储对象实体,方法区存储的信息比较多,主要包括下面几类:
类的基本类型信息
-
类型的全限定名
-
直接超类的全限定名(除了 Object)
-
是类还是接口
-
访问修饰符
该类的常量池
-
虚拟机会为每个转载的类型维护一个常量池
字段信息
-
字段名称
-
字段类型
-
字段修饰符(public,private,protected,static,final,volatile,transient)
方法信息
-
方法名
-
方法的返回值类型或者void
-
方法的参数数量和类型(按照声明顺序)
-
方法的修饰符(public,private,protected,static,final,synchronized,natvie,abstract)
-
如果不是abstract和native方法,还会保存下面的信息
-
方法的字节码
-
操作数栈和局部变量区的大小
-
异常表
类(静态)变量
-
静态常量和非静态常量的处理方式不同,每个类都会把用到的其他类的静态常量拷贝到自己的常量池中。
指向 ClassLoader 类的引用
-
指向 Class 类的引用,对于每个被装载的类型,JVM 都会为其创建一个 java.lang.Classs 类的实例(该实例存在heap中),并且JVM 会以某种方式将该实例和方法区中对应的类型关联起来。
对象如何保存
我们知道一个Java对象包含两部分内容,字段和方法,每个对象的字段值都可能不同,但是所用的方法都是一样的,如果每个对象都保存一套方法定义,显然会浪费很多的空间。所以方法定义相关的都放到了方法区,对象只保存自己的实例数据和指向方法定义的指针。下图是对象保存的一种方式,也是 Hotspot 虚拟机采用的方式,对象在堆中只保存实例的数据,同时会有一个指针指向方法区中的一个方法表(和 c++ 中的Virtual method table 类似)。
方法表保存两个部分:指向类数据的指针和执行各个方法的指针。这里将类数据和方法分开存储,是为了更加快速的找到方法。每个类都会对应一个方法表,这种实现方式会稍微浪费一些内存,但是会获得更好的性能。
我们知道对象是有继承关系的,如果子类没有覆写父类的方法,那么子类会指向父类的中的方法。
HotSpot 内存结构
上面主要是 Java 虚拟机规范中定义的规范,每种虚拟机实现的方式可能不太相同,这里我们主要看下 HotSpot 虚拟机的实现,后面的内容都是基于 HotSpot 虚拟机。 在 Java8 中,HotSpot VM 移除了永生代(PermGen),添加了元数据空间(Metaspace),元空间不使用虚拟机内存,而是使用本地内存。元空间主要和方法区对应,存储类的元数据和常量池(String常量的实例存在堆中)等信息。
Ordinary Object Pointer (OOP)
在 JVM 中 Java 对象使用 OOP(Ordinary Object Pointer) 来表示。
OOP 主要包含两个部分:对象头和实例数据。对象头主要包含四个部分:
-
Mark Word - 会存储对象的多种标记信息,例如哈希值、GC标记、锁等信息
-
Klass Word - 主要指向类的元数据
-
32-bit length word - 只有数组对象才有,记录数组的长度
-
32-bit gap - Java 是 8 字节对齐的,该字段主要用做对齐填充用
对象头后面就是实例数据,可能是基本数据,也可能是指向其他对象的引用。如果实例数据的大小不是 8 的倍数,那么也会插入一些填充的数据来对齐。
对于继承的情况,会先存放父类的实例数据,然后再存放子类的实例数据
Mark word、Klass word 以及对象的引用大小和 JVM 位数相关,32位 JVM 是 4 字节大小,64 位 JVM 是 8 字节大小。对于引用来说,4字节来寻址的话的最多可以表示 232,也就是做大只能支持 4GB 的内存,一般来说4GB 的内存是不大够用的,所以我们常用的是 64 位的 JVM,但是使用 64 位 JVM 带来的一个问题就是引用从 4 个字节变成了 8 个字节,也就是会多占一倍的空间,这样会导致更加频繁的 GC 周期,导致性能变差。
Compressed OOPs
我们使用压缩的 OOP 来实现在64位的 JVM 上使用32位大小的引用来寻址,这个方式主要是基于 Java 对象是 8 字节对齐,即后三位全部为 0,也就是在当前的对象引用中后三位实际上是没有用到的。基于上面的逻辑,我们就可以做一下优化,将当前32位值的表示为第 4-35 位的值,也就是实际的值相当于左移了三位,如下图所示。这样我们就有35位来寻址,内存最大就可以支持到 32GB。
开启了压缩之后,堆中 OOP 里的下列字段会被压缩:
-
每个对象的 Kclass 字段(Mark不会压缩)
-
指向其他 OOP 的引用
-
OOP 数组中的每个元素
下面是 Integer 对象在不同情况下占的内存大小,因为 Java 是 8 字节对齐,所以在64位 VM 上未开启压缩时,Integer 还要加上一个 32bit 填充,即总的大小是 192 bit。
我们可以在启动 Java 程序时使用 -XX:+UseCompressedOops 来开启压缩,Java7之后,如果最大内存小于32G,会自动开启 OOP 压缩。如果想在超过 32G 内存的情况下使用压缩,可以通过指定Java 对象对齐的字节数来实现 -XX:ObjectAlignmentInBytes,该值必须在 8 到 256 之间,并且是 2 的指数倍。假设指定为 16,那么就可以使用 64G 的内存,但是由于对齐造成的内存浪费也会更多。
另外在 Java11 中添加的 ZGC 垃圾回收器必须使用 64 位的指针,所以它不支持压缩的OOP。
下面还为大家做了一些笔记整理!在初学Java的时候,会遇到很多不懂的问题,在此做一些整理。
JRE和JDK的区别
JRE(Java Runtime Environment):java的运行环境,包括jvm+java的核心类库。
JDK(Java Development Kit):java的开发工具,包括jre+开发工具。
环境变量PATH和classpath的作用
path是配置Windows可执行文件的搜索路径,即扩展名为.exe的程序文件所在的目录,用于指定DOS窗口命令的路径。
Classpath是配置class文件所在的目录,用于指定类搜索路径,JVM就是通过它来寻找该类的class类文件的。
变量的作用是储存变量。为什么要定义变量:用来不断的存放同一类型的常量,并可以重复使用。
标示符命名规则
由数字、大小写英文字母、以及_和$组成(不能以数字开头,不能使用关键字来自定义命名)
数据类型
整数类型:byte、short、int、long
浮点数类型:float、double
字符类型:char
布尔类型:ture false
类和对象
类:对现实世界中某类事物的描述,是抽象的。
对象:事物具体存在的个体。
static关键字
静态的意思,用来修饰成员变量和 成员函数
静态的特点:随着类的加载而加载,优先于对象存在,对所有对象共享,可以对类名直接调用。
public static void main(String[] args):
public:公共的意思,是最大权限修饰符。
static:由于jvm调用main方法的时候,没有创建对象。只能通过类名调用。所以,main必须用static修饰。
void:由于main方法是被jvm调用,不需要返回值。用void修饰。
main:main是主要的意思,所以jvm采用了这个名字。是程序的入口。
String[]:字符串数组。
args:数组名。
注释
单行注释: //注释内容
多行注释: /*注释内容*/
文档注释: /**注释内容*/
Java学习视频
Java基础:
Java300集,Java必备优质视频_手把手图解学习Java,让学习成为一种享受
Java项目:
【Java游戏项目】1小时教你用Java语言做经典扫雷游戏_手把手教你开发游戏
【Java毕业设计】OA办公系统项目实战_OA员工管理系统项目_java开发