🔎内存区域划分
JVM 是一个应用程序
在启动时, 会向操作系统申请内存空间
根据不同的需求, 将空间分割成不同的部分, 每个部分的功能各不相同
(类似于我们的房子, 根据不同的需求, 将房子的空间进行分割, 一部分成为了卧室, 一部分成为了厨房…)
- JVM 将内存区域划分为5个部分
- Native Method Stacks(本地方法栈)
- Program Counter Register(程序计数器)
- JVM Stacks(虚拟机栈)
- Heap(堆区)
- Metaspace(元数据区, 也称为方法区)
注意🍭
此处所指的栈
,堆
指代的是JVM
中的内存空间
并非数据结构
中的栈
,堆
Program Counter Register(程序计数器)
程序计数器
记录当前线程执行到哪个指令
(程序计数器是很小的一块内存区域)
Native Method Stacks(本地方法栈)
Native 表示 JVM 内部的 C++ 代码
本地方法栈
调用 Native 方法(JVM 内部的方法)时准备的栈空间
JVM Stacks(虚拟机栈)
虚拟机栈
调用 Java 代码时准备的栈空间
栈空间内部包含很多的元素(每个元素表示一个方法)
每一个元素又称为是一个栈帧
- 栈帧包含
- 方法的入口
- 方法的参数
- 返回地址
- 局部变量
- …
区分虚拟机栈与本地方法栈🍭
- 对于
本地方法栈
, 存储的是Native方法(C++代码)
之间的调用关系 - 对于
虚拟机栈
, 存储的是方法(Java代码)
之间的调用关系 - 方法的调用, 具有
后进先出
的特点, 此处的栈也是后进先出
方法的调用, 后进先出
⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
public static void main(String[] args) { System.out.println(testAndVerify()); } private static String testAndVerify() { return "welcome to bibubibu's blog!"; }
栈是线程私有的🍭
对于栈是线程私有的这句话, 并不是足够的准确(个人理解)
私有表示的意思是我的东西, 你不能碰
类似于这台笔记本是我私有的, 你不能碰我的笔记本
但对于一个线程的内容来说, 另一个线程可以通过变量捕获的方式获取
t1 线程通过变量捕获方式访问 main 线程的局部变量 locker
⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
public class ThreadDemo { public static void main(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { try { System.out.println("wait开始"); synchronized(locker) { locker.wait(); } System.out.println("wait结束"); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); } }
Heap(堆区)
堆区是 JVM 内存空间的最大区域
通常 new 出来的对象都是存储在堆
Metaspace(元数据区)
元数据区, 也称为方法区
元
即 Meta, 表示属性
元数据区
主要存储类对象, 常量池, 静态成员
这里所说的类对象不是 A a = new A();
而是类似于对象的图纸, 描述了该对象的属性
类对象
⬇⬇⬇⬇⬇
总结
名称 | 描述 |
JVM(Java 虚拟机) | 每个进程有一份 |
Program Counter Register(程序计数器) | 每个线程有一份 |
Native Method Stacks(本地方法栈) | 每个线程有一份 |
JVM Stacks(虚拟机栈) | 每个线程有一份 |
Heap(堆区) | 每个进程有一份 |
Metaspace(元数据区) | 每个进程有一份 |
- 局部变量默认存储在栈
- 普通成员变量默认存储在堆
- 静态成员变量默认存储在元数据区(方法区)
🔎类加载
类加载
将 .class 文件, 从文件(硬盘)加载到内存(元数据区)
.java 通过 javac 编译生成 .class
- 类加载的步骤
- 加载(Loading)
- 连接(Linking)
- 验证(Verfication)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
类加载的流程
加载
找到 .class 文件, 打开文件, 读取文件(将文件内容读取至内存中)
验证
检查 .class 文件的格式是否正确
- .class 是一个二进制文件, 其格式有着严格的说明
- 官方提供的 JVM 规范文档详细描述了 .class 的格式
准备
为类对象分配内存空间(此时内存初始化为全0)
静态成员变量的值也就被设为0
解析
初始化字符串常量, 将符号引用转为直接引用
符号引用转为直接引用🍭
- 字符串常量包括
- 内存空间, 存储该字符串的实际内容
- 引用, 存储内存空间的起始地址
类加载之前
字符串常量位于 .class 文件中
此时的引用记录的并非是字符串常量的真正地址, 而是字符串常量在文件中的"偏移量"(符号引用
)
类加载之后
字符串常量位于内存中(即字符串常量拥有了内存地址)
此时的引用记录的才是真正的内存地址(直接引用
)