java对象内存模型

jvm运行时内存分为程序计数器、java虚拟机栈、本地方法栈、方法区、堆这几个区域。那他们是怎么存储对象的,对象内部信息又是怎么存放的?带着这几个问题我们谈论下对象的创建过程,存储和访问。
我们先上一段简单的代码,看看对象的信息如何存储的
//A类的信息、方法、字段信息存储在方法区里
public class A{
    private static int i=0;    //静态变量,存储在方法区(这也是为什么静态变量能在多个对象共享的原因)
    private final int f=0;    //常量,存储在方法区

    private static B b= new B();    //静态变量,存储在方法区(方法区存b对象的引用,b实例存储在堆)

    private int j;       //实例变量,存储在堆
    public String showMethod(String message){
        B b1= new B();

        message = b1.toString();
        System.out.println(message);
        return message;
    }
    //以上showMethod方法存储内的变量,返回值地址,b1变量引用均是存储在java虚拟机栈中
}
通过上面介绍我们大致了解的对象内信息是如何存储在jvm中的,那对象本身内存结构是怎样的呢?就像公司里的所有员工都有一个住址,而公司其实也有一个地址,并且公司也是有一定结构组成的。
对象在内存的存储布局分为3个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头包含2部分信息:
(1)对象运行时数据,包括了哈希码、GC分代年龄、线程持有的锁、偏向锁ID等,这部分数据根据不同系统暂用空间也不一样(32bit和64bit),官方称为Mark World
(2)对象类型数据指针,指向类型元数据,是确定对象所属对象类型的指针。
实例数据:对象实例的数据,很好理解
对齐填充:非必须,占位用。存在原因是java内存大小必须是8字节整数倍。

介绍完对象存储,下面来看对象的创建过程

类加载检查:
检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程。

为对象分配内存
对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

2.1 根据Java堆中是否规整有两种内存的分配方式
(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定)

  • 指针碰撞(Bump the pointer):
    Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew等收集器。
  • 空闲列表(Free List):
    Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于Mark-Sweep算法的收集器。

2.2 分配内存时解决并发问题的两种方案:
对象创建在虚拟机中时非常频繁的行为,即使是仅仅修改一个指针指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

  • 对分配内存空间的动作进行同步处理—实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
  • 把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。

内存空间初始化
虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用了TLAB,这一工作过程也可以提前至TLAB分配时进行。
内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

对象设置
虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

<init>
在上面的工作都完成之后,从虚拟机的角度看,一个新的对象已经产生了。
但是从Java程序的角度看,对象的创建才刚刚开始<init>方法还没有执行,所有的字段都还是零。
所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。

我们知道对象的引用存储在java虚拟机栈中,那么栈是如何定位到对象地址的,这里分两种方式:句柄和直接地址引用
(1)使用句柄,如下图所示,虚拟器在java堆中创建对象句柄池,对象句柄池保存了对象数据和对象类型信息的指针,而栈上引用保存了对象句柄的地址。
《java对象内存模型》

(2)直接使用对象内存地址,如图,reference保存的是对象的实例地址,对象实例地址保存对象类型地址信息。
《java对象内存模型》

两种方式优缺点:
句柄方式好处是发生垃圾收集时,对象地址改变时,栈上reference不许改变,只需要改变句柄池中指针即可,缺点就是多了次指针访问
直接地址访问方式好处是节省一次指针访问,缺点是reference在垃圾回收时需要修改。
问题:JVM有没有参数指定在特定区域不同的对象访问方式(如垃圾收集少老年代可以直接地址,新生代使用句柄方式)??

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/wangweiyan89/article/details/51234367
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞