Java基础知识总结(二):JVM内存结构与变量生命周期
很多Java开发者刚开始学习时都会有这样的疑问:
- 对象到底存在哪里?
- new出来的对象什么时候被回收?
- 局部变量和成员变量有什么区别?
- static变量到底存放在哪里?
这些问题都与 JVM 内存结构密切相关。
本文将从 JVM 运行时数据区出发,深入理解 Java 程序运行过程中内存是如何分配和管理的。
目录
- JVM内存结构概述
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- 堆内存
- 方法区
- 对象创建过程
- 成员变量与局部变量区别
- static变量存储位置
- 生命周期分析
- 常见面试题
- 总结
一、JVM内存结构概述
Java程序运行时,JVM会将内存划分为多个区域。
主要包括:
text
┌──────────────┐
│ 程序计数器 │
├──────────────┤
│ Java虚拟机栈 │
├──────────────┤
│ 本地方法栈 │
├──────────────┤
│ 堆 │
├──────────────┤
│ 方法区 │
└──────────────┘
通常面试中提到的JVM内存结构,指的就是这些运行时数据区。
二、程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间。
作用:
记录当前线程正在执行的字节码指令地址。
简单理解:
text
程序执行到哪一行代码了
程序计数器负责记录
例如:
java
public static void main(String[] args) {
int a = 10;
int b = 20;
int c = a + b;
System.out.println(c);
}
JVM执行时:
text
执行第一句
记录位置
执行第二句
更新位置
执行第三句
更新位置
程序计数器就是一个"导航仪"。
为什么需要程序计数器?
Java支持:
- 多线程
- 线程切换
例如:
text
线程A执行一半
CPU切换到线程B
线程B执行完
再切换回线程A
程序计数器能够帮助线程恢复到之前执行的位置。
特点
程序计数器:
text
线程私有
每个线程都有自己的程序计数器。
也是 JVM 唯一不会发生 OutOfMemoryError 的区域。
三、Java虚拟机栈(Stack)
栈是面试中最常见的知识点之一。
作用:
保存方法运行时产生的数据。
方法调用过程
例如:
java
public static void main(String[] args) {
test();
}
public static void test() {
int num = 10;
}
执行过程:
text
main() 入栈
↓
test() 入栈
↓
test()执行结束
↓
test()出栈
↓
main()结束
↓
main()出栈
图示:
text
┌───────┐
│ test │
├───────┤
│ main │
└───────┘
栈中存放什么?
主要存放:
text
局部变量
方法参数
方法调用信息
返回地址
例如:
java
public void test() {
int a = 10;
String name = "Tom";
}
变量:
java
a
name
都存放在栈中。
栈的特点
先进后出(FILO)
text
Last In First Out
后进入的方法先执行完毕。
四、本地方法栈(Native Method Stack)
本地方法栈专门服务于:
java
native
修饰的方法。
例如:
java
public native void start0();
JDK底层很多功能都依赖native方法实现。
例如:
- 操作系统交互
- 文件系统
- 网络通信
- 硬件访问
为什么需要本地方法?
Java本身无法直接操作:
text
CPU
内存
磁盘
网卡
因此需要借助:
text
C
C++
编写的本地方法实现。
五、堆(Heap)
堆是 JVM 中最大的一块内存区域。
作用:
存储对象和数组。
对象创建过程
例如:
java
Student stu = new Student();
执行时:
第一步
栈中创建引用变量
text
stu
第二步
堆中创建对象
text
Student对象
第三步
引用指向对象
text
stu → Student对象
图示:
text
栈(Stack)
stu
│
▼
堆(Heap)
Student对象
数组也存放在堆中
例如:
java
int[] nums = {1,2,3};
图示:
text
栈
nums
│
▼
堆
[1,2,3]
堆中的默认值
对象创建后会自动初始化。
例如:
java
class User {
int age;
boolean flag;
}
创建对象:
java
User user = new User();
默认值:
java
age = 0
flag = false
为什么堆需要GC?
因为:
java
new User();
new User();
new User();
会不断创建对象。
如果不回收:
text
内存耗尽
因此 JVM 提供:
text
Garbage Collection
垃圾回收机制。
六、方法区(Method Area)
方法区用于保存:
text
类信息
方法信息
常量
静态变量
运行时常量池
例如:
java
public class User {
private String name;
public void login() {
}
}
类加载后:
text
User类的信息
login方法的信息
都会进入方法区。
方法区的发展
JDK6
text
永久代(PermGen)
属于方法区实现。
JDK8
永久代被移除。
改为:
text
元空间(MetaSpace)
使用本地内存。
七、对象创建全过程
例如:
java
User user = new User();
执行过程:
1. 类加载
如果User类未加载:
text
加载User.class
进入方法区。
2. 分配内存
在堆中创建对象。
3. 默认初始化
例如:
java
int age;
初始化为:
java
0
4. 调用构造方法
java
public User() {
}
执行构造方法。
5. 返回对象地址
引用变量保存地址。
text
user → 对象
八、成员变量与局部变量区别
这是面试高频题。
定义位置不同
成员变量:
java
public class User {
int age;
}
定义在类中、方法外。
局部变量:
java
public void test() {
int age = 18;
}
定义在方法内部。
默认值不同
成员变量:
有默认值。
例如:
java
int age;
默认:
java
0
局部变量:
没有默认值。
必须先赋值再使用。
错误示例:
java
public void test() {
int age;
System.out.println(age);
}
编译失败。
作用域不同
成员变量:
text
整个对象
都可以访问。
局部变量:
text
仅当前方法
有效。
存储位置不同
成员变量:
text
堆
跟随对象存在。
局部变量:
text
栈
跟随方法存在。
生命周期不同
成员变量:
text
对象创建
↓
对象销毁
局部变量:
text
方法开始
↓
方法结束
对比表
| 对比项 | 成员变量 | 局部变量 |
|---|---|---|
| 定义位置 | 类中 | 方法中 |
| 默认值 | 有 | 无 |
| 存储位置 | 堆 | 栈 |
| 生命周期 | 对象生命周期 | 方法生命周期 |
| 作用域 | 整个类 | 当前方法 |
九、static变量存储位置
很多初学者认为:
text
static变量在堆中
实际上需要区分JDK版本。
JDK6
text
永久代(方法区)
JDK8+
text
元空间保存类信息
静态变量实际存储在堆中
因此现在更准确的说法:
text
静态变量属于类
生命周期与类一致
对象共享同一份静态变量
示例:
java
public class User {
public static int count = 0;
}
无论创建多少对象:
java
new User();
new User();
new User();
都共享:
java
count
十、常见面试题
面试题1
对象存放在哪里?
答案:
text
堆(Heap)
面试题2
局部变量存放在哪里?
答案:
text
Java虚拟机栈
面试题3
数组存放在哪里?
答案:
text
堆
面试题4
方法调用为什么要入栈?
答案:
text
保存方法运行状态
记录局部变量
记录返回地址
面试题5
成员变量和局部变量有什么区别?
答案:
text
定义位置不同
默认值不同
作用域不同
生命周期不同
存储位置不同
面试题6
为什么Java需要垃圾回收?
答案:
text
对象不断创建
如果不回收
最终导致内存耗尽
总结
本文介绍了 JVM 运行时数据区的重要组成部分:
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- 堆
- 方法区
同时分析了:
- 对象创建过程
- 成员变量与局部变量区别
- static变量存储位置
- 生命周期差异
掌握这些知识后,你将能够更好地理解:
- 对象为什么会被回收
- 内存溢出的原因
- JVM性能调优基础
- Java面试中的内存相关问题