认识Java对象,内存布局,new对象的过程

认识Java对象

先了解一下对象的组成

对象的组成

可以看到,一个对象由:对象头,实例数据,对齐填充组成

1、对象头

markword

涉及到synchronized底层实现原理,你可以暂时简单地理解为一些关键的额外信息,可以参考:synchronized的轻量级锁居然不会自旋?深度解析synchronized实现原理

锁状态 29 bit 或 61 bit 1 bit 是否是偏向锁? 2 bit 锁标志位
无锁 31bit的Hashcode 0 01
偏向锁 线程ID 1 01
轻量级锁 指向栈中锁记录的指针 此时这一位不用于标识偏向锁 00
重量级锁 指向互斥量(重量级锁)的指针 此时这一位不用于标识偏向锁 10
GC标记 此时这一位不用于标识偏向锁 11
Class指针

4字节的指针(指向类元信息地址,class),用于访问Class对象。

默认开启指针压缩,所以是4字节,关闭的话为8字节。

数组的length

这个好理解,数组的.length就是拿到这个信息

这里length四个字节,也解释了一个数组的最大长度,是max_int。

2、实例数据

就是类自己的类似private String name = "123"这样的信息。

像父类的private的属性,也是有的,但访问不了。

3、对齐填充

对齐填充使得对象实例的字节数是8的倍数

64位JVM的寻址空间更大,但是会带来性能的损耗;大多数计算机都是高效的64位处理器,顾名思义,一次能处理64位的指令,即8个字节的数据,HotSpot VM的自动内存管理系统也就遵循了这个要求,这样子性能更高,处理更快。

因此,new一个对象,实际就是要处理上述三大部分。一般有:

  • markword
  • Class对象
  • 实例数据

new对象的过程

一、检查是否类加载

检查常量池中是否能定位到这个类的「符号引用」。

如果没有Class对象,会先执行类加载过程的1~5步骤。类加载的过程可以参考:JVM架构之类加载系统,深入理解类加载器

二、为实例分配内存

首先因为堆全局唯一,因此需要保证线程安全:

方案是:CAS+TLAB

TLAB:Thread Local Allocation Buffer。就是在Eden区直接划分一块给某个线程私有。后续的new对象都在这块私有区域分配。

划分区域也会线程不安全,也需要CAS或锁,但是一种锁粗化的原理,比单单分配一个对象效率要高得多。

在TLAB不够时,采用CAS的方式分配内存。

CAS:compare and swap,保证线程安全

然后开始具体地分配内存

对象所需的内存大小在类加载完成后便可确定,具体如何分配有两种方式:

没有内存碎片:指针碰撞

用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置

有内存碎片:空闲列表

虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。

有没有内存碎片取决于GC算法,清除有内存碎片,复制,整理没有内存碎片

三、初始化实例变量

将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值(一般是0)。

四、设置对象头

即初始化mark word,class指针等信息

对象头是下图绿色的部分

五、初始化

先初始化父类再初始化子类。

初始化时先执行实例代码块 然后是构造方法

这样一个真正可用的对象才算完全产生出来

开发视角:代码块/构造方法/声明调用顺序

我们现在已经学了Class对象的加载,实例对象的加载,这里来梳理一下,父子类的静态(非静态)代码块,构造方法,静态(非静态)字段的调用顺序

Class加载完全早于实例对象的加载,因此:

  1. 父类的:静态变量声明,静态代码块
  2. 子类的:静态变量声明,静态代码块

这里的1~2是类加载的(5)初始化阶段

  1. 父类的:实例变量声明,普通语句块
  2. 父类的:构造函数
  3. 子类的:实例变量声明,普通语句块
  4. 子类的:构造函数

这里的3~6是new对象实例的(5)初始化阶段

没有被声明,代码块,构造函数指定的变量就是默认值(一般为零值)

  • 对于静态的字段由类加载步骤(3)准备阶段保证
  • 对于普通的字段由new对象步骤(3)初始化实例变量阶段保证

声明语句和代码块哪个先执行

为什么把声明和代码块放在同一行内?它们之间的顺序如何?

实际上,这取决于代码的位置

我们知道在初始化之前,无论是静态字段,还是普通字段,都会为它们分配空间并赋予默认值,一般为零值。

因此:

arduino 复制代码
public static int a = 1;

像这样的语句,「为a分配空间」 与 「声明a的值」 ,并不会被一起执行。

完全可以这样写:

ini 复制代码
static {  a = 2; }
public static int a = 1;

此时访问a为1

交换这两行代码的位置:

ini 复制代码
public static int a = 1;
static {  a = 2; }

此时访问a为2

普通字段完全一样,多个代码块也是这样

因此,变量声明与代码块的执行顺序取决于代码的位置顺序

对象如何被访问

句柄

在堆中划分一块区域作为句柄池,指针指向句柄,句柄指向堆内存空间

直接指针

直接指针就是指向堆内存空间的指针

对比

直接指针显然访问速度更快。句柄的好处是对象被移动时(比如GC),只改变句柄中实例数据指针,reference本身不用改变。

HotSpot VM采用直接指针

引用类型

直接指针提供的功能太少,因此JDK1.2之后Java提供了四个级别的引用,以下按引用强度排序:

强引用(StrongReference)

最普遍的引用,垃圾回收器绝不会回收它,宁愿抛出 OutOfMemoryError 错误,使程序异常终止。

new出来的对象的引用都是强引用。像下面这行代码,obj就是个强引用,存储在栈帧的局部变量表中。

ini 复制代码
Object obj = new Object();

执行 obj=null;后,这个obj原本指向的Object实例才可能被GC

软引用(SoftReference)

如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。可有可无。可用来实现内存敏感的高速缓存。

弱引用(WeakReference)

值得被回收。一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

ThreadLocal的key是弱引用

虚引用(PhantomReference)

虚引用:主要用来跟踪对象被垃圾回收的活动。虚引用不会决定GC机制对一个对象的回收权。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

查看Java对象的内存布局

JDK自带

java 复制代码
System.setProperty("java.vm.name","Java HotSpot(TM) ");
System.out.println(ObjectSizeCalculator.getObjectSize(3L));

jol工具

java 复制代码
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>
java 复制代码
// 基本使用
Object o = new Object();
// 实例信息
System.out.println(ClassLayout.parseInstance(o).toPrintable());
// class类信息
System.out.println(ClassLayout.parseClass(String.class).toPrintable());

Instrumentation

获取Instrumentation对象,先引入依赖项

xml 复制代码
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-agent</artifactId>
    <version>1.12.10</version>
</dependency>
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.12.10</version>
</dependency>
java 复制代码
import net.bytebuddy.agent.ByteBuddyAgent;
import java.lang.instrument.Instrumentation;
	ByteBuddyAgent.install();
	final Instrumentation instrumentation = ByteBuddyAgent.getInstrumentation();
    inst.getObjectSize(obj);

最后来看看Object类,它是Java所有类的父类。

浅析Object类

Object有哪些方法

hashcode,equals,toString,wait,notify,clone

为什么wait/notify在Object内而不是Thread

因为等待/通知机制设计是为了解决线程间通信的。

线程通信是否可执行,是由资源决定的,而非线程。

一个线程使用完毕某个资源,别的线程才能使用。

线程知道自己是因为要获取哪个资源而被阻塞,关注的是资源,而不在乎是因为谁(具体哪个线程)。

输出一个没有重写toString方法的对象会发生什么

输出Person类得到 Person@3c1

输出数组得到 [I@4554617c

输出new Object的对象得到 java.lang.Object@4554617c

一个类没有重写toString方法,也会有这个方法

因为Object类下已经做了实现

typescript 复制代码
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

也就是会输出 类名 + @ + hashcode的值(hashcode16进制)

new 一个Object多少字节

这跟硬件是有关系的。还有是否开启指针压缩等。所以没有确切的答案。但可以肯定的是:一定会是8的倍数。开启指针压缩的情况下,为16字节。8字节mark word,4字节指针,以及对齐填充。另外要区分堆与栈。在栈上也会生成一个4字节的指针。

小结一下:new 一个Object,在本地栈生成一个4字节的指针,指向一块堆内存区域。这块堆内存区域存储了markword,实例数据,和一个指针指向方法区class对象(或者用句柄实现,但sunhotspot是指针实现),以及对齐填充

参考文档

Java内存区域详解(重点)

相关推荐
vx-程序开发4 分钟前
django医院预约挂号系统---附源码23353
java·javascript·spring boot·python·eclipse·django·php
金斗潼关6 分钟前
ysoserial的使用
java
ruleslol10 分钟前
volatile 关键字
java
Codelinghu29 分钟前
AI 写代码越强,程序员越不能只懂代码
后端
卡卡敲码35 分钟前
Skills 撞车了,Agent 怎么选
后端
羑悻35 分钟前
周一早上三件事砸过来,我以为要延期,结果 AI 替我扛了一半!
后端
文艺理科生41 分钟前
3 年,8 种方案,1 次重构:LangChain 记忆方案如何从混乱走向清晰
前端·后端·架构
lilian2331 小时前
Harmony os 技术实战|拼豆制图10:把取消、解析失败和保存失败写成可恢复状态机
java·javascript·华为·harmonyos
Ai拆代码的曹操1 小时前
Agent 做错了怎么办?Self-Critique 机制拆解
后端·agent·ai编程
天性1 小时前
AgentScope Java 源码深读:一次请求如何触发工具、Middleware 和子 Agent
后端