Java 面试必问:JVM 运行时数据区域详解(附内存结构图)

Java 面试必问:JVM 运行时数据区域详解(附内存结构图)

文章目录

在 Java 面试中,JVM 内存结构几乎是必问知识点。很多人可以背出程序计数器、虚拟机栈、本地方法栈、堆和方法区这五个区域,但当面试官继续追问它们的作用、线程共享关系以及对象为什么在堆上时,往往就难以解释清楚。本文主要面对面试八股文速成选手,将从 JVM 的整体内存布局出发,带你系统梳理五大运行时数据区域的作用与常见面试考点。

JVM 运⾏时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称 JMM)完全不同,属于完全不同的两个概念,它由以下 5 ⼤部分组成:

在这里插入图片描述

1. 程序计数器(线程私有)

程序计数器的作⽤:⽤来记录当前线程执⾏的⾏号的。 程序计数器是⼀块⽐较⼩的内存空间,可以看做是当前线程所执⾏的字节码的⾏号指⽰器。如果当前线程正在执⾏的是⼀个 Java ⽅法,这个计数器记录的是正在执⾏的虚拟机字节码指令的地址;如果正在执⾏的是⼀个 Native ⽅法,这个计数器值为 undefined。程序计数器内存区域是唯⼀⼀个在 JVM 规范中没有规定任何 OOM 情况的区域!

在 JVM 中,多线程是通过线程轮流切换来获得 CPU 执行时间的,因此,在任一具体时刻,一个 CPU 的内核只会执行一条线程中的指令,因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,并且不能互相干扰,否则就会影响到程序的正常执行次序。也就是说,我们要求程序计数器是线程私有的。

接下来我们一个通过一段非常简单的小代码以及字节码指令来看看程序计数器的作用。

publicclassPcDemo{publicstaticintadd(int a,int b){return a + b;}}

字节码指令大致如下:

0: iload_0 // 从局部变量表中加载变量 a 到操作数栈1: iload_1 // 从局部变量表中加载变量 b 到操作数栈2: iadd // 两数相加3: ireturn // 返回
  1. 初始状态:当方法开始执行时,PC 计数器设置为 0,指向第一条指令 0: iload_0。
  2. 执行第一条指令:执行 iload_0 指令,将局部变量表中索引为 0 的整数(即方法的第一个参数 a)加载到操作数栈顶。执行完成后,PC 计数器更新为 1,指向下一条指令 1: iload_1。
  3. 执行第二条指令:执行 iload_1 指令,将局部变量表中索引为 1 的整数(即方法的第二个参数 b)加载到操作数栈顶。执行完成后,PC 计数器更新为 2,指向下一条指令 2: iadd。
  4. 执行第三条指令:执行 iadd 指令,弹出操作数栈顶的两个整数(即 a 和 b),将它们相加,然后将结果压入操作数栈顶。执行完成后,PC 计数器更新为 3,指向下一条指令 3: ireturn。
  5. 执行最后一条指令:执行 ireturn 指令,弹出操作数栈顶的整数(即 a + b 的结果),并将这个值作为方法的返回值。方法执行完成,控制权返回到方法调用者。

如果 JVM 线程发生切换:

线程APC=2// 线程A下一条指令应该执行 iadd 线程BPC=4// 线程B下一条指令应该执行 ireturn

2. Java 虚拟机栈(线程私有)

Java 虚拟机栈(JVM Stack) 是线程私有的运行时数据区,它的生命周期与线程相同。虚拟机栈用于描述 Java 方法执行时的内存模型:当一个方法被调用时,JVM 会为该方法创建一个 栈帧(Stack Frame) 并压入虚拟机栈中。栈帧中主要包含 局部变量表、操作数栈、动态链接和方法返回地址等信息。当方法执行结束时,对应的栈帧会被弹出。通常我们所说的 “栈内存”,指的就是 Java 虚拟机栈。

在这里插入图片描述
  1. 局部变量表(Local Variable Table):用于存储方法执行过程中使用的方法参数和方法内部定义的局部变量(包括基本数据类型变量和对象引用)。其中对象引用存储的是对象在堆中的引用地址而不是对象本身。局部变量表的大小在编译期就已确定,在方法执行期间不会改变,并以 slot(槽位) 的形式按顺序存放在栈帧中。
  2. 操作数栈:操作数栈是 JVM 在执行字节码指令时,用于临时存放参与运算的数据和保存计算结果的栈结构。字节码指令会通过入栈(push)和出栈(pop)的方式在操作数栈上完成计算,可以理解为 JVM 执行计算时临时使用的“工作台”。
  3. 动态链接:指在方法执行过程中,把常量池中的符号引用解析为实际方法或字段引用的过程。(例如我们想给张三打电话,需要先在通讯录中通过“张三”这个名字查找他的真实电话号码,然后再拨打电话。其中,“张三”这个名字相当于符号引用,张三的电话号码相当于真实引用,而通过名字查找电话号码的过程就类似于动态链接。)
  4. 方法返回地址:指当前方法执行结束后,程序需要返回到调用者方法继续执行的位置。(换个更直白的说法:方法返回地址就是“这个方法执行完之后,下一条要执行的字节码在哪里”)

3. 本地方法栈(线程私有)

本地方法栈(Native Method Stack)与 Java 虚拟机栈类似,只不过 Java 虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。故在此不再赘述。

4. 堆(线程共享)

堆(Heap)是 JVM 中用于存放对象实例和数组(数组也是一种对象)的运行时内存区域,并且是所有线程共享的。在 Java Virtual Machine 中,当代码创建对象时,本质上就是在堆里申请一块内存。如下所示:

User user =newUser(); 线程栈 堆 ------------------ user (引用地址)------->User对象实例 栈里只保存 引用(reference),真正的数据在堆里。这也是为什么 Java 对象通常说是 “在堆上分配”。 

堆为什么必须共享?设想这样一个程序:

User user =newUser();ThreadA 修改 user.name ThreadB 读取 user.name 如果堆不是共享的,每个线程都有自己的对象副本,那数据就会完全不同步。 所以 JVM 的设计非常明确:对象数据在堆里共享,执行过程在栈里隔离。 

我们常⻅的 JVM 参数设置 -Xms10m 最⼩启动内存是针对堆的,-Xmx10m 最⼤运⾏内存也是针对堆的。

堆除了是对象的聚集地,也是 Java 垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度来看,由于垃圾收集器基本都采用了分代垃圾收集的算法,所以堆还可以细分为:新生代和老年代。新生代还可以细分为:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

堆这最容易出现的就是 OutOfMemoryError 错误,分为以下几种表现形式:

  • OutOfMemoryError: GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生该错误。
  • java.lang.OutOfMemoryError: Java heap space:假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象, 就会引发该错误。和本机的物理内存无关,和我们配置的虚拟机内存大小有关!

5. 方法区(线程共享)

方法区是 Java 虚拟机规范上的一个逻辑区域,在不同的 JDK 版本上有着不同的实现。在 JDK 7 的时候,方法区被称为永久代(PermGen),而在 JDK 8 的时候,永久代被彻底移除,取而代之的是元空间。

方法区 ≠ 元空间 方法区 是规范里的概念 元空间 是一种实现方式 

方法区主要存什么?

  1. 类的结构信息
当 JVM 加载一个类,比如: classUser{privateString name;publicvoidrun(){}}JVM 会把这些信息解析后存到方法区:类名 父类信息 实现的接口 字段信息 方法信息 访问修饰符 例如: Class:UserFields: name Methods:run()Superclass:Object 这些都是类的元数据(metadata)。 
  1. 运行时常量池
每个 class 文件都有一个 常量池(ConstantPool),里面存着: 字符串常量 类名 方法名 字段名 符号引用 例如代码:String s ="hello"; 这里 "hello" 就会进入运行时常量池(现代 JVM 中字符串对象在堆,但常量池引用在方法区)。 
  1. 静态变量(static 变量)
例如: classUser{staticint count =0;} count 是类级别变量,不是对象变量。所以它不会放在堆里的对象中,而是放在方法区中。 
  1. JIT 编译后的代码

JVM 有 即时编译器(JIT)。热点方法会被编译成本地机器码,提高执行效率。这些编译后的机器码 也会存储在方法区相关区域。

读到这里,相信大家对 JVM 运行时数据区域的结构与作用 已经有了更加系统的理解。如果文中有不清晰或想进一步探讨的地方,欢迎在评论区交流,也可以私信博主一起讨论。

Read more

飞算JavaAI家庭药品管理系统:从药品收纳到健康守护的全链路解决方案

飞算JavaAI家庭药品管理系统:从药品收纳到健康守护的全链路解决方案

目录 * 一、家庭药品管理系统核心模块搭建 * 1.1 药品信息管理模块:让药品“有迹可循” * 1.2 药品状态监测与提醒模块:让风险“提前预警” * 1.3 用药安全与指导模块:让用药“有据可依” * 二、系统技术架构与部署方案 * 2.1 技术架构设计 * 2.2 部署方案 * 结语:技术守护家庭健康的“隐形防线” 在家庭生活中,“药品安全存储”与“便捷取用”的平衡、“过期预警”与“合理用药”的协同始终是家庭健康管理的痛点。传统家庭药品管理常面临“过期遗漏”“存放散乱”“用药混淆”等问题。所以我打算用飞算JavaAI针对家庭场景特性,构建了从药品收纳到健康提醒的轻量级管理系统,实现药品全生命周期可视化管理,过期预警准确率达100%,为家庭健康保驾护航。本文聚焦家庭药品管理系统的技术实现,解析如何通过技术简化家庭药品管理流程。

By Ne0inhk
基于Java和GeoTools的根据矢量BBOx自动生成格网文件实践

基于Java和GeoTools的根据矢量BBOx自动生成格网文件实践

目录 前言 一、基础数据介绍及生成方法简介 1、矢量数据处理 2、格网生成算法 二、代码实现 1、根据Shp计算Bounds 2、生成经纬网要素集合 3、写入 Shapefile 三、结果输出与验证 1、格网文件输出格式 2、GIS工具验证 四、总结 前言         在当今数字化与信息化高速发展的时代,地理空间数据在众多领域发挥着至关重要的作用,如城市规划、资源管理、环境监测、灾害预警等。矢量文件作为地理空间数据的重要存储形式之一,以其精确表达地理实体的形状、位置和属性等特点,被广泛应用于各种地理信息系统(GIS)项目中。然而,在一些特定的应用场景下,我们需要将矢量数据转换为格网文件格式,以便于进行更高效的计算、分析和可视化处理。格网数据在空间分析中具有独特的优势,例如在进行空间插值、地形分析、空间建模等方面,格网数据能够提供更直观、更易于处理的数据结构。然而,

By Ne0inhk
【从0开始学习Java | 第23篇】动态代理

【从0开始学习Java | 第23篇】动态代理

文章目录 * Java动态代理概述 * 一、动态代理的核心概念 * 形象解释 * 二、两种主流动态代理实现 * 1. JDK动态代理(基于接口) * 原理 * 示例代码 * 优缺点 * 2. CGLIB动态代理(基于子类) * 原理 * 示例代码(需引入CGLIB依赖) * 优缺点 * 三、JDK与CGLIB动态代理对比 * 四、实际应用场景 * 五、总结 Java动态代理概述 在Java开发中,代理模式设计模式之一,而动态代理作为代理模式的进阶形式,在框架开发(如Spring AOP)、日志记录、权限控制等场景中发挥着关键作用。本文将从核心概念出发,拆解两种主流动态代理的实现逻辑,并分析其适用场景。 一、动态代理的核心概念 动态代理指在程序运行时,通过反射机制动态生成代理类,而非在编译期预先定义。其核心价值在于:无需为每个目标类手动编写代理类,即可统一为多个目标类添加横切逻辑(如日志、事务、异常处理),降低代码耦合度。

By Ne0inhk
【JDK】-JDK 17 新特性整理(比较全)

【JDK】-JDK 17 新特性整理(比较全)

JDK 17 新特性 1、JDK 17中的Pattern类增强了哪些功能? 1. 新增asMatchPredicate方法: JDK 17的Pattern类新增了asMatchPredicate方法,可以将正则表达式编译为Predicate。 2. 增强了Unicode属性支持: JDK 17中的Pattern类增强了对Unicode属性的支持,使得正则表达式可以更好地处理Unicode字符。 3. 引入了新的转义语法: JDK 17引入了一种新的转义语法,可以更方便地转义正则表达式中的特殊字符,提高了正则表达式的可读性和可维护性。 4. 优化了性能: JDK 17对Pattern类的底层实现进行了优化,提升了正则表达式匹配的性能和效率。 5. 增加了对断言的支持: JDK 17中的Pattern类增加了对断言的支持,可以更灵活地进行正则表达式的匹配和处理。 2、JDK 17中的HTTP/2 Client有哪些新特性? 1. 提供了WebSocket支持: JDK 17中的HTTP/2 Client新增了对WebSocket的支持,使得开发者可以更方便地进行WebS

By Ne0inhk