1. Java内存区域和内存溢出异常(JVM)

标签(空格分隔): JVM


运行时数据区域

整体架构

1. Java内存区域和内存溢出异常(JVM)_第1张图片
architecture.png

模块详解

由所有线程共享的数据区

  1. Method Area(方法区)
    • 用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,主要针对常量池的回收和对类型的卸载
    • Non-Heap(非堆)
    • 不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集
    • 运行时常量池:用于存放编译期生成的各种字面量和符号引用,这部分将在类加载后进入方法区的运行时常量池中存放
  2. Heap(堆)
    • 唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存
    • 在虚拟机启动时创建
    • 垃圾收集管理的主要区域
    • GC堆:新生代、老年代
    • 可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可,就像磁盘空间一样
  3. Execution Engine(执行引擎)
  4. Native Method Interface(本地方法接口)

线程隔离的数据区

  1. JVM Language Stacks(JVM虚拟机栈)
    • 描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
  2. PC Resgisters(程序计数器)
    • 较小的内存空间
    • 可以看做是当前线程所执行的字节码的行号指示器
    • 在虚拟机概念模型里(仅是概念模型),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  3. Native Method Stacks(本地方法栈)
    • 和虚拟机栈一样,只不过是用到的native方法

其他知识点

  1. 栈帧中所存放的各部分信息的作用和数据结构
    • 局部变量表
      • 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其中存放的数据的类型是编 译期可知的各种基本数据类型、对象引用(reference)和 returnAddress 类型(它指向了一条字节码指令的地 址)。局部变量表所需的内存空间在编译期间完成分配,即在 Java 程序被编译成 Class 文件时,就确定了所需 分配的最大局部变量表的容量。当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定 的,在方法运行期间不会改变局部变量表的大小。
        局部变量表的容量以变量槽(Slot)为最小单位。在虚拟机规范中并没有明确指明一个 Slot 应占用的内存空间大 小(允许其随着处理器、操作系统或虚拟机的不同而发生变化),一个 Slot 可以存放一个32位以内的数据类 型:boolean、byte、char、short、int、float、reference 和 returnAddresss。reference 是对象的引用类
        型,returnAddress 是为字节指令服务的,它执行了一条字节码指令的地址。对于 64 位的数据类型(long和do uble),虚拟机会以高位在前的方式为其分配两个连续的 Slot 空间。
        虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从 0 开始到局部变量表最大的 Slot 数量,对于 32 位数据类型的变量,索引 n 代表第 n 个 Slot,对于 64 位的,索引 n 代表第 n 和第 n+1 两个 Slot。
        在方法执行时,虚拟机是使用局部变量表来完成参数值到参数变量列表的传递过程的,如果是实例方法(非stati c),则局部变量表中的第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用,在方法中可以通过关键 字“this”来访问这个隐含的参数。其余参数则按照参数表的顺序来排列,占用从1开始的局部变量 Slot,参数表 分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。
        局部变量表中的 Slot 是可重用的,方法体中定义的变量,作用域并不一定会覆盖整个方法体,如果当前字节码P C计数器的值已经超过了某个变量的作用域,那么这个变量对应的 Slot 就可以交给其他变量使用。这样的设计不 仅仅是为了节省空间,在某些情况下 Slot 的复用会直接影响到系统的而垃圾收集行为
    • 操作数栈
      • 操作数栈又常被称为操作栈,操作数栈的最大深度也是在编译的时候就确定了。32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2。当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各 种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。
        Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称 Java 虚拟机是基于栈的,这点不同于 Android 虚拟机,Android 虚拟机是基于寄存器的。
        基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提
        供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差
    • 动态链接
      • 每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为 了支持方法调用过程中的动态连接。Class 文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就 以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接 引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动 态连接
    • 方法返回地址
      • 当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异
        常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用
        的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状
        态。一般来说,方法正常退出时,调用者的 PC 计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数 器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
        方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操 作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整 PC 计数器的值以指向方法调用指令后面的 一条指令

HotSpot对象

对象的创建

  1. 创建流程
    new指令
    ->
    检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过;如果没有,则进行初始化
    ->
    虚拟机为新生对象分配内存
    ->
    虚拟机将分配到的内存空间初始化为零值
    ->
    对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄信息
    ->
    从虚拟机的视角来说,一个新的对象已经产生了;但从Java程序视角来看,对象的创建才刚开始,init方法还没有执行,只有init方法执行完成之后,一个真正的可用对象才算产生出来

  2. 具体细节

    • 内存分配方法:
      • 指针碰撞(Bump the Pointer):假设内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就只是把那个指针向空闲空间那一边挪动一段与对象大小相等的距离
      • 空闲列表(Free List):假设内存是不规整的

对象的内存布局

整体布局:

- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)

模块详解:

  1. 对象头
    • 第一部分:存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
    • 第二部分:类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
  2. 实例数据
    • 对象真正存储的有效信息,也是在程序代码中所定义的各种类型字段的内容,无论是从父类继承的还是在子类中定义的
  3. 对齐填充
    • 仅仅起着占位符的作用
    • 因为自动内存管理系统要求对象的起始地址必须是8字节的整数倍

对象的访问定位

  1. 句柄访问
    • Java堆终将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
  2. 直接指针
    • reference中存储的就是对象地址
  3. 比较
    • 这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是 reference 中存放的是稳定的句柄地址,在 对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不 需要修改。使用直接指针访问方式的最大好处是速度快,它节省了一次指针定位的时间开销。目前 Java 默认使 用的 HotSpot 虚拟机采用的便是是第二种方式进行对象访问的。
  4. 图片


    1. Java内存区域和内存溢出异常(JVM)_第2张图片
    handle.png
1. Java内存区域和内存溢出异常(JVM)_第3张图片
direct.png

你可能感兴趣的:(1. Java内存区域和内存溢出异常(JVM))