创建对象的多种方法:new语句,反射,clone, 反序列化 等

其中,Object.clone 方法和反序列化通过直接复制已有的数据,来初始化新建对象的实例字段。,而 new 语句和反射机制,则是通过调用构造器来初始化实例字段。

构造器

1. 如果一个类没有定义任何构造器的话, Java 编译器会自动添加一个无参数的构造器。

2. 子类的构造器需要调用父类的构造器。如果父类存在无参数构造器的话,该调用可以是隐式的,也就是说 Java 编译器会自动添加对父类构造器的调用。但是,如果父类没有无参数构造器,那么子类的构造器则需要显式地调用父类带参数的构造器。

显式调用又可分为两种,一是直接使用“super”关键字调用父类构造器,二是使用“this”关键字调用同一个类中的其他构造器。无论是直接的显式调用,还是间接的显式调用,都需要作为构造器的第一条语句,以便优先初始化继承而来的父类字段。(不过这可以通过调用其他生成参数的方法,或者字节码注入来绕开。)

总而言之,当我们调用一个构造器时,它将优先调用父类的构造器,直至 Object 类。这些构造器的调用者皆为同一对象,也就是通过 new 指令新建而来的对象。

你应该已经发现了其中的玄机:通过 new 指令新建出来的对象,它的内存其实涵盖了所有父类中的实例字段。也就是说,虽然子类无法访问父类的私有实例字段,或者子类的实例字段隐藏了父类的同名实例字段,但是子类的实例还是会为这些父类实例字段分配内存的。

这些字段在内存中的具体分布是怎么样的呢?今天我们就来看看对象的内存布局。

压缩指针

在 Java 虚拟机中,每个 Java 对象都有一个对象头(object header),这个由标记字段和类型指针所构成。其中,标记字段用以存储 Java 虚拟机有关该对象的运行数据,如哈希码、GC 信息以及锁信息,而类型指针则指向该对象的类。

在 64 位的 Java 虚拟机中,对象头的标记字段占 64 位,而类型指针又占了 64 位。也就是说,每一个 Java 对象在内存中的额外开销就是 16 个字节。以 Integer 类为例,它仅有一个 int 类型的私有字段,占 4 个字节。因此,每一个 Integer 对象的额外内存开销至少是 400%。这也是为什么 Java 要引入基本类型的原因之一。

为了尽量较少对象的内存使用量,64 位 Java 虚拟机引入了压缩指针 [1] 的概念(对应虚拟机选项 -XX:+UseCompressedOops,默认开启),将堆中原本 64 位的 Java 对象指针压缩成 32 位的。

这样一来,对象头中的类型指针也会被压缩成 32 位,使得对象头的大小从 16 字节降至 12 字节。当然,压缩指针不仅可以作用于对象头的类型指针,还可以作用于引用类型的字段,以及引用类型数组。

那么压缩指针是什么原理呢?

打个比方,路上停着的全是房车,而且每辆房车恰好占据两个停车位。现在,我们按照顺序给它们编号。也就是说,停在 0 号和 1 号停车位上的叫 0 号车,停在 2 号和 3 号停车位上的叫 1 号车,依次类推。

原本的内存寻址用的是车位号。比如说我有一个值为 6 的指针,代表第 6 个车位,那么沿着这个指针可以找到 3 号车。现在我们规定指针里存的值是车号,比如 3 指代 3 号车。当需要查找 3 号车时,我便可以将该指针的值乘以 2,再沿着 6 号车位找到 3 号车。

这样一来,32 位压缩指针最多可以标记 2 的 32 次方辆车,对应着 2 的 33 次方个车位。当然,房车也有大小之分。大房车占据的车位可能是三个甚至是更多。不过这并不会影响我们的寻址算法:我们只需跳过部分车号,便可以保持原本车号 *2 的寻址系统。

上述模型有一个前提,你应该已经想到了,就是每辆车都从偶数号车位停起。这个概念我们称之为内存对齐(对应虚拟机选项 -XX:ObjectAlignmentInBytes,默认值为 8)。

默认情况下,Java 虚拟机堆中对象的起始地址需要对齐至 8 的倍数。如果一个对象用不到 8N 个字节,那么空白的那部分空间就浪费掉了。这些浪费掉的空间我们称之为对象间的填充(padding)。

在默认情况下,Java 虚拟机中的 32 位压缩指针可以寻址到 2 的 35 次方个字节,也就是 32GB 的地址空间(超过 32GB 则会关闭压缩指针)。

在对压缩指针解引用时,我们需要将其左移 3 位,再加上一个固定偏移量,便可以得到能够寻址 32GB 地址空间的伪 64 位指针了。

此外,我们可以通过配置刚刚提到的内存对齐选项(-XX:ObjectAlignmentInBytes)来进一步提升寻址范围。但是,这同时也可能增加对象间填充,导致压缩指针没有达到原本节省空间的效果。

举例来说,如果规定每辆车都需要从偶数车位号停起,那么对于占据两个车位的小房车来说刚刚好,而对于需要三个车位的大房车来说,也仅是浪费一个车位。

但是如果规定需要从 4 的倍数号车位停起,那么小房车则会浪费两个车位,而大房车至多可能浪费三个车位。

当然,就算是关闭了压缩指针,Java 虚拟机还是会进行内存对齐。此外,内存对齐不仅存在于对象与对象之间,也存在于对象中的字段之间。比如说,Java 虚拟机要求 long 字段、double 字段,以及非压缩指针状态下的引用字段地址为 8 的倍数。

字段内存对齐的其中一个原因,是让字段只出现在同一 CPU 的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。

下面我来介绍一下对象内存布局另一个有趣的特性:字段重排列。

其一,如果一个字段占据 C 个字节,那么该字段的偏移量需要对齐至 NC。这里偏移量指的是字段地址与对象的起始地址差值。

以 long 类为例,它仅有一个 long 类型的实例字段。在使用了压缩指针的 64 位虚拟机中,尽管对象头的大小为 12 个字节,该 long 类型字段的偏移量也只能是 16,而中间空着的 4 个字节便会被浪费掉。

其二,子类所继承字段的偏移量,需要与父类对应字段的偏移量保持一致。

在具体实现中,Java 虚拟机还会对齐子类字段的起始位置。对于使用了压缩指针的 64 位虚拟机,子类第一个字段需要对齐至 4N;而对于关闭了压缩指针的 64 位虚拟机,子类第一个字段则需要对齐至 8N。

class A {
long l;
int i;
} class B extends A {
long l;
int i;
}

我在文中贴了一段代码,里边定义了两个类 A 和 B,其中 B 继承 A。A 和 B 各自定义了一个 long 类型的实例字段和一个 int 类型的实例字段。下面我分别打印了 B 类在启用压缩指针和未启用压缩指针时,各个字段的偏移量。

# 启用压缩指针时,B 类的字段分布
B object internals:
OFFSET SIZE TYPE DESCRIPTION
0 4 (object header)
4 4 (object header)
8 4 (object header)
12 4 int A.i 0
16 8 long A.l 0
24 8 long B.l 0
32 4 int B.i 0
36 4 (loss due to the next object alignment)

当启用压缩指针时,可以看到 Java 虚拟机将 A 类的 int 字段放置于 long 字段之前,以填充因为 long 字段对齐造成的 4 字节缺口。由于对象整体大小需要对齐至 8N,因此对象的最后会有 4 字节的空白填充。

# 启用压缩指针时,B 类的字段分布
B object internals:
OFFSET SIZE TYPE DESCRIPTION
0 4 (object header)
4 4 (object header)
8 4 (object header)
12 4 int A.i 0
16 8 long A.l 0
24 8 long B.l 0
32 4 int B.i 0
36 4 (loss due to the next object alignment)

当关闭压缩指针时,B 类字段的起始位置需对齐至 8N。这么一来,B 类字段的前后各有 4 字节的空白。那么我们可不可以将 B 类的 int 字段移至前面的空白中,从而节省这 8 字节呢?

我认为是可以的,并且我修改过后的 Java 虚拟机也没有跑崩。由于 HotSpot 中的这块代码年久失修,公司的同事也已经记不得是什么原因了,那么姑且先认为是一些历史遗留问题吧。

总结和实践

今天我介绍了 Java 虚拟机构造对象的方式,所构造对象的大小,以及对象的内存布局。

常见的 new 语句会被编译为 new 指令,以及对构造器的调用。每个类的构造器皆会直接或者间接调用父类的构造器,并且在同一个实例中初始化相应的字段。

Java 虚拟机引入了压缩指针的概念,将原本的 64 位指针压缩成 32 位。压缩指针要求 Java 虚拟机堆中对象的起始地址要对齐至 8 的倍数。Java 虚拟机还会对每个类的字段进行重排列,使得字段也能够内存对齐。

JVM总结-java对象的内存布局的更多相关文章

  1. Java对象的内存布局

    对象的内存布局 平时用java编写程序,你了解java对象的内存布局么? 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域: 对象头 实例数据 对齐填充 对象头 对象头包括两部分信息: ...

  2. Java对象的内存布局以及对象的访问定位

    一 Java对象的内存布局 在HotSpot虚拟机中,对象在内存中的布局分为3个区域 对象头(Header) Mark Word(在32bit和64bit虚拟机上长度分别为32bit和64bit)存储 ...

  3. 3 Java对象的内存布局以及对象的访问定位

    先来看看Java对象在内存中的布局   一 Java对象的内存布局 在HotSpot虚拟机中,对象在内存中的布局分为3个区域 对象头(Header) Mark Word(在32bit和64bit虚拟机 ...

  4. Java对象的内存布局以及对象所需内存大小计算详解

    1. 内存布局 在HotSpot虚拟机中,对象的内存布局可以分为三部分:对象头(Header). 实例数据(Instance Data)和对齐填充(Padding). 1) 对象头(Header): ...

  5. 10 Java 对象的内存布局

    Java 创建对象的方式 1:new 语句和反射机制创建.该方式会调用类的构造器,同时满足诸多约束.如果一个类没有构造器的话,Java 编译器会自动添加一个无参数的构造器.子类的构造器需要调用父类的构 ...

  6. 深入理解 Java 对象的内存布局

    对于 Java 虚拟机,我们都知道其内存区域划分成:堆.方法区.虚拟机栈等区域.但一个对象在 Java 虚拟机中是怎样存储的,相信很少人会比较清楚地了解.Java 对象在 JVM 中的内存布局,是我们 ...

  7. 一个Java对象的内存布局

    1.对象的创建过程 class loading class linking(verification,preparation,resolution) class initializing 申请对象内存 ...

  8. java对象的内存布局(二):利用sun.misc.Unsafe获取类字段的偏移地址和读取字段的值

    在上一篇文章中.我们列出了计算java对象大小的几个结论以及jol工具的使用,jol工具的源代码有兴趣的能够去看下.如今我们利用JDK中的sun.misc.Unsafe来计算下字段的偏移地址,一则验证 ...

  9. Object o = new Object()占多少个字节?-对象的内存布局

    一.先上答案 这个问题有坑,有两种回答 第一种解释: object实例对象,占16个字节. 第二种解释: Object o:普通对象指针(ordinary object pointer),占4个字节. ...

随机推荐

  1. 如何使用redis计数器防止并发请求

    需求描述 最近项目中有个需求,短信发送的并发请求问题:业务需求是需要限制一个号码一分钟内只能获取一次随机码,之前的实现是短信发送请求过来后,先去数据库查询发送记录,根据上一次的短信发送时间和当前时间比 ...

  2. js this详解,事件的三种绑定方式

    this,当前触发事件的标签 在绑定事件中的三种用法: a. 直接HTML中的标签里绑定 onclick="fun1()"; b. 先获取Dom对象,然后利用dom对象在js里绑定 ...

  3. spring boot编译项目打jar包

    <build> <plugins> <!--jar包打包--> <plugin> <groupId>org.springframework. ...

  4. JComboBox实现当前所选项功能和JFrame窗口释放资源的dispose()方法

    JComboBox有一个 SelectedItem属性,所以使用getSelectedItem()就可以得到当前选中值. package ltb20180106; import javax.swing ...

  5. vi 编辑器基本命令

    命令模式(esc) k 上移一行j 下移一行h 左移一行l 右移一行 6j  下移6行 5k 上移5行 0 将游标放在一行的开始$ 将游标放在一行的末尾w 将游标移动到下一个单词b 将游标移动到上一个 ...

  6. Howto Building GNU Toolchains

    [REF: https://wiki.linaro.org/WorkingGroups/ToolChain/BuildingGNUToolchains] This page is a work in ...

  7. AS3 内存基础

    1:获取一个对象的字节数: var str:String="ddd啊"; var byte:ByteArray=new ByteArray(); byte.writeMultiBy ...

  8. hadoop fs、hadoop dfs与hdfs dfs的区别

    不多说,直接上干货! hadoop fs:    使用面最广,可以操作任何文件系统. hadoop dfs与hdfs dfs :   只能操作HDFS文件系统相关(包括与Local FS间的操作),前 ...

  9. SQL注入漏洞解决方法

    本文只指针编码层次的SQL注入漏洞解决方法,例子代码是以java为主. 1,参数化的预编译查询语句 不安全例子 String query = "SELECT account_balance ...

  10. vue-router配合vue-cli的实例

    前面在说到vue-router的时候,都是用最简单的方式说明用法的,但是在实际项目中可能会有所出入,所以,今天就结合vue脚手架来展示项目中的vue-router的用法. 创建项目 首先需要使用脚手架 ...