在 Java 生态中,Java 虚拟机(JVM)是实现 “一次编写,到处运行” 的核心基石。它作为连接 Java 源代码与操作系统的中间层,负责将字节码翻译成机器指令并执行,同时管理内存、处理异常、保障线程安全。理解 JVM 的内部结构,不仅是排查性能问题、优化代码的关键,也是深入掌握 Java 语言特性的基础。本文将从 JVM 的整体架构出发,逐一拆解其核心组成部分,详细讲解各模块的功能、工作原理及实际应用场景。
读前小问:局部变量的信息存在哪里?堆?栈?
一、JVM 整体架构:五大核心模块的协同工作
Java 虚拟机的结构遵循《Java 虚拟机规范》,不同厂商(如 Oracle HotSpot、OpenJ9)的实现虽有差异,但核心模块保持一致。整体可划分为类加载子系统、运行时数据区、执行引擎、本地方法接口(JNI) 和垃圾回收器(GC) 五大模块,各模块协同完成字节码的加载、执行与资源管理。
其工作流程可概括为:
类加载子系统将.class字节码文件加载到运行时数据区;
运行时数据区为程序执行提供内存空间(如对象存储、线程栈);
执行引擎将字节码翻译成机器指令并执行,过程中需调用本地方法时通过 JNI 接口与操作系统交互;
垃圾回收器持续回收运行时数据区中不再使用的对象,释放内存;
全程由 JVM 的监控与管理机制(如 JMX)保障稳定性与性能。
下图展示了 JVM 五大模块的交互关系:
二、类加载子系统:字节码的 “入口关卡”
类加载子系统负责将磁盘或网络中的.class文件(字节码)加载到 JVM 内存中,并完成验证、准备、解析等操作,最终生成可被执行引擎使用的 “Class 对象”。整个过程遵循 “双亲委派模型”,确保类加载的安全性与唯一性。
1. 类加载的完整流程:加载、验证、准备、解析、初始化
类加载是一个有序的阶段过程,每个阶段各司其职,缺一不可:
(1)加载(Loading):获取字节码并生成初步结构
核心任务:通过类的全限定名(如java.lang.String)找到对应的.class文件,读取字节流,然后在运行时数据区的 “方法区” 中创建一个代表该类的Class对象,作为后续操作的访问入口。
常见来源:本地磁盘(项目编译后的target/classes目录)、网络(如 Web 应用的WAR包)、动态生成(如ASM框架、Groovy脚本编译)。
实例:当执行Class.forName("com.example.User")时,类加载子系统会从类路径中查找com/example/User.class文件,读取其字节码并生成User.class对象。
(2)验证(Verification):保障字节码的安全性与合法性
核心任务:校验字节码是否符合《Java 虚拟机规范》,避免恶意或非法字节码(如破坏内存安全、违反语法规则)导致 JVM 崩溃。
主要校验内容:
格式验证:检查字节码文件的魔数(0xCAFEBABE)、版本号是否与当前 JVM 兼容;
语义验证:确保类的继承关系合法(如不继承final类)、方法参数与返回值类型匹配;
字节码验证:通过控制流分析,确保指令执行不会跳转到非法位置,不会访问未定义的内存;
符号引用验证:检查类中引用的其他类、方法、字段是否存在(如引用的java.util.List是否真实存在)。
(3)准备(Preparation):为静态变量分配内存并设置默认值
核心任务:在方法区为类的静态变量(static修饰)分配内存空间,并设置 “默认初始值”(而非代码中定义的初始值)。
注意事项:
仅处理静态变量,实例变量的内存分配在对象创建时(在堆中)完成;
默认初始值遵循 Java 基本类型的默认规则(如int为0、boolean为false、引用类型为null)。
实例:若类中有static int num = 10;,准备阶段会为num分配内存,设置默认值0,而非10;10的赋值会在 “初始化” 阶段完成。
(4)解析(Resolution):将符号引用转为直接引用
核心任务:将类、方法、字段的 “符号引用”(如.class文件中用字符串表示的类名、方法名)转换为 “直接引用”(如内存地址、偏移量),让 JVM 能直接定位到目标资源。
触发时机:多数情况下在初始化前完成,但也可延迟到 “首次使用时”(如动态绑定的方法,即多态场景)。
实例:类中引用java.util.ArrayList的add方法,解析前是符号引用(字符串 “java/util/ArrayList.add”),解析后转为直接引用(ArrayList类在方法区的内存地址 + add方法的偏移量)。
(5)初始化(Initialization):执行静态代码块与静态变量赋值
核心任务:执行类的静态代码块(static {})和静态变量的显式赋值语句,将静态变量设置为代码中定义的初始值,这是类加载的最后阶段。
触发条件(主动使用场景):
创建类的实例(如new User());
调用类的静态方法(如User.getVersion());
访问类的静态变量(如System.out.println(User.num));
反射调用(如Class.forName("com.example.User"));
初始化子类时,其父类会先初始化。
实例:对static int num = 10;,初始化阶段会执行赋值语句,将num从默认值0改为10;若有static { System.out.println("User类初始化"); },则会执行该代码块打印日志。
2. 双亲委派模型:类加载的 “安全屏障”
类加载子系统通过 “双亲委派模型” 确保类加载的唯一性和安全性,其核心规则是:当一个类加载器收到类加载请求时,先委托给其父加载器加载,只有父加载器无法加载时,才由自身加载。
(1)类加载器的层级结构
JVM 中默认的类加载器分为三层,自上而下形成父子关系:
启动类加载器(Bootstrap ClassLoader):最顶层,由 C/C++ 实现,负责加载 JVM 核心类库(如java.lang、java.util包下的类),加载路径为JAVA_HOME/lib目录下的rt.jar等文件;
扩展类加载器(Extension ClassLoader):由 Java 实现,父加载器为启动类加载器,负责加载 JVM 扩展类库(如java.nio、javax.swing包下的类),加载路径为JAVA_HOME/lib/ext目录;
应用程序类加载器(Application ClassLoader):又称系统类加载器,父加载器为扩展类加载器,负责加载应用程序的类(如项目中的com.example包下的类),加载路径为classpath(如target/classes、WAR包中的WEB-INF/classes)。
(2)双亲委派的优势
避免类重复加载:若父加载器已加载某类,子加载器无需重复加载,确保内存中只有一个Class对象(如java.lang.String只会被启动类加载器加载,避免应用程序自定义同名类覆盖核心类);
保障核心类安全:禁止应用程序类加载器加载核心类库(如java.lang.Object),防止恶意代码篡改核心类(如自定义java.lang.String并植入恶意逻辑)。
三、运行时数据区:JVM 的 “内存管家”
运行时数据区是 JVM 为程序执行分配内存的区域,也是内存溢出(OOM)问题的高发地。根据《Java 虚拟机规范》,它被划分为方法区、堆、虚拟机栈、本地方法栈和程序计数器五个部分,其中方法区和堆为 “线程共享区域”,其余三个为 “线程私有区域”(每个线程创建时单独分配,线程结束后释放)。
1. 程序计数器:线程执行的 “进度条”
定义:一块较小的内存空间,用于存储当前线程正在执行的字节码指令的 “地址”(行号),是 JVM 中唯一不会发生内存溢出的区域。
核心作用:
线程切换时,保存当前线程的执行位置,恢复时能准确回到切换前的指令;
支持 Java 的分支、循环、跳转等控制流(如if-else、for循环需通过程序计数器定位下一条指令)。
特殊场景:若线程执行的是本地方法(如System.currentTimeMillis(),由 C/C++ 实现),程序计数器的值为undefined(因本地方法不执行字节码)。
2. 虚拟机栈:方法执行的 “临时工作台”
定义:线程私有,每个方法执行时会创建一个 “栈帧”(Stack Frame)并压入虚拟机栈,方法执行完毕后栈帧弹出。虚拟机栈的大小可通过-Xss参数配置(如-Xss256k),超出则抛出StackOverflowError。
栈帧的组成:每个栈帧对应一个方法,包含三个核心部分:
局部变量表:存储方法的参数和局部变量(如int a、User user),容量以 “变量槽(Slot)” 为单位,long 和 double 类型占 2 个 Slot,其余类型占 1 个 Slot;
操作数栈:方法执行过程中用于临时存储操作数和运算结果(如执行a + b时,先将a、b压入操作数栈,再执行加法指令,结果压回栈);
动态链接:将栈帧中引用的 “符号引用”(如方法调用的类名)转为 “直接引用”(如方法在方法区的内存地址),支持方法的动态绑定(多态)。
实例:执行public int add(int a, int b) { return a + b; }时:
方法调用前,a和b作为参数被压入局部变量表;
执行a + b时,从局部变量表取出a、b压入操作数栈,执行加法指令后,结果压回操作数栈;
方法返回时,操作数栈中的结果作为返回值传递给调用者,栈帧弹出。
3. 本地方法栈:本地方法的 “执行空间”
定义:与虚拟机栈功能类似,但专门为 “本地方法”(由 C/C++ 编写,通过 JNI 调用)提供内存支持,线程私有。
差异点:虚拟机栈处理 Java 方法的字节码,本地方法栈处理本地方法的机器指令;不同 JVM 实现对本地方法栈的处理不同(如 HotSpot 将虚拟机栈和本地方法栈合并为同一区域)。
异常场景:本地方法栈溢出时,HotSpot 同样抛出StackOverflowError(而非单独的异常类型)。
4. 堆:对象存储的 “核心仓库”
定义:JVM 中最大的内存区域,线程共享,用于存储所有对象实例(new创建的对象)和数组,是垃圾回收器(GC)的主要工作区域,因此也被称为 “GC 堆”。
内存划分(逻辑分区):为优化 GC 效率,堆通常按对象生命周期划分为三个区域:
新生代(Young Generation):存储新创建的对象,生命周期短(如临时变量),GC 频率高(“Minor GC”)。进一步分为:
Eden 区:对象首次创建时分配的区域,占新生代的 80%;
Survivor 区(From/To):各占新生代的 10%,用于存储 Eden 区 GC 后存活的对象,每次 GC 后 From 和 To 区角色互换。
老年代(Old Generation):存储新生代中多次 GC 后仍存活的对象(生命周期长,如单例对象、缓存对象),GC 频率低(“Major GC/Full GC”),GC 耗时更长。
元空间(Metaspace,JDK 8+):替代 JDK 7 及之前的 “永久代”,用于存储类的元数据(如类结构、方法信息、静态变量),物理内存基于本地内存(而非 JVM 堆内存),默认无大小限制(可通过-XX:MaxMetaspaceSize配置上限)。
对象分配流程:
新对象优先在 Eden 区分配内存;
Eden 区满时触发 Minor GC,存活对象被移到 Survivor From 区;
后续 Minor GC 中,Survivor From 区的存活对象年龄 + 1,年龄达到阈值(默认 15)时移到老年代;
老年代满时触发 Full GC,回收老年代和新生代的对象,若回收后仍无内存,抛出OutOfMemoryError: Java heap space。
配置参数:
堆初始大小:-Xms(如-Xms2g);
堆最大大小:-Xmx(如-Xmx4g);
新生代大小:-Xmn(如-Xmn1g);
元空间最大大小:-XX:MaxMetaspaceSize=512m。
5. 方法区:类元数据的 “存储中心”
定义:线程共享,用于存储类的元数据(如类结构、方法字节码、字段信息、静态变量、常量池),在 JDK 8 之前称为 “永久代”(PermGen),JDK 8 及之后被元空间(Metaspace)替代。
核心存储内容:
类的结构信息:类的全限定名、父类、接口、访问修饰符(如public、final);
方法信息:方法的字节码、参数列表、返回值类型、异常表;
常量池:存储类中的常量(如字符串常量"hello"、整数常量100、符号引用);
静态变量:JDK 7 及之前,静态变量存储在永久代;JDK 8 及之后,静态变量移至堆中(作为Class对象的一部分)。
异常场景:
JDK 7 及之前:永久代满时抛出OutOfMemoryError: PermGen space(可通过-XX:PermSize和-XX:MaxPermSize配置大小);
JDK 8 及之后:元空间若达到MaxMetaspaceSize限制,抛出OutOfMemoryError: Metaspace。
四、执行引擎:字节码的 “翻译官” 与 “执行者”(补全 + 续更)
2. 即时编译(JIT):热点代码编译,提升效率(补全)
热点代码的判定:JVM 通过 “计数器” 机制识别热点代码,核心是两个计数器:
方法调用计数器:统计方法被调用的次数,默认阈值在 Client 模式下为 1500 次,Server 模式下为 10000 次(可通过-XX:CompileThreshold调整)。当调用次数达到阈值时,触发 JIT 编译;
回边计数器:统计循环代码块的执行次数(“回边” 指循环跳转指令),默认阈值为方法调用计数器阈值的 1/10。若循环执行次数达标,即使方法总调用次数未到阈值,也会触发 JIT 编译(避免 “循环内代码反复解释执行” 的低效问题)。
JIT 编译器的分类(以 HotSpot 为例):
C1 编译器(Client Compiler):轻量级编译器,编译速度快,针对客户端应用(如桌面程序)优化,注重启动速度;
C2 编译器(Server Compiler):重量级编译器,编译速度慢但优化效果好(如循环展开、常量折叠、逃逸分析),针对服务端应用(如 Java Web)优化,注重长期运行效率;
分层编译(JDK 7+):结合 C1 和 C2 的优势,将编译分为 5 个层级,从 “纯解释执行” 到 “C2 优化编译” 逐步升级,平衡启动速度与运行效率。
3. 混合模式:解释与 JIT 的协同优化
原理:JVM 默认采用 “混合模式” 执行字节码 —— 代码首次执行时通过解释器逐行翻译,同时通过计数器统计热点代码;当代码成为热点后,触发 JIT 编译为机器指令并缓存,后续执行直接调用缓存的机器指令,未成为热点的代码仍保持解释执行。
优势:兼顾 “启动速度” 与 “运行效率”—— 解释器保障快速启动,JIT 保障热点代码的高效执行,是当前主流 JVM(如 HotSpot)的默认执行模式(可通过-Xint强制纯解释执行,-Xcomp强制纯 JIT 编译)。
4. 执行引擎的辅助组件
逃逸分析:JIT 编译时的核心优化手段,分析对象是否 “逃逸” 出方法(如是否被外部引用)。若对象未逃逸(仅在方法内使用),可将其分配在 “栈内存”(而非堆内存),减少 GC 压力;若对象完全未被引用,甚至可直接 “标量替换”(将对象拆解为局部变量,消除对象创建)。
实例:public void calculate() { User user = new User(); user.setName("test"); }中,user对象未逃逸出calculate方法,JIT 可将其分配在栈上,方法执行结束后随栈帧弹出销毁,无需 GC 回收。
常量折叠与常量传播:优化编译后的机器指令,例如将int a = 1 + 2;直接编译为int a = 3;(常量折叠),将int b = a; int c = b + 5;优化为int c = 3 + 5;(常量传播),减少运行时计算量。
五、本地方法接口(JNI):JVM 与本地代码的 “桥梁”
本地方法接口(Java Native Interface,JNI)是 JVM 与操作系统本地代码(如 C/C++、汇编)交互的标准接口,其核心作用是让 Java 代码能调用本地方法,同时让本地方法能访问 Java 虚拟机的内存(如对象、数组)。
1. 本地方法的定义与调用流程
本地方法的定义:在 Java 方法上添加native关键字,声明该方法由本地代码实现,无需编写方法体,例如:
public class NativeDemo {
// 声明本地方法,由C/C++实现
public native long getCurrentTime();
static {
// 加载本地库(Windows为.dll,Linux为.so,Mac为.jnilib)
System.loadLibrary("NativeLib");
}
public static void main(String[] args) {
NativeDemo demo = new NativeDemo();
// 调用本地方法
System.out.println("当前时间戳:" + demo.getCurrentTime());
}
}
调用流程:
Java 代码通过System.loadLibrary加载本地库(包含本地方法实现);
调用native方法时,JVM 通过 JNI 接口找到本地库中对应的方法(方法名需遵循 JNI 命名规范,如Java_NativeDemo_getCurrentTime);
本地方法执行时,可通过 JNI 提供的 API(如GetObjectField、SetIntArrayRegion)访问 JVM 内存中的对象、字段或数组;
本地方法执行完毕后,将结果返回给 Java 代码。
2. JNI 的核心作用与风险
核心作用:
调用操作系统底层功能:如操作硬件(打印机、传感器)、访问系统内核接口(如进程管理、内存分配),弥补 Java “跨平台” 特性带来的底层操作限制;
复用现有 C/C++ 库:如调用 OpenCV(图像处理)、FFmpeg(音视频处理)等成熟本地库,避免重复开发;
提升性能敏感场景的效率:如高频数据计算、大规模 IO 操作,通过 C/C++ 的高效执行提升性能。
潜在风险:
破坏跨平台性:本地库依赖具体操作系统(如 Windows 的.dll 无法在 Linux 运行),导致 Java 程序失去 “一次编写,到处运行” 的特性;
内存安全问题:本地代码直接操作内存,若存在内存泄漏(如未释放 malloc 分配的内存)或野指针,会导致 JVM 崩溃(且难以排查);
调试难度大:本地代码的调试需依赖 C/C++ 调试工具(如 GDB、Visual Studio),与 Java 调试工具(如 IDEA Debug)不兼容,问题定位复杂。
六、垃圾回收器(GC):JVM 的 “内存清洁工”
垃圾回收器(Garbage Collector,GC)是 JVM 负责回收 “无用对象”(不再被引用的对象)、释放堆内存的核心组件。其核心目标是:在保证程序正常运行的前提下,高效回收内存,减少内存溢出风险,同时降低 GC 对业务线程的影响(即 “低停顿”)。
1. 垃圾判定算法:如何识别 “无用对象”
GC 的第一步是判定堆中哪些对象是 “无用的”(需回收的),主流判定算法有两种:
(1)引用计数法
原理:为每个对象维护一个 “引用计数器”,当对象被引用时计数器 + 1,引用失效时计数器 - 1;当计数器为 0 时,判定为无用对象。
优势:实现简单,判定效率高,无需暂停业务线程;
缺陷:无法解决 “循环引用” 问题(如 A 引用 B,B 引用 A,且两者均无其他外部引用,计数器均为 1,无法被回收),因此主流 JVM(如 HotSpot)未采用该算法。
(2)可达性分析算法(主流算法)
原理:以 “GC Roots”(根对象)为起点,遍历对象引用链(如 A→B→C),若某个对象无法通过任何 GC Roots 遍历到(即 “不可达”),则判定为无用对象。
GC Roots 的常见类型:
虚拟机栈中局部变量表引用的对象(如方法中的User user = new User(),user引用的对象);
方法区中静态变量引用的对象(如static User globalUser = new User());
方法区中常量引用的对象(如final User CONST_USER = new User());
本地方法栈中本地方法引用的对象(如 JNI 调用中引用的 Java 对象);
JVM 内部引用(如 Class 对象、异常对象、系统类加载器)。
优势:能解决循环引用问题,是 HotSpot 等主流 JVM 的默认垃圾判定算法。
2. 垃圾回收算法:如何回收 “无用对象”
当 GC 识别出无用对象后,需通过具体算法回收其占用的内存,核心算法分为四类:
(1)标记 - 清除算法(Mark-Sweep)
流程:分为 “标记” 和 “清除” 两个阶段:
标记阶段:通过可达性分析,标记所有无用对象;
清除阶段:遍历堆内存,直接回收无用对象的内存空间,标记为 “空闲内存”。
优势:实现简单,无需移动对象;
缺陷:
内存碎片:回收后的空闲内存分散在堆中,若后续需要分配大对象,可能因 “找不到连续的空闲内存” 导致 OOM(即使总空闲内存足够);
效率低:需遍历两次堆内存(标记一次、清除一次),对大堆内存场景性能较差。
(2)标记 - 复制算法(Mark-Copy)
流程:将堆内存划分为两个大小相等的区域(如 From 区和 To 区),仅使用其中一个区域(From 区):
标记阶段:标记 From 区中的有用对象;
复制阶段:将 From 区的有用对象复制到 To 区(按顺序排列,无内存碎片);
切换阶段:清空 From 区,将 From 区和 To 区角色互换,后续对象分配到新的 From 区。
优势:无内存碎片,分配对象时只需 “指针碰撞”(移动指针即可找到连续空闲内存),效率高;
缺陷:内存利用率低(仅使用 50% 的堆内存),适合 “对象存活率低” 的场景(如新生代,90% 以上对象会在 Minor GC 中被回收)。
(3)标记 - 整理算法(Mark-Compact)
流程:结合 “标记 - 清除” 和 “标记 - 复制” 的优势,适合 “对象存活率高” 的场景(如老年代):
标记阶段:标记所有无用对象;
整理阶段:将有用对象向堆内存的一端移动,集中排列;
清除阶段:直接回收有用对象另一端的所有无用对象(批量清除,无需遍历单个对象)。
优势:无内存碎片,内存利用率 100%;
缺陷:整理阶段需移动对象,且需更新所有引用该对象的指针(如虚拟机栈中的引用、其他对象的字段引用),耗时较长,会导致 GC 停顿时间增加。
(4)分代收集算法(Generational Collection)
原理:基于 “对象生命周期不同,回收策略不同” 的思想,将堆分为新生代和老年代,分别采用不同的回收算法:
新生代(对象存活率低):采用 “标记 - 复制算法”,只需复制少量存活对象,效率高;
老年代(对象存活率高):采用 “标记 - 清除” 或 “标记 - 整理算法”,平衡内存碎片与回收效率。
优势:针对不同区域的特性选择最优算法,是当前所有主流 GC(如 CMS、G1)的基础设计思想。
3. 主流垃圾回收器(以 HotSpot 为例)
不同 JDK 版本默认的 GC 不同(如 JDK 8 默认 Parallel GC,JDK 9 + 默认 G1 GC),核心 GC 的特性对比如下:
4. GC 的核心参数配置(常用)
启用 G1 GC:-XX:+UseG1GC(JDK 9 + 默认,JDK 8 需手动开启);
设置最大 GC 停顿时间:-XX:MaxGCPauseMillis=200(G1 GC 优先保证停顿时间不超过该值);
设置新生代比例:-XX:NewRatio=2(老年代与新生代的比例为 2:1,默认值);
打印 GC 日志:-XX:+PrintGCDetails -Xloggc:gc.log(输出 GC 详情到 gc.log 文件,用于排查 GC 问题);
禁用显式 GC:-XX:+DisableExplicitGC(禁止代码中调用System.gc()触发 Full GC,避免手动干扰 GC)。
七、JVM 结构的整体总结与实践意义
1. 核心模块的协同关系
JVM 的五大模块(类加载子系统、运行时数据区、执行引擎、JNI、GC)并非独立工作,而是形成闭环协作:
类加载子系统将字节码加载到运行时数据区,为执行提供 “原材料”;
执行引擎从运行时数据区读取字节码,翻译为机器指令并执行,过程中通过 JNI 调用本地方法;
运行时数据区为执行提供内存支持,同时 GC 持续回收无用对象,释放内存;
全程由 JVM 的监控机制(如 JMX、JVM Profiler)跟踪状态,确保各模块稳定运行。
2. 理解 JVM 结构的实践意义
排查性能问题:例如 “频繁 Full GC” 可能是老年代对象过多(需优化对象生命周期),“StackOverflowError” 可能是虚拟机栈过小(需调整-Xss);
优化代码与配置:例如通过逃逸分析优化对象分配(减少 GC),通过 G1 GC 降低服务响应延迟;
应对面试与技术选型:理解 JVM 结构是 Java 高级工程师的核心能力,也是选择 JDK 版本(如 JDK 8 vs JDK 17)、GC 类型(如 G1 vs ZGC)的关键依据;
排查内存溢出:例如OutOfMemoryError: Java heap space需增大堆内存(-Xmx),Metaspace溢出需检查类加载是否存在泄漏(如频繁动态生成类)。
3. 常见问题与解决方案(实践案例)
案例 1:服务启动慢:可能是类加载时间过长(如依赖包过多),可通过 “类加载预热”(-XX:+TraceClassLoading分析加载耗时)、优化依赖(剔除无用 jar 包)解决;
案例 2:接口响应延迟高:可能是 GC 停顿时间长(如 Parallel GC 的 Full GC),可切换为 G1 GC 并设置MaxGCPauseMillis,或通过 GC 日志分析是否存在内存泄漏;
案例 3:内存溢出(OOM):通过jmap -dump:format=b,file=heap.dump <pid>导出堆快照,使用 MAT(Memory Analyzer Tool)分析大对象或泄漏对象(如未关闭的连接、静态集合无限存储)。
通过深入理解 JVM 的内部结构,开发者能从 “代码层面” 上升到 “虚拟机层面” 优化系统,应对高并发、大数据量场景下的性能挑战,这也是 Java 技术栈从 “入门” 到 “精通” 的关键跨越。
回答上面问题:在 Java 中,局部变量的存储位置取决于其数据类型:
基本数据类型的局部变量:直接存储在虚拟机栈的栈帧局部变量表中。
例如:
int a = 10; boolean flag = true;这些变量的值直接存放在栈内存中,随着方法执行结束(栈帧弹出)而销毁。
引用类型的局部变量:变量本身(引用地址)存储在虚拟机栈的局部变量表中,而引用指向的对象实例则存储在堆中。
例如:
User user = new User();其中
user变量(引用)在栈上,new User()创建的对象在堆上。当方法执行结束,栈上的引用
user会被销毁,但堆上的对象会保留,直到被垃圾回收器清理。
关注公众号:砚知集!!!