Fork me on GitHub

Java虚拟机---内存区域和对象创建过程

内存区域

Java虚拟机在运行时的数据区可以分为:程序计数器、虚拟机栈、本地方法栈、方法区、堆区

程序计数器

什么是程序计数器?

当前线程正在执行字节码的行号指示器,就是说记录着当前线程正在执行的是哪一条字节码指令的地址。

作用

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制(顺序执行,选择,循环等等)
  2. 如果是在多线程的情形下,程序计数器会记录下当前线程执行到什么位置,当线程切换回来的时候就可以知道之前的线程执行到什么地方了。

特点

  1. 程序计数器记录的是正在执行的虚拟机字节码指令的地址
  2. 线程私有,也就意味着生命周期随着线程创建而创建,死亡而死亡
  3. Java虚拟机中唯一一个不会出现OOM的内存区域

Java虚拟机栈

什么是Java虚拟机栈?

描述的是Java方法执行的内存模型,当一个方法在即将运行的时候,Java虚拟机就会在Java虚拟机栈上开辟一块”栈帧”,在里面存储着局部变量表操作数栈动态链接方法出口等信息。

局部变量表里面存储着在编译期就可知内存空间大小的各种基本数据类型(java中8种基本类型)、对象引用和returnAddress(指向一个字节码指令的地址)。

进入一个方法的时候,这个方法需要的内存空间大小在编译期的时候就已经知道了,而且在方法的运行期间是不会改变布局变量表的大小

特点

  1. 线程私有,也就意味着生命周期随着线程创建而创建,死亡而死亡
  2. 局部变量表的大小在编译的时候就被确定下了,但是局部变量表的创建是在方法执行的时候
  3. 会出现栈溢出(线程请求栈深度超过虚拟机允许的深度)和内存溢出情况

StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。
OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。

本地方法栈

什么是本地方法栈?

与虚拟机栈类似,只是这里指的方法是本地方法

作用

本地方法在被执行的时候,会在本地方法栈中创建一个栈帧,用于存放本地方法的局部变量表、操作数栈、动态链接、出口信息等

特点

  1. 线程私有,随着线程生与死
  2. 会出现内存溢出和栈溢出

什么是堆?

存放对象实例的内存空间,几乎所有对象实例都存放在堆中

特点

  1. 线程共享
  2. 在虚拟机启动的时候创建
  3. 垃圾回收的主要场所
  4. 内存回收的角度可以分为新生代(Eden区域 Survivor From区域 Survivor To区域)、老年代
  5. 线程共享的角度可以分为多个线程私有的分配缓冲区(TLAB)
  6. 堆的大小可以固定也可以扩展(逻辑连续即可),当堆中的对象实例没有完成分配且堆也不可以扩展的时候,就会出现内存溢出的情况

方法区

什么是方法区?

方法区是堆的一个逻辑部分,存放着已经被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据

特点

  1. 线程共享
  2. 老年代主要区域
  3. 垃圾回收频率低,对方法区回收的主要目标是常量池的回收和类型的卸载
  4. 方法区可以固定大小、也可以不固定还可以允许不垃圾回收

运行时常量池

运行时常量池是方法区中的一部分,在Class文件中有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。这些内容就存放在运行时常量池中。还会把翻译出来的直接引用也存储在里面。

  1. 常量储存在运行时常量池中
  2. 一般在一个类中通过public static final 来声明一个常量,这个类被编译之后会生成一个Class文件,类中的所有信息都被储存在这个class文件中
  3. 当上面的这个类被虚拟机加载之后,class文件中的常量就会存放在方法区中的运行时常量池中,而且在运行期间可以向常量池中添加新的常量。String类中的intern()方法就能在运行期间向常量池中添加字符串常量
  4. 在运行时常量池中的如果某些常量没有被对象引用,同时也没有被变量引用,那么就会被垃圾收集器回收

Java对象创建的过程

对象的创建过程

当虚拟机遇到一条含有new指令的时候,会进行一系列对象创建的过程

  1. 检查常量池中是否含有要创建的这个对象的类的符号引用
    • 如果常量池中没有这个类的符号引用,说明这个类还没有被定义,报ClassNotFoundException
    • 有的话进行第二步操作
  2. 接着检查这个符号引用代表的类有没有被JVM加载
    • 没有加载的话,就找到该类的class文件,并加载到方法区
    • 类加载了的话,就准备为对象分配内存
  3. 根据方法区中该类的信息确定该类需要的内存大小
  4. 从堆中划分一块对应大小的内存空间分配给新的对象。分配堆中内存有两种方式
    • 指针碰撞:如果JVM的垃圾收集器采用的是复制算法或者标记-整理算法,那么堆中的空闲内存会是一片完整的区域,并且空闲内存和已使用的内存之间有一个指针标记。当一个对象分配内存的时候,只需要移动指针就可以了。这种在完整空闲内存区域通过移动指针来分配内存区域的方法就被称之为”指针碰撞”
    • 空闲列表:如果JVM的垃圾收集器采用的是标记-清除算法,那么在堆中的内存已使用区域和空闲区域就会相互交错,所有这个时候就会需要一个空闲列表来记录堆中那些区域是空闲的。所以创建对象的时候会根据这个空闲列表来进行分配内存
    • 划分可用的内存空间(并发时,解决线程不安全)
      1. 对分配内存空间的动作进行同步处理(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性)
      2. 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称之为本地线程分配缓冲(TLAB),哪一个线程需要分配内存就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
  5. 为对象中的成员变量进行赋初值(上一步的内存空间划分完毕进行,如果是使用TLAB的话,赋初值也可以日前在TLAB分配的时候进行),这意味着Java中对象实例可以 不用赋初值就可以直接使用
  6. 设置对象头中的消息(这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息)
  7. 调用对象的构造函数进行初始化

访问对象的过程

  1. 句柄访问方式:堆中有一块叫做”句柄池”的内存空间,用来存放所有对象的地址和所有对象所属类的类信息。引用类型的变量存放的是该变量在句柄池中的地址。访问对象的时候,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址再访问对象
    • 优点:引用中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而引用本身不需要修改
  2. 直接指针访问方式:引用类型的变量直接存放对象的地址,不需要句柄池,通过引用能够直接访问对象。但是对象所在的内存空间中需要额外的策略存储对象所属的类信息的地址
    • 优点速度更快