JVM 是运行 Java 字节码的虚拟计算机,是 Java 实现跨平台的核心。
Java 源码 .java → 编译器 javac 编译成 字节码 .class
口号:一次编译,到处运行
同一套 class 文件,Windows、Linux、Mac 上,只要装了对应平台的 JVM 就能跑。 注意:JVM 本身不是跨平台的,不同系统有专属 JVM。
一、双亲委派模型
1.类加载器
负责加载class文件,class文件在文件开头有特定的文件标识(cafe babe)。
2.沙箱安全机制
核心目标是隔离不可信代码,限制它对本地系统资源的访问,就算恶意代码执行,影响也被限制在沙箱内,不会破坏主机。
3.双亲委派模型

第一步:从下到上
-
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器PlatformClassLoader去完成。
-
2、当PlatformClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器BootStrapClassLoader去完成。
-
第二步:从上到下
-
3、如果BootStrapClassLoader加载失败,会用PlatformClassLoader来尝试加载;
-
4、若PlatformClassLoader也加载失败,则会使用AppClassLoader来加载
-
5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
作用:
- 安全:防止自定义的 java.lang.String 这类核心类替换掉 JDK 原生类。
- 保证类唯一:同一个类加载器下,一个类只会加载一次。
二、堆、栈、方法区
1.堆
- 归属:线程共享,JVM 启动创建
- 存什么 :所有 new 出来的对象实例、数组;对象里的实例变量( 静态变量和字符串常量池**)**
- GC :主要 GC 区域,垃圾回收重点(垃圾回收)自动帮JVM清理堆里面不再使用的对象,释放内存,不用程序员手动写代码释放内存 )
- 生命周期:JVM 关闭才释放
特点:存放对象本体,多个线程都能访问,容易线程安全问题
Java堆分代1 新生代(类诞生的地方)
- 存放刚 new 出来、存活时间短的对象。绝大多数对象朝生夕灭。
- 划分为:Eden 区 : Survivor0 : Survivor1 = 8 : 1 : 1
- 新对象优先放 Eden;Eden 满触发Minor GC(新生代 GC)
- 存活对象复制到空闲 Survivor 区,年龄 + 1;
- 对象年龄达到阈值(默认 15),晋升到老年代。
- GC 算法:复制算法
2.老年代
- 存放存活久、年龄大的对象;大对象(很长的数组)直接进老年代。
- GC:Major GC(老年代 GC),经常伴随 Minor GC,合称 Full GC。
- GC 算法:标记 - 整理
3.永久代 / 元空间(方法区,不属于堆)
存放jdk自身存在的class对象,接口interface,元数据,存储的是Java运行时的一些环境
- JDK7 及更早:永久代 PermGen,放在堆内存,容易 OOM。
- JDK8:删除永久代 → 元空间 Metaspace,使用操作系统本地内存。
- 存:Class 类模板、类元信息、运行时常量池。
- GC:不存在垃圾回收,关闭虚拟机就会释放这个区域的内存
年轻代GC触发机制(Minor GC ):年轻代的Eden空间不足,触发Minor GC。
每次Minor GC在清理Eden的同时会清理Survivor From区。
Minor GC非常频繁,回收速度块。
引发STW(Stop The World),暂停其他用户线程,垃圾回收结束,用户线程恢复。
老年代GC触发机制(Major GC 和 Full GC ):
老年代满了,对象从老年代消失是因为发生了Major GC 。
Major GC比Minor GC速度慢10倍以上,STW时间更长。
如果Major GC后,内存还不足,就报OOM。
什么情况下,对象进去到养老区中
第一种情况:对象经过15次gc还存活,进入养老区中 第二种情况:对象伊甸区放不下,直接进入养老区中,如果养老区放不下直接抛出oom异常
2.虚拟机栈
栈:线程私有,生命周期和线程一致。存局部变量、方法调用。速度快,内存自动回收。
- 归属 :线程私有,一个线程对应一个栈
- 存什么 :栈帧;栈帧里面是局部变量表、操作数栈、方法返回地址等
- 局部变量(基本类型、对象引用地址)
- 生命周期:线程结束,栈直接销毁
- **栈溢出:出现在递归调用时。**每次调用方法压入栈帧,无限递归 / 方法嵌套过深,栈帧堆满,超出栈最大深度就报错。
3.方法区
- 归属:线程共享
- 存什么 :类元信息(Class 模板)、字段 / 方法描述、运行时常量池
- static 静态变量
- GC:可以卸载类,条件苛刻;大量动态类会 Metaspace OOM
三、Native关键字
ative 用来修饰本地方法 。被 native 修饰的方法,只有方法声明,没有 Java 实现,
凡是带了native关键字的,说明Java范围打不到了,只能回去调用c语言库。
会进入本地方法栈,调用本地方法接口,JNI(扩展Java的使用,融合不同的语言为Java使用)
本地方法栈,就是专门给 native 方法服务的。
Native Method Stack本地方法栈存储了从Java代码中调用本地方法时所需的信息。是线程私有的。
四、OOM异常
OOM内存不足错误,当 JVM 需要分配内存,但没有足够可用内存时抛出,属于 Error,不能 try-catch 捕获。
最常见是堆 OOM :堆用来存放对象实例,不断创建对象且 GC 无法回收(内存泄漏 / 对象一直被引用),堆内存占满,抛出。
什么情况下,出现堆OOM异常?
情况一:养老区满了,执行gc还满的状态,抛出oom异常
情况二:大对象,伊甸区放不下,养老区也放不下,抛出oom异常
OOM 分 4 种常见:
Java heap space:堆内存满,对象太多,Full GC 之后还是放不下;PermGen space:JDK7 及以前永久代溢出,加载类过多;JDK8 废弃;Metaspace:JDK8 之后元空间溢出,大量动态类,使用本地内存;Direct Memory space:NIO 堆外直接内存溢出,不受‑Xmx 控制,由 MaxDirectMemorySize 控制大小。
五、GC垃圾回收
Java引入了自动的垃圾回收(Garbage Collection简称GC )机制。通过垃圾回收器来对不再使用的对象完成自动的回收,主要负责对堆回收
1.GC作用区域:堆
JVM 堆分为新生代、幸存区(From、To)、老年代。GC 不会一次性回收整个堆,大部分回收发生在新生代。
GC 分为:
- 轻 GC:只回收新生代,发生频率高,速度快,暂停所有用户线程。
- 重 GC:回收整个堆(新生代 + 老年代 + 元空间),STW 时间很长,尽量避免触发。
判断对象是否存活方法:
① 引用计数法
判断对象的引用数量来决定对象是否可以被回收。
基本思路:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;
任何时刻计数器为0的对象就是不可能再被使用的。目前主流的Java虚拟机都摒弃掉了这种算法
②可达性分析算法
可达性分析是 JVM 判断对象是否存活的算法,也是现代 GC 的核心算法。
思路:从一系列GC Roots(根对象) 作为起始节点向下搜索,走过的路径叫引用链。能被引用链到达的对象就是存活对象;无法到达的对象,判定为可回收垃圾对象。
2.垃圾回收算法
①标记‑清除法(老年代)
核心流程
- 标记:用可达性分析标记所有存活对象
- 清除:遍历堆,回收没有标记的垃圾对象
适用:老年代(CMS 收集器底层就是标记清除)
②复制算法(新生代)
核心流程 把内存划分为两块空间 From、To;GC 时把 From 中存活对象全部复制到空的 To 区域,然后清空 From,交换 From/To 角色。
HotSpot 新生代就是复制算法:Eden + From Survivor + To Survivor(8:1:1),不是对半分,解决一半内存浪费问题。 适合对象朝生夕死、存活率低的新生代。老年代不适合用复制算法。
③标记压缩
核心流程
- 标记:标记存活对象
- 整理:所有存活对象向内存一端移动压缩,然后直接清理边界以外全部垃圾
适合对象存活率高的老年代,Serial Old、Parallel Old 使用标记整理。
④分代收集算法
⚠重点:不是全新算法,是上面 3 种的组合策略,
HotSpot 核心思想。 根据对象存活生命周期不同,把堆分成新生代、老年代,不同区域使用不同回收算法:
- 新生代(Young) :对象存活率低,朝生夕死 → 使用复制算法,回收速度快。
- 老年代(Old) :对象存活率高 → 使用标记清除 / 标记整理。
| 算法 | 优点 | 缺点 | 适用区域 |
|---|---|---|---|
| 标记清除 | 简单 | 内存碎片,两次扫描 | 老年代 CMS |
| 复制 | 无碎片,速度快 | 浪费内存;高存活时代价大 | 新生代 |
| 标记整理 | 无碎片,不浪费内存 | 移动对象开销大 | 老年代 SerialOld/ParallelOld |
| 分代收集 | 因地制宜,综合性能最优 | 无新算法,组合使用 | 整个堆,HotSpot 默认 |
六、四种引用
Java 把对象引用分为 4 类:强引用、软引用、弱引用、虚引用 ,用来控制 GC 对待对象的回收行为,配合java.lang.ref包。
1、强引用
java
User user = new User();
我们平时写的普通对象赋值就是强引用。
- 只要强引用还存在,GC 永远不会回收该对象。
- 即使 OOM,也不会回收。
- 想要回收对象:必须切断强引用
user = null,消除引用关系。
2、软引用
java
SoftReference<User> ref = new SoftReference<>(new User(1,"张三"));
User u = ref.get(); // 获取对象
SoftReference 类实现软引用。在系统要发生内存溢出(OOM)之前,才会将这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。软引用可用来实现内存敏感的高速缓存。
- 内存充足,不回收;内存快要 OOM 之前,会回收软引用对象。
- 适合做内存敏感的高速缓存,比如本地缓存。
- 获取对象用
.get();如果已经被回收,get 返回null。 - 使用场景:图片缓存,内存够用就保留,内存不够自动清理缓存。
3、弱引用
java
WeakReference<User> ref = new WeakReference<>(new User(1,"张三"));
User u = ref.get();
WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集(GC)之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
- 只要发生 GC,不管内存是否充足,都会回收只被弱引用关联的对象。
.get()获取对象,回收后返回 null。- 典型场景:
WeakHashMap,key 为弱引用,key 对象没有其他引用就会被 GC 清理。 - 区别软引用:软引用等到内存不足才回收;弱引用只要 GC 就回收。
面试高频区分:
- 软引用:内存不足才回收
- 弱引用:GC 到来就回收
4、虚引用
java
ReferenceQueue queue = new ReferenceQueue();
PhantomReference<User> ref = new PhantomReference<>(new User(), queue);
**PhantomReference类实现虚引用。**无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。它主要用于执行一些清理操作或监视对象的回收状态。
- 不能通过 get () 拿到对象实例,get 永远返回 null。
- 唯一作用:对象被垃圾回收的时候收到系统通知,做回收前的资源清理。
- 必须配合
ReferenceQueue引用队列使用。对象回收后,虚引用会入队列。 - 典型场景:NIO 的直接内存
DirectByteBuffer,用来管理堆外直接内存释放。
| 引用类型 | 回收时机 | get () 获取对象 | 使用场景 |
|---|---|---|---|
| 强引用 | 永远不回收,直到引用置 null | 正常拿到对象 | 日常开发普通对象 |
| 软引用 | 内存不足 (OOM 前) 回收 | 回收后返回 null | 内存敏感缓存 |
| 弱引用 | 只要 GC 就回收 | 回收后返回 null | WeakHashMap |
| 虚引用 | 对象被回收时入队列 | 永远返回 null | 堆外内存回收监听 |