简介
在之前的文章中,我们介绍了使用JOL这一神器来解析java类或者java实例在内存中占用的空间地址。
今天,我们会更进一步,剖析一下在之前文章中没有讲解到的更深层次的细节。一起来看看吧。
对象和其隐藏的秘密
java.lang.Object大家应该都很熟悉了,Object是java中一切对象的鼻祖。
接下来我们来对这个java对象的鼻祖进行一个详细的解剖分析,从而理解JVM的深层次的秘密。
工具当然是使用JOL:
@Slf4j
public class JolUsage {
@Test
public void useJol(){
log.info("{}", VM.current().details());
log.info("{}", ClassLayout.parseClass(Object.class).toPrintable());
log.info("{}", ClassLayout.parseInstance(new Object()).toPrintable());
}
}
代码很简单,我们打印JVM的信息,Object class和一个新的Object实例的信息。
看下输出:
[main] INFO com.flydean.JolUsage - # Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
10:27:32.311 [main] INFO com.flydean.JolUsage - java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
10:27:32.312 [main] INFO com.flydean.JolUsage - java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 86 06 00 00 (10000110 00000110 00000000 00000000) (1670)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
从上面的结果我们知道,在64位的JVM中,一个Object实例是占用16个字节。
因为Object对象中并没有其他对象的引用,所以我们看到Object对象只有一个12字节的对象头。剩下的4个字节是填充位。
Object对象头
那么这12字节的对象头是做什么用的呢?
如果想要深入了解这12字节的对象头,当然是要去研读一下JVM的源码:src/share/vm/oops/markOop.hpp。
有兴趣的小伙伴可以去看看。如果没有兴趣,没关系,这里给大家一个张总结的图:
javaObject对象的对象头大小根据你使用的是32位还是64位的虚拟机的不同,稍有变化。这里我们使用的是64位的虚拟机为例。
Object的对象头,分为两部分,第一部分是Mark Word,用来存储对象的运行时数据比如:hashcode,GC分代年龄,锁状态,持有锁信息,偏向锁的thread ID等等。
在64位的虚拟机中,Mark Word是64bits,如果是在32位的虚拟机中Mark Word是32bits。
第二部分就是Klass Word,Klass Word是一个类型指针,指向class的元数据,JVM通过Klass Word来判断该对象是哪个class的实例。
且慢!
有的小伙伴可能发现了问题,之前我们用JOL解析Object对象的时候,Object head大小是12字节,也就是96bits,这里怎么写的是128bits?
没错,如果没有开启COOPs就是128bits,如果开启了COOPs,那么Klass Word的大小就从64bits降到了32bits。
还记得我们之前讲的COOPs吗?
COOPs就是压缩对象指针技术。
对象指针用来指向一个对象,表示对该对象的引用。通常来说在64位机子上面,一个指针占用64位,也就是8个字节。而在32位机子上面,一个指针占用32位,也就是4个字节。
实时上,在应用程序中,这种对象的指针是非常非常多的,从而导致如果同样一个程序,在32位机子上面运行和在64位机子上面运行占用的内存是完全不同的。64位机子内存使用可能是32位机子的1.5倍。
而压缩对象指针,就是指把64位的指针压缩到32位。
怎么压缩呢?64位机子的对象地址仍然是64位的。压缩过的32位存的只是相对于heap base address的位移。
我们使用64位的heap base地址+ 32位的地址位移量,就得到了实际的64位heap地址。
对象指针压缩在Java SE 6u23 默认开启。在此之前,可以使用-XX:+UseCompressedOops来开启。
数组对象头
java中有一个非常特别的对象叫做数组,数组的对象头和Object有什么区别吗?
我们用JOL再看一次:
log.info("{}",ClassLayout.parseClass(byte[].class).toPrintable());
log.info("{}",ClassLayout.parseInstance("www.flydean.com".getBytes()).toPrintable());
上面的例子中我们分别解析了byte数组的class和byte数组的实例:
10:27:32.396 [main] INFO com.flydean.JolUsage - [B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 16 (object header) N/A
16 0 byte [B.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
10:27:32.404 [main] INFO com.flydean.JolUsage - [B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 22 13 07 00 (00100010 00010011 00000111 00000000) (463650)
12 4 (object header) 0f 00 00 00 (00001111 00000000 00000000 00000000) (15)
16 15 byte [B.<elements> N/A
31 1 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 1 bytes external = 1 bytes total
看到区别了吗?我们发现数组的对象头是16字节,比普通对象的对象头多出了4个字节。这4个字节就是数组的长度。
整个对象的结构
好了,写到这里我们来总结一下,java对象的结构可以分为普通java对象和数组对象两种:
数组对象在对象头中多了一个4字节的长度字段。
大家看到最后的字节是padding填充字节,为什么要填充呢?
相关推荐
**为对象分配内存:** 在JVM中,对象的内存分配主要发生在堆中。对象的内存分配策略包括对象优先在Eden区分配、大对象直接进入老年代等。 **处理并发安全问题:** 在多线程环境下,为了防止多个线程同时为同一对象...
类加载是指将类的`.class`文件中的二进制数据读入到内存中,并在堆区创建一个`java.lang.Class`对象来封装类在方法区内的数据结构的过程。类加载主要包括三个阶段: - **加载**:找到类的二进制数据并将其加载到...
### JVM虚拟机面试题知识点详解 #### 一、JVM运行时内存结构 JVM运行时数据区(Runtime Data Area)主要包括以下几部分: 1. **程序计数器(Program Counter Register)**:是一块较小的内存空间,当前线程所执行的...
2. **堆(Heap)**:所有对象实例和数组都在堆中分配内存,是JVM中最大的一块内存区域,支持垃圾回收。 3. **栈(Stack)**:每个线程都有一个独立的栈,用于存储局部变量、方法调用信息等。 4. **程序计数器...
- **堆(Heap)**:所有对象实例都在堆中分配内存,是所有线程共享的区域。 - **方法区(Method Area)**:存储类和接口的信息,如类名、父类名、是否为类或接口等。 5. **方法区内容** 方法区主要存储以下内容...
- **跨平台性**:Java程序可以在任何安装了Java虚拟机(JVM)的平台上运行,这是由于Java编译后的字节码文件(.class)能够在不同系统上的JVM上运行而实现的。 - **面向对象**:Java支持面向对象编程,具备封装、...
1. **运行时数据区**:Java虚拟机管理的内存主要分为以下几个部分: - **方法区(Method Area)**:存储类的信息(如类名、字段、方法等)、常量、静态变量等。每个JVM实例只有一个方法区,被所有线程共享。方法区是...
句柄访问方式是指在Java堆外专门划分出一块内存作为句柄池,句柄中存储了对象实例数据与类型数据各自的具体地址信息。直接指针则是直接保存对象的地址。 #### 四、垃圾回收机制 1. **垃圾回收的两种判定方法**: ...
JRE中的核心类库提供了一系列基础功能,如集合、网络、I/O等。 3. JVM运作原理 JVM是Java平台的核心,它实现了Java的跨平台特性。JVM通过加载、验证、准备、解析、执行字节码来运行Java程序。JIT(Just-In-Time)...
在JAVA中,异常通常被视为程序执行过程中的一种特殊事件,它打断了正常的程序流程。当程序违反了JAVA的语义规则时,如数组下标越界、访问null对象等,JAVA虚拟机会将其表示为一个异常。JAVA的异常处理主要包括以下几...
- Object类中的方法,可以在对象被垃圾回收前做一些清理工作。 - 通常用于释放系统资源,如关闭文件等。 - 可以被子类重写。 #### 四、heap与stack的区别 - **栈(Stack)**: - 是一种先进后出(LIFO)的数据...
Java中的super关键字详解 Java中的super关键字是用于访问父类的成员变量或成员方法的关键字。super关键字的使用可以让子类访问父类的成员变量或成员方法,从而实现继承的效果。 1. super关键字的使用 super关键字...
3. **finalize**:是`Object` 类中的一个方法,用于在对象被垃圾回收之前执行清理工作,但在Java 9之后已被废弃。 以上是Java面试中常见的知识点,涵盖了异常处理、多态、垃圾回收、线程同步、编程风格等多个方面,...
在Java中,GC通常使用有向图结构来记录和管理堆(heap)中的所有对象,以此来判断哪些对象是可达的,哪些是不可达的。当GC发现某些对象不再可达时,它们的内存空间就会被回收。 尽管GC的基本任务是明确的,但Java规范...
在Java中,对象通过引用进行操作,而不是直接操作对象本身。 #### Reference (引用) 引用是指向对象的一个地址,而不是对象本身。在Java中,所有的对象都是通过引用来操作的。 #### Reflection (反射) 反射是...
- **finalize()方法**:在对象被垃圾回收之前由Java虚拟机调用,给对象最后一次机会清理资源。 2. **final** - **final关键字**:用于声明不可变的变量或类。 - **final类**:声明为final的类不能被继承。 - **...
- **finalize**:`finalize`方法是`Object`类中的一个方法,可以在对象被垃圾回收前由JVM调用。开发者可以覆盖此方法来执行清理工作,如关闭不再使用的系统资源。需要注意的是,`finalize`方法并不是必须被调用的,...