java运行时数据区域

  1. 程序计数器:是一块较小的内存空间,可以理解为当前线程所执行的字节码的行号指示器。由于虚拟机的多线程是通过线程轮流切换来实现的,所以每条线程都拥有一个独立的程序计数器,这类内存区域为“线程私有”的内存。主要作用有两个:
    • 字节码解释器通过改变程序计数器来实现对代码的流程控制:比如顺序执行、选择、循环、异常处理。
    • 多线程情况下,程序计数器用于记录当前线程执行的位置,从而线程被切换回来时就能知道运行到哪了。
  2. 虚拟机栈:虚拟机栈也是线程私有的,它的生命周期和线程相同,它是描述Java方法执行的线程内存模型,每个方法被执行时,虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表。虚拟机栈随着线程的创建而创建,随着线程的死亡而死亡。> Java方法有两种返回方式:
    1. return语句。2.抛出异常。不管哪种返回方式都会导致栈帧被弹出。
  1. 本地方法栈:和虚拟机栈作用相似,区别只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的本地方法服务。
    • JAVA方法 是由JAVA编写的,编译成字节码,存储在class文件中。
    • 本地方法 是由其它语言编写的,编译成和处理器相关的机器代码
  2. Java堆:堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。Java堆是垃圾收集器管理的内存区域。
    java堆可分为新生代、老年代。新生代可分为Eden空间、From Survivor、To Survivor空间等。> Jdk1.7已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用,那么对象可以直接在栈上分配内存
  1. 方法区:用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码等数据。该区域的约束非常宽松,甚至还可以选择不实现垃圾收集。(方法区不等价于永久代)
    • JDK 7 的 Hotspot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终 于完全废弃了永久代的概念,改用与 JRockit、 J9 一样在本地内存中实现的元空间( Metaspace)来代替
  2. 运行时常量池:Runtime Constant Pool是方法区的一部分,除了类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,这部分内容在类加载后放到方法区的运行时常量池中。(String类的intern()方法会把新的常量放入池中)。
  3. 直接内存:该区域不是虚拟机运行时数据区的一部分,但是这部分内存也被频繁地使用,可能导致OutofMemoryError异常出现。

image.png

  1. Java 虚拟机规范 中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflow Error 异常;如果 Java 虚拟机栈容量 可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。

对象的创建

  1. 类加载检查:


    当虚拟机遇到一条字节码new指令时,先检查是否能在常量池定位到一个类的符号引用,并检查这个符号引用的类是否被加载、解析、初始化。若没有则进行相应的类加载过程。
  2. 分配内存:


    加载检查通过后,为新生对象分配内存(内存大小在加载完后可以完全确定)为对象分配空间的任务实际上等同于把一块确定大小的内存块从 Java 堆中划分出来(系统采用的分配算法根据堆中内存是否是绝对规整的,分配方法有指针碰撞空闲列表
    在分配内存时,在并发情况下也并不是线程安全的。系统采用两种方案:
    1. CAS 配上失败重试的方式保证更新操作的原子性。
    2. 使用本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),每个线程在堆中预先分配一小块内存,当有本地缓冲区用完了,分配新的缓存区时 才需要同步锁定。
  3. 初始化零值:


    内存分配完后,虚拟机将内存空间初始化为零值,这样保证对象的成员字段不赋初始值便能直接使用,使程序能访问这些数据类型所对应的零值。
  4. 设置对象头:


    初始化零值完成后,虚拟机对对象进行必要的设置,比如这个对象是哪个类的实例、如何能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄。这些信息存放在对象头
  5. 执行init方法:


    new 指令之后会接着执行 <init>()方法,按照程序员的 意愿对对象进行初始化 这样一个真正可用的对象才算完全被构造出来。

对象的内存布局

对象在堆内存中的存储布局可以划分为三个部分: 对象头 (Header)、 实例数据(Instance Data)和对齐填充(Padding) 。

  1. 对象头:
    • 第一类是用于存储对象自身的运行时 数据,如哈希码( HashCode)、 GC 分代年龄、锁状态标志、线程持有的锁、 偏向线程 ID、 偏向时间戳等, 这部分数据的长度在 32 位和 64 位的虚拟机中分别为 32 个比特和 64 个比特, 官方称它为Mark Word 。(对象头可能存在与对象无关的数据,Mark Word可根据对象的状态复用自身的存储空间)
    • 第二类是类型指针,即对象指向它的类型元数据的指针,Java 虚拟机通 过这个指针来确定该对象是哪个类的实例。另外数组还需要记录数组长度的数据。
  2. 实例数据:对象真正存储的有效信息,在这些信息中
    1. 相同宽度的字段总是会被分配到一起存放。
    2. 父类定义的变量会在子类之前。
  3. 对齐填充:无特别含义,不是必须要求存在,起占位符的作用。HotSpot虚拟机的自动内存管理要求对象的起始地址是8字节的整数倍,对象头已正好是8字节的整数倍,但实例数据没有对齐的话,就需要对齐填充来补全。

对象的访问

主流的对象访问方法有:

  1. 句柄:Java 堆中将可能会划分出一块内存来作为句柄池, 栈中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
  2. 直接指针:Java 堆中对象的内存布局就必须考虑如何放置访问类 型数据的相关信息, 栈中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。

image.png

  • 使用句柄在对象被移动时,只会改变句柄中的实例数据指针,而栈中的数据不需要被修改。
  • 使用直接指针来访问速度较快,省去了一次指针定位的时间开销。(HotSpot主要使用第二种方式来进行对象访问)

关于常量池永久代历代的变化

  1. JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
  2. JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代 。
  3. JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)