Java 虚拟机:运行时数据区域

运行时数据区域完整图

在这里插入图片描述

程序计数器

程序计数器(Program Counter Register)是一块较小的区域,它可以看作是当前线程所执行的字节码的行号指示器。在 Java 虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
由于 Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一个线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应当为空。此内存区域是唯一一个在 《Java 虚拟机规范》 中没有规定任何 OutOfMemoryError 情况的区域。如下案例所示:

public class PCRegisterTest {
    public static void main(String[] args) {
        int i = 10;
        int j = 20;
        int k = i + j;

        String s = "abc";
        System.out.println(i);
        System.out.println(k);
    }
}

左边的数字代表指令地址(指令偏移),即 PC 寄存器中可能存储的值,然后执行引擎读取 PC 寄存器中的值,并执行该指令
在这里插入图片描述

虚拟机栈

Java 虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧用于存储局部变量表操作数栈动态连接方法出口等信息。每个方法都被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表

局部变量表定义为一个数字数组,存放了编译器可知的各种 Java 虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它并不等于对象本身,可能是一个只想对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一个字节码指令的地址)。
由于局部变量表是建立在线程的栈上,是线程私有数据,因此不存在数据安全问题。局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 Code 属性的 local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

局部变量槽

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的 long 和 double 类型的数据会占用两个变量槽,其余的数据类型只占用一个。JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值,this 放在 index = 0 的位置,而 static 方法无法调用 this
在这里插入图片描述

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

public void test4() {
    int a = 0;
    {
        int b = 0;
        b = a + 1;
    }
    //变量c使用之前已经销毁的变量b占据的slot的位置
    int c = a + 1;
}

在这里插入图片描述

操作数栈

操作数栈也常被称为操作栈,它是一个后入先出栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数栈的最大深度在编译的时候被写入到 Code 属性的 max_stacks 数据项之中。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。例如整数加法的字节码指令 iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个 int 型的数值,当执行这个指令时,会把这两个 int 值出栈并相加,然后将相加的结果重新入栈。
另外在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在大多虚拟机的视线里都会进行一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无须进行额外的参数复制传递了。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在 class 文件的常量池里。字节码的方法调用指令就以常量池里指向方法的符号应用作为参数。这些参数引用一部分会在类加载阶段或者第一次使用的时候就被转换为直接引用,这种转换被称为静态解析。另外一部分将在每一次运行期间都转换为直接引用,这部分就称为动态连接。

方法返回地址

当一个方法开始执行后,只有两种方式退出这个方法。一种方式是正常调用完成,另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理,即异常调用完成
无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法的 PC 计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数值。正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值

附加信息

《Java 虚拟机规范》 允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,也是线程私有的。其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法(如 C语言)服务。它的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine(执行引擎) 执行时加载本地方法库。
《Java 虚拟机规范》对本地方法栈中方法使用的语言、作用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的 Java 虚拟机(譬如 HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一

堆区

Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。《Java虚拟机规范》 中对 Java 堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)

堆内存分区

Java 7 及之前堆内存逻辑上分为三部分:新生代 + 老年代 + 永久区

  • Young/New Generation Space 新生代,又被划分为 Eden 区(伊甸园区)和 Survivor 区 (幸存者 from 区 和幸存者 to 区)
  • Old/Tenure generation space 老年代
  • Permanent Space 永久区 Perm

Java 8及之后堆内存逻辑上分为三部分:新生代 + 老年代 + 元空间

  • Young/New Generation Space 新生代,又被划分为 Eden 区(伊甸园区)和 Survivor 区 (幸存者 from 区 和幸存者 to 区)
  • Old/Tenure generation space 老年代
  • Meta Space 元空间 Meta
    在这里插入图片描述
新老年代比例

新生代与老年代在堆结构的占比默认为 -XX:NewRatio=2,表示新生代占 1,老年代占 2,即新生代占整个堆的 1/3。
在新生代中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发人员可以通过选项 -XX:SurvivorRatio 调整这个空间比例,比如 -XX:SurvivorRatio=8。几乎所有的 Java 对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了(有些大的对象在Eden区无法存储时候,将直接进入老年代),也可以使用选项 -Xmn 设置新生代最大内存大小,但这个参数一般使用默认值就可以了。新生区的对象默认生命周期超过 15 ,就会去养老区养老

设置堆内存大小

默认情况下初始内存大小是物理电脑内存大小 1/64,最大内存大小是物理电脑内存大小 1/4。初始内存大小和最大内存大小可以通过选项 -Xms-Xmx 来进行设置。通常会将 -Xms 和 -Xmx 两个参数配置相同的值,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能

  • -Xms 用于表示堆区的起始内存,等价于 -XX:InitialHeapSize
  • -Xmx 则用于表示堆区的最大内存,等价于 -XX:MaxHeapSize
/**
 * 1. 设置堆空间大小的参数
 * -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
 *      -X 是jvm的运行参数
 *      ms 是memory start
 * -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
 *
 * 2. 默认堆空间的大小
 *      初始内存大小:物理电脑内存大小 / 64
 *      最大内存大小:物理电脑内存大小 / 4
 *
 * 3. 手动设置:-Xms600m -Xmx600m
 *     开发中建议将初始堆内存和最大的堆内存设置成相同的值。
 *
 * 4. 查看设置的参数:方式一: jps   /  jstat -gc 进程id
 *                  方式二:-XX:+PrintGCDetails
 *
 */
public class HeapSpaceInitial {
    public static void main(String[] args) {

        //返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");
    }
}

对象分配过程

创建的对象一般都是存放在 Eden 区的,当 Eden 区满了后,就会触发 GC 操作,一般被称为 YGC / Minor GC操作。
在这里插入图片描述
当进行一次垃圾收集后,红色的对象将会被回收,而绿色的对象还被占用着,存放在 S0(Survivor From) 区,同时给每个对象设置了一个年龄计数器,将经过一次回收后还存在的对象其年龄加 1。
同时 Eden 区继续存放对象,当 Eden 区再次存满的时候,又会触发一个 GC 操作,此时 GC 将会把 Eden 和 Survivor From 中的对象进行一次垃圾收集,把存活的对象放到 Survivor To 区,同时让存活的对象年龄 + 1。
在这里插入图片描述
继续不断的进行对象生成和垃圾回收,当 Survivor 区中的对象的年龄达到15的时候,将会触发一次 Promotion 晋升的操作,也就是将年轻代中的对象晋升到老年代中。
在这里插入图片描述

TLAB(堆当中的线程私有缓存区域)

由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。从分配内存的角度看,所有线程共享的 Java 堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer),以提升对象分配时的效率。
尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但明确是是将TLAB作为内存分配的首选;在程序中,开发人员可以通过选项“-XX:UseTLAB“ 设置是够开启 TLAB 空间;默认情况下,TLAB 空间的内存非常小,仅占有整个EDen空间的1%,也可以通过选项 ”-XX:TLABWasteTargetPercent“ 设置 TLAB 空间所占用 Eden 空间的百分比大小;一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配了内存。

TLAB 对象分配过程

在这里插入图片描述

堆空间参数设置

  • -XX:PrintFlagsInitial: 查看所有参数的默认初始值
  • -XX:PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)
  • 具体查看某个参数的指令:
    • jps:查看当前运行中的进程
    • jinfo -flag 参数 进程id(如 SurvivorRatio参数 查看新生代中 Eden 和 S0/S1 空间的比例)
  • -Xms: 初始堆空间内存(默认为物理内存的1/64)
  • -Xmx: 最大堆空间内存(默认为物理内存的1/4)
  • -Xmn: 设置新生代大小(初始值及最大值)
  • -XX:NewRatio: 配置新生代与老年代在堆结构的占比
  • -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
  • -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄(默认15)
  • -XX:+PrintGCDetails:输出详细的GC处理日志
    • 打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
  • -XX:HandlePromotionFailure:是否设置空间分配担保

方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。 而元空间,可以理解为是方法区的一种具体实现,也可以把方法区理解为 Java 中定义的一个接口,把永久代/元空间看做这个接口的具体实现类,方法区是规范,而永久代/元空间是 Hotspot 针对该规范进行的实现。
方法区在 JVM 启动的时候被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的,关闭 JVM 就会释放这个区域的内存。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址

常量池

运行时常量池是方法区的一部分,而常量池表是 Class 字节码文件的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。如果不使用常量池,就需要将用到的类信息、方法信息等记录在当前的字节码文件中,容易造成文件臃肿
运行时常量池相对于 Class 文件常量池的另外一个重要特性就是具备动态性,Java 语言并非要求常量一定只有编译期才能产生,也就是说,并非预置在入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法(判断运行时常量池中有无相同值的常量,若无则加入)。
在这里插入图片描述

堆、栈、方法区的交互关系

在这里插入图片描述

  • Person 类的 .class 信息存放在方法区中
  • person 对象存放在 Java 栈的局部变量表中
  • 真正的 person 对象存放在 Java 堆中
  • 在 person 对象中,有个指针指向方法区中的 person 类型数据,表明这个 person 对象是用方法区中的 Person 类 new 出来的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java虚拟主要分为以下几个内存区域: 1. 程序计数器(Program Counter Register) 2. Java虚拟栈(Java Virtual Machine Stacks) 3. 本地方法栈(Native Method Stacks) 4. Java堆(Java Heap) 5. 方法区(Method Area) 6. 运行常量池(Runtime Constant Pool) 7. 直接内存(Direct Memory) 其中,程序计数器、Java虚拟栈、本地方法栈都是线程私有的内存区域Java堆、方法区、运行常量池、直接内存则是线程共享的内存区域。 ### 回答2: Java虚拟(JVM)中有几个重要的内存区域,它们分别是堆、栈、方法区、程序计数器和本地方法栈。 堆是Java程序最主要的内存区域之一,用于存储对象实例和数组。所有通过关键字new创建的对象都会在堆上分配内存。堆是被所有线程共享的内存区域,每个对象的实例变量在堆中占用一定的空间。 栈是一个线程私有的内存区域,用于存储线程的方法调用。每个线程在执行方法都会在栈中创建一个栈帧,栈帧包含方法的参数、局部变量和返回值等信息。栈的操作是后进先出的,所以栈也被称为LIFO(Last In First Out)数据结构。 方法区(JDK 8及以前版本称为“永久代”)用于存储类的元数据信息,如类名、方法名、字段名和运行常量池等。方法区也是被所有线程共享的内存区域。 程序计数器是一块较小的内存区域,它用来指示线程当前执行的字节码指令地址。每个线程都有一个独立的程序计数器,因此它是线程私有的。 本地方法栈与栈类似,但用于执行本地方法调用(Native Method)。本地方法是用C或C++等本地语言编写的方法,它们可以直接在系统级别访问硬件设备或其他资源。本地方法栈也是线程私有的。 总之,Java虚拟的内存区域包括堆、栈、方法区、程序计数器和本地方法栈。这些内存区域各有不同的作用,用于支持Java程序的执行和对象的管理。 ### 回答3: 在Java虚拟中,主要有以下几个内存区域: 1. 程序计数器(Program Counter Register): 程序计数器是一块较小的内存区域,它保存着当前线程正在执行的字节码指令的地址。每个线程都有自己独立的程序计数器。 2. Java栈(Java Stack): Java栈用于存储Java方法的局部变量、参数、方法返回值以及一些操作数。每个线程的方法在执行都会创建一个对应的栈帧,栈帧中存放了方法的信息。 3. 本地方法栈(Native Method Stack): 本地方法栈与Java栈类似,但它是为Native方法服务的。Native方法是使用其他语言(如C、C++)编写的方法,本地方法栈用于保存这些方法的本地变量。 4. Java堆(Java Heap): Java堆是Java虚拟所管理的内存中最大的一块区域,被所有线程共享。Java堆用于存储对象实例和数组,是垃圾回收的主要区域Java堆可以分为新生代和老年代两个区域。 5. 方法区(Method Area): 方法区用于存储已被加载的类信息、常量、静态变量等数据。在方法区中还有一个叫做运行常量池的区域,它存放着每个类的运行常量信息。 6. 运行常量池(Runtime Constant Pool): 运行常量池是方法区中的一部分,用于保存编译器生成的各种字面量和符号引用。 除了以上几个内存区域Java虚拟还包括了直接内存(Direct Memory)。直接内存并不是Java内存区域的一部分,它使用的是操作系统的内存,但可以被Java虚拟直接访问,通常用于NIO(New IO)操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值