JVM 是一个应用程序
在启动时, 会向操作系统申请内存空间
根据不同的需求, 将空间分割成不同的部分, 每个部分的功能各不相同
(类似于我们的房子, 根据不同的需求, 将房子的空间进行分割, 一部分成为了卧室, 一部分成为了厨房…)
注意
此处所指的栈
,堆
指代的是JVM
中的内存空间
并非数据结构
中的栈
,堆
程序计数器
记录当前线程执行到哪个指令
(程序计数器是很小的一块内存区域)
Native 表示 JVM 内部的 C++ 代码
本地方法栈
调用 Native 方法(JVM 内部的方法)时准备的栈空间
虚拟机栈
调用 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();
}
}
堆区是 JVM 内存空间的最大区域
通常 new 出来的对象都是存储在堆
元数据区, 也称为方法区
元
即 Meta, 表示属性
元数据区
主要存储类对象, 常量池, 静态成员
这里所说的类对象不是 A a = new A();
而是类似于对象的图纸, 描述了该对象的属性
名称 | 描述 |
---|---|
JVM(Java 虚拟机) | 每个进程有一份 |
Program Counter Register(程序计数器) | 每个线程有一份 |
Native Method Stacks(本地方法栈) | 每个线程有一份 |
JVM Stacks(虚拟机栈) | 每个线程有一份 |
Heap(堆区) | 每个进程有一份 |
Metaspace(元数据区) | 每个进程有一份 |
类加载
将 .class 文件, 从文件(硬盘)加载到内存(元数据区)
.java 通过 javac 编译生成 .class
找到 .class 文件, 打开文件, 读取文件(将文件内容读取至内存中)
检查 .class 文件的格式是否正确
为类对象分配内存空间(此时内存初始化为全0)
静态成员变量的值也就被设为0
初始化字符串常量, 将符号引用转为直接引用
类加载之前
字符串常量位于 .class 文件中
此时的引用记录的并非是字符串常量的真正地址, 而是字符串常量在文件中的"偏移量"(符号引用
)
类加载之后
字符串常量位于内存中(即字符串常量拥有了内存地址)
此时的引用记录的才是真正的内存地址(直接引用
)
针对类对象的内容进行初始化
执行代码块, 静态代码块, 加载父类…
并非 Java 程序运行, 所有的类就会被加载
而是真正用到该类, 才会被加载
(懒汉模式)
(加载过一次之后, 后续使用就不必重复加载)
加载
找到 .class 文件, 打开文件, 读取文件(将文件内容读取至内存中)
双亲委派模型
描述的是找到 .class 文件的基本过程
类加载器
上述三个类加载器
, 存在"父子关系"
此处所说的父子关系并不是父类子类
(可以简单理解为 Parent 属性)
类加载器
进行加载类加载器
进行加载ClassNotFoundException(未找到指定类异常)
加载器
自行加载的情况
类加载器
没有父类加载器
的父加载完毕后仍未找到所需加载的类为什么双亲委派模型的执行顺序是这样的?
上述过程是一个递归的过程(保证了 BootstrapClassLoader 最先执行), 避免因用户创建一些奇怪的类从而引起的 Bug
假设用户在代码中创建了一个系统已存在的类
根据上述的加载流程, 此时 JVM 会先加载标准库中的类, 而不是用户自己代码中的类
这样避免了因为类相同从而可能引起 JVM 标准库中的类出现混乱
自己写的类加载器可以遵守上述的执行过程, 也可以不遵守上述的执行过程
看实际的需求
垃圾
不再使用的内存
垃圾回收
将不用的内存进行释放
如果内存一直占用, 不去释放, 就会导致剩余的空间越来越少, 从而导致后续申请内存失败
由此, Java 中引入了 GC, 帮助我们自动进行释放"垃圾"
假设内存中的垃圾很多, 此时触发一次 GC 操作
其开销可能非常大, 大到可能将系统资源耗光
另一方面, GC 回收垃圾时, 可能会涉及一些锁操作, 导致业务代码无法正常运行
这样的卡顿, 极端情况下可能是几十毫秒甚至上百毫秒
注意
Scanner sc = new Scanner(System.in);
sc.close();
类似于这种释放的是文件资源, 并非内存
GC 主要是针对堆进行内存释放的
这是因为堆上的对象存活时间相对较长
而栈上的对象会随着方法的结束而结束
寻找垃圾
python
/ php
)Java
)引用计数
为每个对象分配一个计数器
创建一个指向该对象的引用时, 该对象的计数器 + 1
销毁一个指向该对象的引用时, 该对象的计数器 - 1
举个栗子
Test t1 = new Test();// Test 对象引用计数 + 1
Test t2 = new Test();// Test 对象引用计数 + 1
Test t3 = new Test();// Test 对象引用计数 + 1
t1 = null;// Test 对象引用计数 - 1
内存空间浪费
循环引用
分析如下伪代码
public class Node {
Node next = null;
}
Node a = new Node();// 1号对象, 引用计数为1
Node b = new Node();// 2号对象, 引用计数为1
a.next = b;// 2号对象, 引用计数为2(a.next 指向 b)
b.next = a;// 1号对象, 引用计数为2(b.next 指向 a)
此时将 a 和 b 进行销毁
a = null;// 1号对象, 引用计数为1(2 - 1 = 1)
b = null;// 2号对象, 引用计数为1(2 - 1 = 1)
此时1号对象和2号对象的引用计数为1, 表示无法释放内存
(引用计数为0时, 释放内存)
但此刻1号对象与2号对象却无法被访问(循环引用
)
可达性分析
Java 中的对象是通过引用进行指向并访问的
可达性分析, 就是将这些对象被组织的结构视为链式结构
从起始位置出发, 遍历链
能够被访问到的对象标记为"可达"
反之即为"不可达"
(将不可达的作为"垃圾"进行回收)
举个栗子
class TreeNode {
int val;
TreeNode left;
TreeNode right;
}
目前所有的节点都是可达的
此时3 → 6之间的连接断开
6不可达
8不可达
于是6, 8被当作垃圾进行回收
将垃圾标记并回收
标记清除的不足
内存碎片问题
被释放的空间是零散的, 不是连续的
申请内存的空间要求是连续的空间
导致总的空闲空间可能很大(非连续), 但每个具体的空间(连续)却较小
从而申请内存失败
将内存空间划分为两半
将不是垃圾的对象复制到另一半, 然后将整个空间删除
复制算法解决了内存碎片问题
但也有其不足
复制算法的不足
将垃圾进行标记
将不是垃圾的对象覆盖垃圾的空间
删除剩余空间
解释
1为垃圾
后面的元素依次进行覆盖
由
1, 2, 3, 4, 5, 6
变为
2, 3, 4, 5, 6
解释
3为垃圾
后面的元素依次进行覆盖
由
2, 3, 4, 5, 6
变为
2, 4, 5, 6
解释
5为垃圾
后面的元素依次进行覆盖
由
2, 4, 5, 6
变为
2, 4, 6
解释
删除剩余空间
标记整理解决了复制算法的空间利用率较低的问题
但也有其不足
标记整理的不足
效率问题
(需要将对象进行搬运, 如果要搬运的对象较多, 此时效率就会较低)
可以看出, 上述对于垃圾清理的操作, 都有其相对的适用场景与不足
那么, 能不能让合适的垃圾清理方法去其适合的场景呢
于是引出了分代回收
分代回收
将垃圾回收划分成不同的场景
不同的场景应用不同的清理方法
(各展所长)
分代是如何划分的
基于经验规律
是的, 基于经验规律
这个规律是这样的
如果一个东西存在的时间比较长, 那么大概率还会继续长时间的存在下去
这个规律, 对于 Java 的对象也是有效的(由一系列的实验和论证过程得出)
于是, 给对象引入了一个概念—年龄
(此处的年龄指代的是熬过GC的轮次, 可以类比于人类的年龄)
于是将堆进行了区域的划分
更为具体的划分
创作不易,如果对您有帮助,希望您能点个免费的赞
大家有什么不太理解的,可以私信或者评论区留言,一起加油