likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 21

认识Java对象

先了解一下对象的组成

对象的组成

认识Java对象,内存布局,new对象的过程 可以看到,一个对象由:对象头,实例数据,对齐填充组成

1、对象头

markword
锁状态29 bit 或 61 bit1 bit 是否是偏向锁?2 bit 锁标志位
无锁31bit的Hashcode001
偏向锁线程ID101
轻量级锁指向栈中锁记录的指针此时这一位不用于标识偏向锁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对象的过程

一、检查是否类加载

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

二、为实例分配内存

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

方案是: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)初始化实例变量阶段保证

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

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

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

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

因此:

public static int a = 1;

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

完全可以这样写:

static {  a = 2; }
public static int a = 1;

此时访问a为1

交换这两行代码的位置:

public static int a = 1;
static {  a = 2; }

此时访问a为2

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

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

对象如何被访问

句柄

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

直接指针

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

对比

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

HotSpot VM采用直接指针

引用类型

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

强引用(StrongReference)

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

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

Object obj = new Object();

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

软引用(SoftReference)

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

弱引用(WeakReference)

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

ThreadLocal的key是弱引用

虚引用(PhantomReference)

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

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

查看Java对象的内存布局

JDK自带

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

jol工具

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

Instrumentation

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

<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>
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类下已经做了实现

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内存区域详解(重点)