本文以如何计算Java对象占用内存大小为切入点,在讨论计算Java对象占用堆内存大小的方法的基础上,详细讨论了Java对象头格式并结合JDK源码对对象头中的协议字段作了介绍,涉及内存模型、锁原理、分代GC、OOP-Klass模型等内容。最后推荐JDK自带的Hotspot Debug工具——HSDB,来查看对象在内存中的具体存在形式,以论证文中所述内容。javascript
目前咱们系统的业务代码中大量使用了LocalCache的方式作本地缓存,并且cache的maxSize一般设的比较大,好比10000。咱们的业务系统中就使用了size为10000的15个本地缓存,因此最坏状况下将可缓存15万个对象。这会消耗掉不菲的本地堆内存,而至于实际上到底应该设多大容量的缓存、运行时这大量的本地缓存会给堆内存带来多少压力,实际占用多少内存大小,会不会有较高的缓存穿透风险,目前并不方便知悉。考虑到对缓存实际占用内存的大小能有个更直观和量化的参考,须要对运行时指定对象的内存占用进行评估和计算。css
要计算Java对象占用内存的大小,首先须要了解Java对象在内存中的实际存储方式和存储格式。java
另外一方面,你们都了解Java对象的存储总得来讲会占用JVM内存的堆内存、栈内存及方法区,但因为栈内存中存放的数据能够看作是运行时的临时数据,主要表现为本地变量、操做数、对象引用地址等。这些数据会在方法执行结束后当即回收掉,不会驻留。对存储空间空间的占用也只是执行函数指令时所必须的空间。一般不会形成内存的瓶颈。而方法区中存储的则是对象所对应的类信息、函数表、构造函数、静态常量等,这些信息在类加载时(按需)只会在方法区中存储一份,不会产生额外的存储空间。所以本文所要讨论的主要目标是Java对象对堆内存的占用。c++
若是读者关心对象在JVM中的存储原理,可阅读本文后边几个小节中关于对象存储原理的介绍。若是不关心对象存储原理,而只想直接计算内存占用的话,其实并不难,笔者这里总结了三种方法以供参考:git
使用java.lang.instrument.Instrumentation.getObjectSize()方法,能够很方便的计算任何一个运行时对象的大小,返回该对象自己及其间接引用的对象在内存中的大小。不过,这个类的惟一实现类InstrumentationImpl的构造方法是私有的,在建立时,须要依赖一个nativeAgent,和运行环境所支持的一些预约义类信息,咱们在代码中没法直接实例化它,须要在JVM启动时,经过指定代理的方式,让JVM来实例化它。github
具体来说,就是须要声明一个premain方法,它和main方法的方法签名有点类似,只不过方法名叫“premain”,同时方法参数也不同,它接收一个String类型和instrumentation参数,而String参数实际上和String[]是同样的,只不过用String统一来表达的。在premain函数中,将instrumentation参数赋给一个静态变量,其它地方就可使用了。如:apache
/** * @author yepei * @date 2018/04/23 * @description */ public class SizeTool { private static Instrumentation instrumentation; public static void premain(String args, Instrumentation inst) { instrumentation = inst; } public static long getObjectSize(Object o) { return instrumentation.getObjectSize(o); } }
从方法名能够猜到,这里的premain是要先于main执行的,而先于main执行,这个动做只能由JVM来完成了。即在JVM启动时,先启动一个agent,操做以下:数组
假设main方法所在的jar包为:A.jar,premain方法所在的jar包为B.jar。注意为main所在的代码打包时,和其它工具类打包同样,须要声明一个MANIFEST.MF清单文件,以下所求:缓存
Manifest-Version: 1.0 Main-Class: yp.tools.Main Premain-Class: yp.tools.SizeTool
而后执行java命令执行jar文件:ruby
java -javaagent:B.jar -jar A.jar
点评:这种方法的优势是编码简单,缺点就是必须启动一个javaagent,所以要求修改Java的启动参数。
java中的sun.misc.Unsafe类,有一个objectFieldOffset(Field f)方法,表示获取指定字段在所在实例中的起始地址偏移量,如此能够计算出指定的对象中每一个字段的偏移量,值为最大的那个就是最后一个字段的首地址,加上该字段的实际大小,就能知道该对象总体的大小。如现有一Person类:
class Person{ int age; String name; boolean married; }
假设该类的一个实例p,经过Unsafe.objectFieldOffset()方法计算到得age/birthday/married三个字段的偏移量分别是16,21, 17,则代表p1对象中的最后一个字段是name,它的首地址是21,因为它是一个引用,因此它的大小默认为4(开启指针压缩),则该对象自己的大小就是21+4+ 7= 32字节。其中7表示padding,即为了使结果变成8的整数倍而作的padding。
但上述计算,只是计算了对象自己的大小,并无计算其所引用的引用类型的最终大小,这就须要手工写代码进行递归计算了。
点评:使用Unsafe能够彻底不care对象内的复杂构成,能够很精确的计算出对象头的大小(即第一个字段的偏移)及每一个字段的偏移。缺点是Unsafe一般禁止开发者直接使用,须要经过反射获取其实例,另外,最后一个字段的大小须要手工计算。其次须要手工写代码递归计算才能获得对象及其所引用的对象的综合大小,相对比较麻烦。
这里要介绍的是lucene提供的专门用于计算堆内存占用大小的工具类:RamUsageEstimator,maven坐标:
<dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>4.0.0</version> </dependency>
RamUsageEstimator就是根据java对象在堆内存中的存储格式,经过计算Java对象头、实例数据、引用等的大小,相加而得,若是有引用,还能递归计算引用对象的大小。RamUsageEstimator的源码并很少,几百行,清晰可读。这里不进行一一解读了。它在初始化的时候会根据当前JVM运行环境、CPU架构、运行参数、是否开启指针压缩、JDK版本等综合计算对象头的大小,而实例数据部分则按照java基础数据类型的标准大小进行计算。思路简单,同时也在必定程度上反映出了Java对象格式的奥秘!
经常使用方法以下:
//计算指定对象及其引用树上的全部对象的综合大小,单位字节 long RamUsageEstimator.sizeOf(Object obj) //计算指定对象自己在堆空间的大小,单位字节 long RamUsageEstimator.shallowSizeOf(Object obj) //计算指定对象及其引用树上的全部对象的综合大小,返回可读的结果,如:2KB String RamUsageEstimator.humanSizeOf(Object obj)
点评:使用该第三方工具比较简单直接,主要依靠JVM自己环境、参数及CPU架构计算头信息,再依据数据类型的标准计算实例字段大小,计算速度很快,另外使用较方便。若是非要说这种方式有什么缺点的话,那就是这种方式计算所得的对象头大小是基于JVM声明规范的,并非经过运行时内存地址计算而得,存在与实际大小不符的这种可能性。
在HotSpot虚拟机中,Java对象的存储格式也是一个协议或者数据结构,底层是用C++代码定义的。Java对象结构大体以下图所示——
即,Java对象从总体上能够分为三个部分,对象头、实例数据和对齐填充
对象头:Instance Header,Java对象最复杂的一部分,采用C++定义了头的协议格式,存储了Java对象hash、GC年龄、锁标记、class指针、数组长度等信息,稍后作出详细解说。
实例数据:Instance Data,这部分数据才是真正具备业务意义的数据,实际上就是当前对象中的实例字段。在VM中,对象的字段是由基本数据类型和引用类型组成的。其所占用空间的大小以下所示:
说明:其中ref表示引用类型,引用类型其实是一个地址指针,32bit机器上,占用4字节,64bit机器上,在jdk1.6以后,若是开启了指针压缩(默认开启: -XX:UseCompressedOops
,仅支持64位机器),则占用4字节。Java对象的全部字段类型均可映射为上述类型之一,所以实例数据部分的大小,实际上就是这些字段类型的大小之和。固然,实际状况可能比这个稍微复杂一点,如字段排序、内部padding以及父类字段大小的计算等。
对齐填充:Padding,VM要求对象大小须是8的总体数,该部分是为了让总体对象在内存中的地址空间大小达到8的整数倍而额外占用的字节数。
对象头是理解JVM中对象存储方式的最核心的部分,甚至是理解java多线程、分代GC、锁等理论的基础,也是窥探JVM底层诸多实现细节的出发点。作为一个java程序猿,这是不可不了解的一部分。那么这里提到的对象头究竟是什么呢?
参考OpenJDK中JVM源码部分,对对象头的C++定义以下:
class oopDesc { friend class VMStructs; private: volatile markOop _mark; union _metadata { wideKlassOop _klass; narrowOop _compressed_klass; } _metadata; ... }
源码里的 _mark
和 _metadata
两个字段就是对象头的定义,分别表示对象头中的两个基本组成部分,_mark用于存储hash、gc年龄、锁标记、偏向锁、自旋时间等,而_metadata是个共用体(union
),即_klass字段或_compressed_klass
,存储当前对象到所在class的引用,而这个引用的要么由“_klass”来存储,要么由“_compressed_klass
”来存储,其中_compressed_klass
表示压缩的class指针,即当JVM开启了 -XX:UseCompressedOops
选项时,就表示启用指针压缩选项,天然就使用_commpressed_klass
来存储class引用了,不然使用_klass
。
注意到,_mark
的类型是 markOop
,而_metadata
的类型是union
,_metadata
内部两个字段:_klass
和_compressed_klass
类型分别为wideKlassOop
和narrowOop
,分别表示什么意思呢?这里顺便说一个union联合体的概念,这是在C++中的一种结构声明,相似struct,称做:“联合”,它是一种特殊的类,也是一种构造类型的数据结构。在一个“联合”内能够定义多种不一样的数据类型, 一个被说明为该“联合”类型的变量中,容许装入该“联合”所定义的任何一种数据,这些数据共享同一段内存,已达到节省空间的目的。因而可知,刚刚所说的使用-XX:UseCompressedOops
后,就自动使用_metadata
中的_compressed_klass
来做为指向当前对象的class引用,它的类型是narrowOop
。能够看到,对象头中的两个字段的定义都包含了“Oop”字眼,不难猜出,这是一种在JVM层定义好的“类型”。
实际上,Java的面向对象在语言层是经过java的class定义实现的,而在JVM层,也有对应的实现,那就是Oop模型。所谓Oop模型,全称:`Ordinary Object Pointer`,即普通对象指针。JVM层用于定义Java对象模型及一些元数据格式的模型就是:Oop,能够认为是JVM层中的“类”。经过[JDK源码](https://github.com/openjdk-mirror/jdk7u-hotspot/tree/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/oops)能够看到,有不少模型定义的名称都是以Oop结尾:`arrayOop`/`markOop`/`instanceOop`/`methodOop`/`objectArrayOop`等,什么意思呢? HotSpot是基于c++语言实现的,它最核心的地方是设计了两种模型,分别是`OOP`和`Klass`,称之为`OOP-Klass Model`. 其中`OOP`用来将指针对象化,比C++底层使用的"`*`"更好用,**每个类型的OOP都表明一个在JVM内部使用的特定对象的类型**。而`Klass`则用来描述JVM层面中对象实例的具体类型,它是java实现语言层面类型的基础,或者说是**对java语言层类型的VM层描述**。因此看到openJDK源码中的定义基本都以Oop或Klass结尾,如图所示: 由上述定义能够简单的说,Oop就是JVM内部对象类型,而Klass就是java类在JVM中的映射。其中关于Oop和Klass体系,参考定义:[https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/oops/oop.hpp](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/oops/oop.hpp);JVM中把咱们上层可见的Java对象在底层实际上表示为两部分,分别是oop和`klass`,其中`oop`专一于表示对象的实例数据,不关心对象中的实例方法(包括继承、重载等)所对应的函数表。而klass则维护对象到java class及函数表的功能,它是java class及实现多态的基础。这里列举几个基础的Oop和Klass——
Oop:
//定义了oops共同基类 typedef class oopDesc* oop; //表示一个Java类型实例 typedef class instanceOopDesc* instanceOop; //表示一个Java方法 typedef class methodOopDesc* methodOop; //定义了数组OOPS的抽象基类 typedef class arrayOopDesc* arrayOop; //表示持有一个OOPS数组 typedef class objArrayOopDesc* objArrayOop; //表示容纳基本类型的数组 typedef class typeArrayOopDesc* typeArrayOop; //表示在Class文件中描述的常量池 typedef class constantPoolOopDesc* constantPoolOop; //常量池告诉缓存 typedef class constantPoolCacheOopDesc* constantPoolCacheOop; //描述一个与Java类对等的C++类 typedef class klassOopDesc* klassOop; //表示对象头 typedef class markOopDesc* markOop;
Klass:
//klassOop的一部分,用来描述语言层的类型 class Klass; //在虚拟机层面描述一个Java类 class instanceKlass; //专有instantKlass,表示java.lang.Class的Klass class instanceMirrorKlass; //表示methodOop的Klass class methodKlass; //最为klass链的端点,klassKlass的Klass就是它自身 class klassKlass; //表示array类型的抽象基类 class arrayKlass; //表示constantPoolOop的Klass class constantPoolKlass;
结合上述JVM层与java语言层,java对象的表示关系以下所示:
OopDesc
是对象实例的基类(Java实例在VM中表现为
instanceOopDesc
),Klass是类信息的基类(Java类在VM中表现为
instanceKlass
),
klassKlass
则是对
Klass
自己的描述(Java类的
class
对象在VM中表现为
klassKlass
)。
有了对上述结构的认识,对应到内存中的存储区域,那么对象是怎么存储的,就了比较清楚的认识:对象实例(instanceOopDesc
)保存在堆上,对象的元数据(instanceKlass
)保存在方法区,对象的引用则保存在栈上。
所以,关于本小节,对OOP-Klass Model的讨论,能够用一句简洁明了的话来总结其意义:一个Java类在被VM加载时,JVM会为其在方法区建立一个instanceKlass
,来表示该类的class信息。当咱们在代码中基于此类用new建立一个新对象时,实际上JVM会去堆上建立一个instanceOopDesc对象,该对象保含对象头markWord和klass指针,klass指针指向方法区中的instanceKlass,markWord则保存一些锁、GC等相关的运行时数据。而在堆上建立的这个instanceOopDesc所对应的地址会被用来建立一个引用,赋给当前线程运行时栈上的一个变量。
mark word是对象头中较为神秘的一部分,也是本文讲述的重点,JDK oop.hpp源码文件中,有几行重要的注释,揭示了32位机器和64位机器下,对象头的格式:
// Bit-format of an object header (most significant first, big endian layout below): // // 32 bits: // -------- // hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) // size:32 ------------------------------------------>| (CMS free block) // PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object) // // 64 bits: // -------- // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) // PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object) // size:64 ----------------------------------------------------->| (CMS free block) // // unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object) // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object) // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object) // unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
在oop.hpp源码文件中,有对Oop基类中mark word结构的定义,以下:
class oopDesc { friend class VMStructs; private: volatile markOop _mark; union _metadata { wideKlassOop _klass; narrowOop _compressed_klass; } _metadata; ... }
其中的mark word即上述 _mark字段,它在JVM中的表示类型是markOop, 部分关键源码以下所示,源码中展现了markWord各个字段的意义及占用大小(与机器字宽有关系),如GC分代年龄、锁状态标记、哈希码、epoch、是否可偏向等信息:
...
class markOopDesc: public oopDesc { private: // Conversion uintptr_t value() const { return (uintptr_t) this; } public: // Constants enum { age_bits = 4, lock_bits = 2, biased_lock_bits = 1, max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits, hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, cms_bits = LP64_ONLY(1) NOT_LP64(0), epoch_bits = 2 }; // The biased locking code currently requires that the age bits be // contiguous to the lock bits. enum { lock_shift = 0, biased_lock_shift = lock_bits, age_shift = lock_bits + biased_lock_bits, cms_shift = age_shift + age_bits, hash_shift = cms_shift + cms_bits, epoch_shift = hash_shift }; ...
由于对象头信息只是对象运行时自身的一部分数据,相比实例数据部分,头部分属于与业务无关的额外存储成功。为了提升对象对堆空间的复用效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽可能多的信息,它会根据对象的状态复用本身的存储空间。
对于上述源码,mark word中字段枚举意义解释以下:
hash: 保存对象的哈希码
age: 保存对象的分代年龄
biased_lock: 偏向锁标识位
lock: 锁状态标识位
JavaThread*: 保存持有偏向锁的线程ID
epoch: 保存偏向时间戳
锁标记枚举的意义解释以下:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; padding: 0px; margin: 0px; line-height: 1.42857; color: rgb(51, 51, 51); word-break: break-all; word-wrap: break-word; border: 1px solid rgb(204, 204, 204); border-radius: 4px;">locked_value = 0,//00 轻量级锁
unlocked_value = 1,//01 无锁
monitor_value = 2,//10 监视器锁,也叫膨胀锁,也叫重量级锁
marked_value = 3,//11 GC标记
biased_lock_pattern = 5 //101 偏向锁</pre>
实际上,markword的设计很是像网络协议报文头:将mark word划分为多个比特位区间,并在不一样的对象状态下赋予不一样的含义, 下图是来自网络上的一张协议图。
上述协议字段正对应着源码中所列的枚举字段,这里简要进行说明一下。
对象的hash码,hash表明的并不必定是对象的(虚拟)内存地址,但依赖于内存地址,具体取决于运行时库和JVM的具体实现,底层由C++实现,实现细节参考OpenJDK源码。但能够简单的理解为对象的内存地址的整型值。
对象分代GC的年龄。分代GC的年龄是指Java对象在分代垃圾回收模型下(如今JVM实现基本都使用的这种模型),对象上标记的分代年龄,当该年轻代内存区域空间满后,或者到达GC最达年龄时,会被扔进老年代等待老年代区域满后被FullGC收集掉,这里的最大年龄是经过JVM参数设定的:-XX:MaxTenuringThreshold ,默认值是15。那这个年龄具体是怎么计算的呢?
下图展现了该年龄递增的过程:
1. 首先,在对象被new出来后,放在Eden区,年龄都是0
2. 通过一轮GC后,B0和F0被回收,其它对象被拷贝到S1区,年龄增长1,注:若是S1不能同时容纳A0,C0,D0,E0和G0,将被直接丢入Old区
3. 再经一轮GC,Eden区中新生的对象M0,P0及S1中的B1,E1,G1不被引用将被回收,而H0,K0,N0及S1中的A1,D1被拷贝到S2区中,对应年龄增长1
4. 如此通过二、3过滤循环进行,当S1或S2满,或者对象的年龄达到最大年龄(15)后仍然有引用存在,则对象将被转移至Old区。
锁标记位,此锁为重量级锁,即对象监视器锁。Java在使用synchronized
关键字对方法或块进行加锁时,会触发一个名为“objectMonitor
”的监视器对目标代码块执行加锁的操做。固然synchronized
方法和synchronized
代码块的底层处理机制稍有不一样。synchronized
方法编译后,会被打上“ACC_SYNCHRONIZED
”标记符。而synchronized
代码块编译以后,会在同步代码的先后分别加上“monitorenter
”和“monitorexit
”的指令。当程序执行时遇到到monitorenter
或ACC_SYNCHRONIZED
时,会检测对象头上的lock标记位,该标记位被若是被线程初次成功访问并设值,则置为1,表示取锁成功,若是再次取锁再执行++
操做。在代码块执行结束等待返回或遇到异常等待抛出时,会执行monitorexit
或相应的放锁操做,锁标记位执行--
操做,若是减到0,则锁被彻底释放掉。关于objectMonitor
的实现细节,参考JDK源码
注意,在jdk1.6以前,synchronized
加锁或取锁等待操做最终会被转换为操做系统中线程操做原语,如激活、阻塞等。这些操做会致使CPU线程上下文的切换,开销较大,所以称之为重量级锁。但后续JDK版本中对其实现作了大幅优化,相继出现了轻量级锁,偏向锁,自旋锁,自适应自旋锁,锁粗化及锁消除等策略。这里仅作简单介绍,不进行展开。
如图所示,展现了这几种锁的关系:
轻量级锁,如上图所示,是当某个资源在没有竞争或极少竞争的状况下,JVM会优先使用CAS操做,让线程在用户态去尝试修改对象头上的锁标记位,从而避免进入内核态。这里CAS尝试修改锁标记是指尝试对指向当前栈中保存的lock record的线程指针的修改,即对biased_lock标记作CAS修改操做。若是发现存在多个线程竞争(表现为CAS屡次失败),则膨胀为重量级锁,修改对应的lock标记位并进入内核态执行锁操做。注意,这种膨胀并不是属于性能的恶化,相反,若是竞争较多时,CAS方式的弊端就很明显,由于它会占用较长的CPU时间作无谓的操做。此时重量级锁的优点更明显。
偏向锁,是针对只会有一个线程执行同步代码块时的优化,若是一个同步块只会被一个线程访问,则偏向锁标记会记录该线程id,当该线程进入时,只用check 线程id是否一致,而无须进行同步。锁偏向后,会依据epoch(偏向时间戳)及设定的最大epoch判断是否撤销锁偏向。
自旋锁大意是指线程不进入阻塞等待,而只是作自旋等待前一个线程释放锁。不在对象头讨论范围之列,这里不作讨论。
实例数据instance Data是占用堆内存的主要部分,它们都是对象的实例字段。那么计算这些字段的大小,主要思路就是根据这些字段的类型大小进行求和的。字段类型的标准大小,如Java对象格式概述中表格描述的,除了引用类型会受CPU架构及是否开启指针压缩影响外,其它都是固定的。所以计算起来比较简单。但实际情其实并不这么简单,例如以下对象:
class People{ int age = 20; String name = "Xiaoming"; } class Person extends People{ boolean married = false; long birthday = 128902093242L; char tag = 'c'; double sallary = 1200.00d; }
Person对象实例数据的大小应该是多少呢?这里假设使用64位机器,采用指针压缩,则对象头的大小为:8(_mark)+4(_klass) = 12
而后实例数据的大小为: 4(age)+4(name) + 8(birthday) + 8(sallary) + 2(tag) + 1(married) = 27
所以最终的对象自己大小为:12+27+1(padding) = 40字节
注意,为了尽可能减小内存空间的占用,这里在计算的过程当中须要遵循如下几个规则:
> <pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; padding: 0px; margin: 0px; line-height: 1.42857; color: rgb(51, 51, 51); word-break: break-all; word-wrap: break-word; background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 4px;">/** > > * 1: 除了对象总体须要按8字节对齐外,每一个成员变量都尽可能使自己的大小在内存中尽可能对齐。好比 int 按 4 位对齐,long 按 8 位对齐。 > > * 2:类属性按照以下优先级进行排列:长整型和双精度类型;整型和浮点型;字符和短整型;字节类型和布尔类型,最后是引用类型。这些属性都按照各自的单位对齐。 > > * 3:优先按照规则一和二处理父类中的成员,接着才是子类的成员。 > > * 4:当父类中最后一个成员和子类第一个成员的间隔若是不够4个字节的话,就必须扩展到4个字节的基本单位。 > > * 5:若是子类第一个成员是一个双精度或者长整型,而且父类并无用完8个字节,JVM会破坏规则2,按照整形(int),短整型(short),字节型(byte),引用类型(reference)的顺序,向未填满的空间填充。 > > */</pre>
最后计算引用类型字段的实际大小:"Xiaoming",按字符串对象的字段进行计算,对象头12字节,hash字段4字节,char[] 4字节,共12+4+4+4(padding) = 24字节,其中char[]又是引用类型,且是数组类型,其大小为:对象头12+4(length) + 9(arrLength) * 2(char) +4(padding) = 40字节。
因此综上所述,一个Person对象占用内存的大小为104字节。
一个比较明显的问题是,在64位机器上,若是开启了指针压缩后,则引用只占用4个字节,4字节的最大寻址空间为2^32=4GB, 那么如何保证能知足寻址空间大于4G的需求呢?
开启指针压缩后,实际上会压缩的对象包括:每一个Class的属性指针(静态成员变量)及每一个引用类型的字段(包括数组)指针,而本地变量,堆栈元素,入参,返回值,NULL这些指针不会被压缩。在开启指针压缩后,如前文源码所述,markWord中的存储指针将是_compressed_klass,对应的类型是narrowOop,再也不是wideKlassOop了,有什么区别呢?
wideKlassOop
和narrowOop
都指向InstanceKlass对象,其中narrowOop指向的是通过压缩的对象。简单来讲,wideKlassOop能够达到整个寻址空间。而narrowOop虽然达不到整个寻址空间,但它面对也再也不是个单纯的byte地址,而是一个object,也就是说使用narrowOop后,压缩后的这4个字节表示的4GB其实是4G个对象的指针,大概是32GB。JVM会对对应的指针对象进行解码, JDK源码中,oop.hpp源码文件中定义了抽象的编解码方法,用于将narrowOop解码为一个正常的引用指针,或将一下正常的引用指针编码为narrowOop:
// Decode an oop pointer from a narrowOop if compressed. // These are overloaded for oop and narrowOop as are the other functions // below so that they can be called in template functions. static oop decode_heap_oop_not_null(oop v); static oop decode_heap_oop_not_null(narrowOop v); static oop decode_heap_oop(oop v); static oop decode_heap_oop(narrowOop v); // Encode an oop pointer to a narrow oop. The or_null versions accept // null oop pointer, others do not in order to eliminate the // null checking branches. static narrowOop encode_heap_oop_not_null(oop v); static narrowOop encode_heap_oop(oop v);
对齐填充是底层CPU数据总线读取内存数据时的要求,例如,一般CPU按照字单位读取,若是一个完整的数据体不须要对齐,那么在内存中存储时,其地址有极大可能横跨两个字,例如某数据块地址未对齐,存储为1-4,而cpu按字读取,须要把0-3字块读取出来,再把4-7字块读出来,最后合并舍弃掉多余的部分。这种操做会不少不少,且很频繁,但若是进行了对齐,则一次性便可取出目标数据,将会大大节省CPU资源。
在hotSpot虚拟机中,默认的对齐位数是8,与CPU架构无关,以下代码中的objectAlignment
:
// Try to get the object alignment (the default seems to be 8 on Hotspot, // regardless of the architecture). int objectAlignment = 8; try { final Class<?> beanClazz = Class.forName("com.sun.management.HotSpotDiagnosticMXBean"); final Object hotSpotBean = ManagementFactory.newPlatformMXBeanProxy( ManagementFactory.getPlatformMBeanServer(), "com.sun.management:type=HotSpotDiagnostic", beanClazz ); final Method getVMOptionMethod = beanClazz.getMethod("getVMOption", String.class); final Object vmOption = getVMOptionMethod.invoke(hotSpotBean, "ObjectAlignmentInBytes"); objectAlignment = Integer.parseInt( vmOption.getClass().getMethod("getValue").invoke(vmOption).toString() ); supportedFeatures.add(JvmFeature.OBJECT_ALIGNMENT); } catch (Exception e) { // Ignore. } NUM_BYTES_OBJECT_ALIGNMENT = objectAlignment;
能够看出,经过HotSpotDiagnosticMXBean.getVMOption("ObjectAlignmentBytes").getValue()
方法能够拿到当前JVM环境下的对齐位数。
注意,这里的HotSpotDiagnosticMXBean
是JVM提供的JMX中一种可被管理的资源,即HotSpot信息资源。
前文所述都是源码+理论,其实Hotspot为咱们提供了一种工具能够方便的用来查询运行时对象的Oops结构,即SA Hotspot Debuger
,简称HSDB. 其中SA指“Serviceability Agent
”,它是一个JVM服务工具集的Agent,它本来是sun公司用来debug Hotspot的工具,如今开放给开发者使用,可以查看Java对象的oops、查看类信息、线程栈信息、堆信息、方法字节码和JIT编译后的汇编代码等。SA提供的入口在$JAVA_HOME/lib/sa-jdi.jar中,包含了不少工具,其中最经常使用的工具就是HSDB。
下面演示一下HSDB的使用——
1. 先准备以下代码并运行:
public class Obj{ private int age; private long height; private boolean married; private String name; private String addr; private String sex; ... get/set }
package yp.tools; /** * @author yepei * @date 2018/05/14 * @description */ public class HSDBTest { public static void main(String[] args) throws InterruptedException { Obj o = new Obj(20, 175, false, "小明", "浙江杭洲", "男"); Thread.sleep(1000 * 3600); System.out.println(o); } }
2. 执行jps命令,获取当前运行的Java进程号:
3. 启动HSDB,并添加目标进程:
`sudo java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB`
能够看到当前Java进程中的线程信息:
双击指定线程,能够查看到当前线程对象的Oop结构信息,能够看到线程对象头也是包含_mark和_metadata两个协议字段的:
点击上方的栈图标,能够查询当前线程的栈内存:
那么如何查看当前线程中用户定义的类结存储信息呢?
先到方法区去看一下类信息吧
Tools——Class Browser,搜索目标类
能够看到该类对应的对象的各个字段的偏移量,最大的是36,String类型,意味着该对象自己的大小就是36+4 = 40字节。同时,下方能够看到这个类相关的函数表、常量池信息。
要查看对象信息,从Tools菜单,打开Object Histogram
在打开的窗口中搜索目标类:yp.tools.Obj
双击打开:
点击Inspect查看该对象的Oop结构信息:
如上图所示便是对象Obj的Oop结构,对象头包含_mark与表明class指针的_metadata。示例中的类没有并发或锁的存在,因此mark值是001,表明无锁状态。
除此以外,HSDB还有其它一些不错的功能,如查看反编译信息、根据地址查找对象、crash分析、死锁分析等。
本文围绕“计算Java对象占用内存大小”这一话题,简要介绍了直接计算指定对象在内存中大小的三种方法:使用Instrumentation、Unsafe或第三方工具(RamUsageEstimator)的方式,其中Instrumentation和Unsafe计算精确,但使用起来不太方便,Instrumentation须要以javaagent代理的方式启动,而Unsafe只能计算指定对象的每一个字段的地址起始位置偏移量,须要手工递归并增长padding才能完整计算对象大小,使用RamUsageEstimator能够很方便的计算对象自己或对象引用树总体的大小,但其并不是直接基于对象的真实内存地址而计算的,而是经过已知JVM规则和数据类型的标准大小推算的,存在计算偏差的可能性。
为了揭开Java对象在堆内存中存储格式的面纱,结合OpenJDK源码,本文着重讨论了Java对象的格式:对象头、实例数据及对齐填充三部分。其中对象头最为复杂,包含_mark、_klass以及_length(仅数组类型)的协议字段。其中的mark word字段较为复杂,甚至涉及了OOP-Klass模型、hash、gc、锁的原理及指针压缩等知识。
最后,从实践的方面入手,介绍了JDK自带的Hotspot Debuger工具——HSDB的使用,透过它可以让咱们更直观的查看运行中的java对象在内存中的存在形式和状态,如对象的oops、类信息、线程栈信息、堆信息、方法字节码和JIT编译后的汇编代码等。
本文查询了一些资料,并参考了OpenJDK源码。可能会有些不正确的地方敬请指正,欢迎探讨。
本文做者:一人浅醉
本文为云栖社区原创内容,未经容许不得转载。