JVM中的内存与对象

虚拟机内存的各个区域

按照私有和共享对区域进行区分

  • 私有
    • 程序计数器:当前线程所执行的字节码的行号指示器。在Java虚拟机里面,字节码解释器就是通过这个行号来读取下一条要运行的指令,也可以说是记录了正在执行的虚拟机字节码指令的地址。在线程切换还有基础的逻辑(if、循环、switch)都需要用到这个程序计数器。每条线程都有一个独立的程序计数器。
    • 虚拟机栈:每个方法在创建的同时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、方法出口等。也就是说当我们调用某个方法,那么就会有个栈帧入栈,当执行完成了,该栈帧就会出栈。也可把虚拟机栈叫做局部变量表,存放了基本数据类型和reference类型,returnAddress类型。
    • 本地方法栈:与虚拟机栈非常类似,不过本地方法栈执行的是native方法。
  • 共享
    • 堆:被所有线程共享的内存区域,唯一的目的就是存放对象实例,也是GC作用的主要区域。
    • 方法区:存储已被虚拟机加载的类信息、常量、静态变量等数据。
      • 运行时常量池:存放编译期生成的各种字面量和符号引用。也就是加载类的各种描述信息。

对象的创建

  1. 检查常量池中是否有该类的符号引用。并查看该类是否被加载、初始化,如果没有那么需要先执行类加载过程。
  2. 为对象分配一块确定大小的内存,有两种方式,根据堆是否规整来决定,决定堆是否规整是由采用的GC方式是否带有压缩整理功能。

    • 指针碰撞:如果Java堆中内存是绝对规整的,那么会将堆分成两部分,一部分是已经使用的内存,一部分是未使用的内存,分界线就是指针,如果需要分配内存那么移动指针即可。
    • 空闲列表:如果不是绝对规整的,那么就需要创建两个列表来维护。

      需要考虑内存的分配是否有线程安全问题。一个方案是进行同步处理,采用CAS配上失败重试的方法来保证更新操作的原子性。另一种方案则是每个线程在堆中预先分配一小块内存。称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程需要创建对象,先从TLAB上分配,当TLAB分配光了,再用同步的方式分配新的TLAB。

  3. 分配完内存之后,将分配到的内存空间都初始化为零值。保证对象的实例字段可以不被赋值就直接使用。

  4. 设置一些信息,例如类的元数据信息,哈希码,GC分代年龄等等。
  5. 执行init方法,按照我们的意愿进行初始化。

对象的内存布局

对象的内存布局主要有三块:对象头、实例数据、对齐填充。

对象头:

第一部分为Mark Word,用于存储对象自身的运行时数据,例如哈希码、GC分代年龄、线程持有的锁等等。由于对象头信息与对象自身的数据没有关系,因此属于额外成本,因为被设计成非固定的数据结构以便能用最小的空间存储尽量多的信息。

第二部分为类型指针,就是对象指向它的类元数据的指针,通过这个指针能知道这个这个对象是哪个类的实例。

实例数据:该数据为对象存储的有效信息。也就是代码中所写的一切,包括父类继承的或者子类中定义的各种信息。

对齐填充:因为HotSpot VM的自动内存管理系统要求对象的大小必须是8字节的整数倍,对象头部分刚好是8字节的整数倍,如果实例数据不是8字节的整数倍,需要通过对齐填充部分进行补齐操作。