类变量解析

2020-03-25  本文已影响0人  _云起

类变量解析

1 java中private 字段可以被继承吗

有一种说法是凡是父类中被定义为private的字段,都是“老子的”私有财产,即便是儿子,也继承不了

如果没有继承的话,为什么父类和子类的大小都是一样呢?所以子类继承了父类的私有字段,只是没法访问而已。而当我们调用父类get XXX方法时,就拿到了父类的字段。也就是说虽然儿子继承了父类的私有财产,但是却没有直接权力支配,除非老子给儿子开放了特有的接口访问,否则儿子不能动老子的私有财产。

我们可以接着进行验证,首先我们定义一个Myclass类,里面定义4个字段。接着我们定义一个Test类,去继承它,然后我们去验证这个结论。

咦,为什么debug的时候a,b,c,d字段居然在test实例中呢?哈哈,这就说明子类确实是可以继承到父类的私有字段的。

我们可以使用jdk自带工具HSDB进行验证。在jdk/lib目录下,使用如下命令:java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB,去查看大小。具体如何使用HSDB可以参考下面这个链接。https://www.cnblogs.com/alinainai/p/11070923.html

我们可以看到nonstatic_fied_size:5至于为什么是5呢?因为我们最后定义一个为long类型的大小为2个slot槽所以size为3+2=5

以下所有代码引用open-jdk1.8 hotspot源码

在调用parse_fields()函数之前,先定义了一个变量fac,这里的FieldAllocationCount是一个class类型,声明如下:

通过声明可知FieldAllocationCount的变量实例将会记录5种静态(static)类

型变量的数量和5种对应的非静态类型的变量的数量,这5种变量类型分别是:

Oop,引用类型

Byte字节类型

Short短整型

Word,双字类型

Double,浮点型

layout _fields()函数里面会分别统计static和非static种变量的数量,后面JVM为

Java类分配内存空间时,会根据这些变量的数量计算所占内存大小。可能有小伙伴会问Java

语言所支持的实际数据类型远不止这5种呀,例如boolean float int等等,为什么JVM

统计这5种呢?这是因为在JVM部,除了引用类型,所有内置的基本类型都使用剩余的

4种类型来表示,因此想知道Java类的域变量需要占用多少内存,只需要分别统计这几种类型的数量即可

这个Array类就是用于元数据分配的数组

上面对ClassFileParser: :parse_ fields()的主要逻辑进行了注释,通过注释可以知道JVM  Java类域变量的逻辑是:

获取Java类中的变量数量

为字段数据分配临时资源数组

读取变量的访问标识

读取变量名称索引

读取变量类型索引

读取常量索引

读取签名表索引,

对字段表属性进行解析

读取变量属性

判断变量类型

统计各类型数量,并分配字段数据大小

从临时资源数组获取字段的数据

将字段值保存到元数组

2 偏移量

解析完字节码文件中Java类的全部域变量信息后,JVM计算出5种数据类型各自的数量。依旧在 ClassFileParser::parseClassFile()中,具体实现在layout_fields()函数中

计算全部静态变量所占的字节数。

这段逻辑很简单,先拿到起始偏移量,接着分别根据各个静态类型变量的偏移量计算总偏移量。计算顺序是:

static_oop

static_double

static_word

static_short

static_byte

每一种“下游”数据类型的偏移量都依赖其“上游”数据类型所占的字节宽度以及数量。

JVM为何要记录每一个变量的偏移量呢?其实。这与静态变量的存储机制和访问机制有关。对于JDK1.8而言,静态变量存储在Java类在JVM所对应的镜像类instanceMirrorKlass.cpp 中,当Java代码访问静态变量时,最终JVM也是通过设置偏移量进行访问。

3 非静态变量偏移量

相比于静态变量偏移量,非静态变量的偏移量计算稍显复杂。

获取完非静态字段起始偏移量之后就获取double和oop这 两种非静态变量的起始偏移量,,分配策略不同,2种变量的起始偏移量也不同。下面分别为处理非压缩类型,以及处理压缩类型

紧接着计算剩余三种类型变量的起始偏移量

5大类型变量的偏移量都计算完,现在开始遍历所有字段,分别计算各类型具体变量的偏移值,这里包括静态的和非静态的。

如上面代码所注释的那样,非静态类型变量的偏移量计算逻辑可以分为5个步骤

(1) 计算非静态变量起始偏移量,这一步需要首先获取父类的非静态字段大小。

(2) 计算non_static_double_offset和ono_static_oop_offset这两种非静态变量的起始偏移量。

(3) 计算剩余三种类型变量起始偏移量:non_static_word_offset,non_static_short_offset和non_static_byte_offset

(4) 计算对齐补白空间。

(5) 计算补白后非静态变量所需要的内存空间总大小。

3.1什么是偏移量?

对于非静态变量类型的变量,其偏移量是相对于未来即将new出来的Java对象实例在JVM内部所对应的instanceOop对象实例首地址的偏移位置。我们知道常量池对象采用oop-klass这种一分为二的模型来描述。对于Java类,JVM内部同样适用这种一分为二的模型来描述,对应的oop类是instanceOopDesc,对应的klass类是instanceKlass。在oop-klass模型中,oop模型主要存储对象实例的实际数据,而klass模型则主要存储对象的结构信息和虚函数方法表,说白了就是描述类的结构和行为。

当JVM加载一个Java类,会首先构建对应的instanceKlass对象,而当new一个Java对象实例时,则会构建出对应的instanceOop对象。InstanceOop对象主要由Java类的成员变量组成,而这些成员变量在instanceOop中的排列顺序,便由各种变量类型的偏移量决定。

在Hotspot内部,任何oop对象都包含对象头,因此实际上非静态变量的偏移量要从对象头的末尾开始计算。Java类实例在堆内存的起始偏移量如下

知道了偏移量的含义,现在来看看JVM的实现,源代码逻辑如下

起始位置等于base_offset_in_bytes + nonstatic_field_size*heapOopSize;也就是要获父类的非静态字段大小,这里说明父类的字段是可以让子类继承的,因为计算子类的时候,需要获取父类的字段大小。而父类的这个大小定义在instanceKlass.hpp

而base_offset_in_bytes大小定义在instanceOop.hpp中,如果启用压缩指针,那么则调用klass_gap_offset_in_bytes方法,否则为实例instanceOopDesc大小。instanceOopDesc继承自oopDesc,而oopDesc类型大小在前面文章已经计算出来了,是两个指针宽度。假设JVM运行在64位架构上,则这个值是16,而启用了压缩指针,则在64位架构上,该值为12,这是因为无论是否开启压缩策略,oop._mark作为一个指针是不会被压缩的,任何时候都会占用8字节,而oop._klass则会受压缩策略的影响,若开启压缩策略,则_klass仅会占用4字节,所以在64位架构上开启压缩策略的情况下,oop对象头总共占用12字节的内存空间。  

Java类是面向对象的,继承是面向对象编程的三大特性之一,除了 java.lang. Object 类以外,所有的 Java 类都显式或隐式地继承了某个父类,而字段继承和方法继承则构成了继承的全部。

如果说继承是目标,那么字段在子类中的内存占用则是技术手段子类必须将父类中定

义的非静态字段信息全部复一遍,才能实现字段继承的目标因此,在计算子类非静态字段的起始偏移量时,必须将父类可被继承字段的内存大小考虑在内。具体而言,子类的非静态字段起始偏移量,在计算完oop对象头的大小后,还需要为父类的可被继承的字段预留空间。

Hotspot将父类可被继承的字段的内存空间安排在子类所对应的oop对象头的后面,因此最终一个Java类中非静态字段的起始偏移位置在父类被继承的字段域的末尾,所下所示

内存对齐与字段重排

Java类字段的偏移地址与内存对齐有脱不开的关系,JVM为了处理内存对齐,颇费了一番心思,不惜将字段进行重排。

什么是内存对齐?

内存对齐与数据在内存中的位置有关如果一个变量的内存起始地址正好等于其长度的整数倍,则这种内存分配就被称作自然对齐。

(1)在32位x86平台上,基本的内存对齐规则如下所示

举个例子,在32位CPU下,假设一个int整形变量的内存地址为0x00000008,那么这个变量就是自然对齐的。

(2)为什么需要字节对齐

现代计算机中内存空间都是按照字节划分的,也即内存的力度细分到存储单元,而一个存储单元恰恰包含8个比特,正好是一字节,因此从理论上讲似乎对任何类型的变量的访问可以从任何地址开始。

但实际情况恰恰相反,各个硬件平台在对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定的地址开始存取。例如有些架构的CPU在访问一个没有进行对齐的变量时会反生错误,那么在这种架构下进行编程就需要保证字节对齐。

3.2内存分配总结

规则1:任何对象都是以8字节为粒度进行对齐的。

规则2:类属性按照如下优先级进行排列:长整型和双精度类型;整型和浮点型; 字符和短整型;字节类型和布尔类型;最后是引用类型。这些属性都按照各自类 型宽度对齐。 。

规则3:不同类继承关系中的成员不能混合排列。首先按照规则2处理父类中的成员,接着才是子类的成员 。

规则4:当父类最后一个属性和子类第一个属性之间间隔不足4字节时,必须扩展到4字节的基本单位。  

规则5:如果子类第一个成员是一个双精度或长整型,并且父类没有用完 8 字节 (没有显式的父类,并且JVM开启了指针压缩策略,oop对象头只占用 12 字节时), JVM 会破坏规则 2,按整型(int)、短整型(short)、字节型(byte)、引用类型 (reference)的顺序向未填满的空间填充

4.Java类在堆内存中的内存空间,主要由Java类非静态字段占据。Hotspot解析Java类非静态字段和分配堆内存空间的主要逻辑总结为如下几步:

(1)解析常量池,统计Java 类中非静态字段的总数量,按照5大类型(oops、 longs/doubles、ints、horts/chars、bytes)分别统计。

(2)计算Java类字段的起始偏移量,起始偏移位置从父类继承的字段域的末尾开始

(3)按照分配策略,计算5大类型中的每一个类型的起始偏移量。

(4) 以5大类型各个类型的起始偏移量为基准,计算每一个大类型下各个具体字段的偏移量。

(5)计算Java类在堆内存中所需要的内存空间。

经过上面5步,HotSpot便能确定一个Java类所需要的堆内存空间。 当全部解析完Java 类之后,Java 类的全部字段信息及其偏移量将会保存到Hotspot 所构建出来的instanceKlass 中,至此,一个Java类的字段结构信息便全部解析完成。当Java程序中使用new关键字创建Java类的实例对象时,Hotspot便会从instanceKlass中读取Java类所需要的堆内存大小并分配对应的内存空间。

上一篇下一篇

猜你喜欢

热点阅读