JVM第二篇:运行时数据区

本文详细介绍了Java虚拟机的运行时数据区,包括PC计数器、虚拟机栈(局部变量表、操作数栈、动态链接、方法返回地址)、本地方法栈、堆(TLAB、对象分配、逃逸分析)以及方法区(运行时常量池)。各区域的作用、特点和内存管理策略进行了深入阐述,例如栈帧的工作原理、对象实例化内存布局以及动态链接的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述
运行时数据区由五部分组成,分别为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。具体如下:

(1)PC计数器

  1. 线程私有的,每个线程都会一个独自的程序计数器,生命周期与线程一致。
  2. 程序计数器会存储当前线程正在执行的Java方法的JVM指令地址,如果是native方法,则是undefined。
  3. 程序计数器是控制流的指示器、分支、循环、跳转、异常处理、线程恢复等基础都需要依赖程序计数器。
  4. 字节码解释器就是通过改变计数器值来选取下一条需要执行的字节码指令。
  5. 无OOM、无GC区域。
    在这里插入图片描述

为什么要有PC计数器呢?为什么PC计数器要是线程私有的呢?
因为CPU不断的切换各个线程,这就需要PC计数器记录每个线程执行的代码位置,以便CPU回来执行的时候,可以紧接着执行,不会出现乱的现象。这同时也是线程私有的原因。

(2)虚拟机栈(Java栈)

栈是运行时的单位,堆是存储时的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理程序。
堆解决的是数据存储的问题,即数据怎么放,放在哪。

什么是Java栈?
虚拟机栈,也叫Java栈。每个线程都有一个独立的Java栈,每个Java栈内部存储着栈帧,每个栈帧对应一个Java方法的调用。
在这里插入图片描述

  1. 虚拟机栈是线程私有的,每个线程都会创建一个虚拟机栈,生命周期与线程一致。
  2. 虚拟机栈的大小可以是固定的(可能出现StackOverflowError),也可以是动态的(可能出现OutOfMemoryError)。

栈运行原理

  1. 不可能存在一个栈帧之中引用另一个线程的栈帧。
  2. 当前方法运行完毕,如果有返回值,会将返回值传递给前一个栈帧,然后丢弃当前栈帧,让前一个栈帧成为当前栈帧。
  3. Java方法有两种返回形式,一个是正常返回,一个是异常返回。不论哪种返回方式,当前栈帧都会被弹出。

栈帧内部结构

在这里插入图片描述

(1)局部变量表

定义为一个数字数组。主要存储方法参数(形参)和定义在方法内部的局部变量。主要包括8种基本数据类型、对象引用和returnAddress类型。

为什么是数字数组?
因为byte、short、char、int、boolean都以int类型存储,占一个slot。float、long、double占两个slot。

  1. 由于局部变量表存储在栈中,所以数据安全。
  2. 局部变量表在编译期就确定下来了,在运行期间不会改变大小。
  3. 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也随之销毁。

Slot

package DataArea;

/**
 * @author shang
 * @PackageName:DataArea
 * @ClassName: Test
 * @Description:
 * @date 2020/9/13 10:50
 */
public class Test {

    public void A(){
        int a = 17;
        double d =17.5;
        byte b =12;
    }

}

在这里插入图片描述

  1. 参数值的存放总是在局部变量表的index0位置处开始,到数组长度为-1的索引结束。
  2. 局部变量表最基本的单位是Slot(变量槽)。
  3. 局部变量表存放着编译期可知的8种基本数据类型、引用类型、returnAddress类型变量。
  4. 局部变量表中,32位以内的类型占据一个slot(包括returnAddress类型),64位(long、double)占据两个slot。
  5. 如果当前帧是由构造方法或者实例方法(非static方法)创建的,那么该对象引用this将会存放在index0处,其余参数按照声明位置按顺序排放。

为什么静态方法中不能使用this呢?
因为局部变量表中不存在对象引用this,所以无法使用,具体如下所示。

public class Test {

    public static void B(){
        int a = 15;
    }

}

在这里插入图片描述

变量分类
(1)按数据类型分:基本数据类型、引用数据类型
(2)按在类中声明的位置分

  1. 成员变量:在使用前都经过默认初始化
    (1)类变量(static):linking阶段的prepare阶段给类变量默认赋值,initial阶段给类变量显示赋值,即静态代码块赋值。
    (2)实例变量(非static):随着对象的创建会在堆空间中分配实例变量空间并进行默认赋值。
  2. 局部变量:在使用前必须显示赋值,否则编译不通过,如代码所示。
public class Test {

    int a;
    int b = a+2;

    public void A(){
        int a;
        int b = a+2; //Variable 'a' might not have been initialized
    }
}
(2)操作数栈

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈。
某些字节码指令会将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
在这里插入图片描述

  1. 操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
  2. 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
  3. 操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的入栈和出栈操作来完成一次数据访问。
  4. 如果被调用的方法带有返回值,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
  5. Java虚拟机的解释引擎是基于栈的执行引擎,其中栈指的就是操作数栈。

栈顶缓存技术
由于操作数是存储在内存中,因此频繁地执行内存读/写操作必然会影响执行速度,为解决这个问题,提出了栈顶缓存技术。
栈顶缓存技术:将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

(3)动态链接(对运行时常量池的访问)

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
在Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用保存在Class文件的常量池中。
在这里插入图片描述
动态链接的作用就是将符号引用转换为调用方法的直接引用。

在这里插入图片描述

为什么需要动态链接?
假如说没有动态链接,那么常量池就需要存在栈帧中,这就会导致栈帧占用空间大的问题,所以说为了节约空间,不造成重复,浪费资源,所以有了动态链接。

方法的调用

(4)方法返回地址

方法返回地址存放调用该方法的PC寄存器的值。
在这里插入图片描述

(3)本地方法栈

针对于native方法。

(4)堆

在这里插入图片描述

  1. 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。Java堆区在JVM启动的时候被创建,其大小也就确定了。
  2. 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上应视为连续的。
  3. 所有的线程共有堆,在这里可以划分线程私有缓冲区(TLAB)。
  4. “几乎”所有的对象都在堆中分配内存。
  5. 方法结束后,堆中的对象并不会马上移除,仅仅在垃圾收集的时候才会被移除。
  6. 堆是GC执行垃圾回收的重点区域。
  7. 几乎所有的对象都是在Eden区被new出来的。
  8. 80%的Java对象在新生代被销毁,“朝生夕死”。

对象分配一般过程
在这里插入图片描述
在这里插入图片描述

动态对象年龄判断
如果Survivor区中相同年龄的所有对象大小的总和大于的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需达到阈值(默认阈值15)。

TLAB

(1)为什么引入TLAB?
堆区是线程共享区域,任何区域都可以访问到堆中的共享区域。由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆中划分到内存空间是线程不安全的。为避免多个线程同时操作同一地址,需要使用加锁机制,进而影响到分配速度。
(2)什么是TLAB?
从内存模型的角度而不是垃圾回收的角度,从Eden区域划分出1%的区域作为私有缓存区域。多个线程同时分配内存时,使用TLAB可以避免一系列的线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式成为快速分配策略。

堆是分配对象的唯一选择吗?

不是。如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收。这就是最常见的堆外存储技术

是否发生逃逸?

  1. 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
  2. 当一个对象在方法中被定义后,它被外部方法引用,则认为发生逃逸。

代码优化(基于逃逸分析)

  1. 栈内分配
  2. 同步省略
  3. 标量替换

(5)方法区

在这里插入图片描述

  1. 方法区与Java堆一样,是线程共享区域。
  2. 方法区在JVM启动时被创建,并且它的实际的物理内存空间和Java堆一样都可以是不连续的。
  3. 方法区的大小,跟堆空间一样,可以固定大小或者可扩展。
  4. 方法区的大小决定系统可以保存多少个(类信息,DNA模板),如果系统定义了太多的类导致方法区溢出,虚拟机会同样抛出内存溢出错误。
  5. 关闭JVM会释放这个区域的内存。

元空间和永久代
jdk7之前成永久代,jdk8之后称元空间。
元空间不在是虚拟机设置的内存中,而是使用本地内存(与永久代相比)。
Jdk1.8及以后,字符串常量池、静态变量被分离到堆中。

方法区内部结构

方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
在这里插入图片描述
什么是Class文件常量池?

Class文件常量池可以看做是一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型。

什么是运行时常量池?

  1. 运行时常量池是方法区的一部分。
  2. 常量池表是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  3. 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
  4. JVM为每个已加载的类型(类或接口)都维护一个常量池。池中数据项就像数组项一样,是通过索引访问的。
  5. 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能获得的方法或者字段引用。此时不再是常量池中的符号地址,这里就转换为真实地址。
    =》运行时常量池相较于Class文件常量池的另一个重要特种是具有动态性。

在这里插入图片描述

对象的实例化内存布局与访问地址

(1)对象的实例化内存布局

  1. 对象头:对象头记录对象的信息,包括哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向时间戳,类型指针。
    (1)运行时数据区
    (2)类型指针
  2. 实例数据:存储对象自身定义的数据
  3. 对齐填充:为了对齐填充的额外数据。
    在这里插入图片描述
    (2)对象访问地址
    在这里插入图片描述
    HotSport虚拟机使用如下方式实现,如下图。
    在这里插入图片描述

StringTable(字符串常量池)为什么要调整?
jdk7将StringTable放到了堆空间中。因为永久代的回收效率很低,在FULL GC时才会触发,而FULL GC是在老年代空间不足、永久代不足时才会触发。
这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值