这篇文章特别适合那些刚刚接触 Java 虚拟机(JVM)的新手,以及希望快速回顾 JVM 核心知识的同学。本文基于我个人的学习经验整理而成,旨在帮助大家更好地理解和记忆 JVM 的重要概念。跟着我的步骤来,相信大家的脑子里一定会对JVM相关的知识体系有个清晰的印象。
记忆流程:
当你点击运行按钮,我们编写的Java文件,会经过javac
编译成.class
文件(字节码文件)。字节码文件会被Classloader(类加载器) 加载到 运行时数据区(内存中)。
思考:
操作系统能直接识别字节码文件吗?
不能!需要JVM的执行引擎担任解释器,将字节码解释成机器码。 因为字节码指令集属于JVM自己的一套指令规范,操作系统不能直接识别,所以需要这么一个解释器。
解释过程中会调用第三方语言(如C++)提供的本地方法接口(如驱动、地图制作等)。
上面的描述中,我们已经记住了JVM的几个核心组成
JVM核心组成:
- 类加载器 (负责把类文件加载到内存中)
- 运行时数据区 (也是我们常说的内存)
- 执行引擎 (解释器)
- 本地方法接口
然后就从JVM的核心组成出发,顺序去深入了解JVM即可
类加载机制
1. 类加载流程:
- 加载
查找并加载类文件到JVM中。 - 链接
分三阶段:- 验证:检查字节码是否符合JVM规范。
- 准备:为静态变量分配内存,赋初始值(如
int
为0)。 - 解析:将常量池的符号引用转为直接引用(这里的符号引用我们举个例子,myclass类中,我写了个System.out.println(“HELLOW”),这里的System.out.println就像一个符号引用,大致意思就是 “隔壁老王要打印hellow”,解析之后,就是隔壁老王住在哪儿,我去找他让他打印hellow。这里举例子大家应该懂了,符号引用就像一个签名,我根据签名能找到对应的对象来帮我做事)
- 初始化
执行静态代码块和静态变量赋值。
2. 双亲委派机制
双亲委派机制是JVM推荐的一种类加载机制,非强制性的哦~
- 核心逻辑:加载时先委托父类加载器加载,父类无法加载时自己处理。
- 优点:防止核心类被篡改。
- 加载顺序:自下而上(子→父),父类加载器不记录子类,向上委托效率更高。
3. 类加载器分类
- 启动类加载器:加载
lib
下的核心类。 - 扩展类加载器:加载
lib/ext
的扩展类。 - 应用类加载器:加载用户编写的
classpath
下的类。
运行时数据区(内存)
这块儿建议大家结合自己写的java代码进行理解记忆,就很ez了。
1. 方法区
- 存储内容:类信息、常量、静态变量(线程共享)。
- 演进:
- JVM8前:由永久代实现(基于堆内存,可能OOM)。
- JVM8后:由元空间实现(基于本地内存,降低OOM风险)。
这里我们多提一个即时编译器的概念,还记得执行引擎么,每次都会把我们的字节码指令转换为机器码,每次都要一行行读,我们是否可以把经常被执行的代码给“缓存”起来,便于下次直接使用,就不用解释了?
即时编译器(JIT):
- 作用:缓存高频执行的字节码解释结果(直接复用机器码,提升效率)。
- 存储位置:编译后的代码存入
Code Cache
(特殊内存空间,不属于方法区或堆)。 其实就是个特殊的内存空间,不属于任何地方,因为他“特殊”。
2. 虚拟机栈
- 特性:线程私有,每个方法调用创建一个栈帧。
- 栈帧内容:局部变量表、操作数栈、动态链接、方法出口。
- 风险:大部分的stackoverflow都是因为自己的写法有问题导致无限递归,如果是因为内存不足,可以配置对应的栈内存。
3. 堆
-
特性:线程共享,存储所有对象和数组(GC主战场)。
-
垃圾回收算法:
- 标记清除法
- 流程:标记存活对象→清除未标记对象。
- 缺点:内存碎片。
- 标记整理法
- 流程:标记存活对象→清除未标记对象→整理内存。
- 缺点:效率低。
- 复制算法
- 流程:将存活对象复制到另一块内存→清空原内存。
- 缺点:内存利用率50%(需预留一半空间)。
- 标记清除法
-
堆内存划分:
- 新生代
- Eden区(80%):新对象创建区。
- Survivor区(S0/S1各10%):采用复制算法(内存利用率90%)。
这里解释下,新生代是新生JAVA对象玩的地方,复制算法的核心是空间分成两半,但是那样空间的利用率只有百分之50,因为还有一半要用来放可用对象,可如果我每次都使用Eden区和S区的任意一个比如S0,做垃圾回收的时候,把可用对象放到S1区,清除伊甸和S0,然后在用伊甸和S1,做垃圾回收的时候把可用对象再放到S0,以此往复,利用率就有百分之90
这里会产生很多有趣的问题,比如有同学会问,假如可用的大对象在S1放不下咋办? 放到老年代做兜底
可能又有同学会问,假如老年代也放不下咋办? 那就根据当前的垃圾回收器,做可以对老年代的垃圾回收,然后再去放,比如CMS,做FullGC,对新老年代做垃圾回收,然后在放大对象。
可能又又有同学会问,假如还是放不下咋办,还能咋办? OOM了呗!
- 老年代
- 长期存活对象存放区(大对象直接进入老年代)。
- 新生代
现成的著名的垃圾回收器:
- CMS:低停顿,标记清除。
- G1:分代+分区,平衡延迟与吞吐。
- ZGC:超低停顿(TB级堆)。
4. 本地方法栈
- 功能:为
JNI
调用的本地方法分配内存(类似虚拟机栈,线程私有)。
5. 程序计数器
- 特性:线程私有,唯一无OOM区域。
- 作用:记录当前线程执行的指令位置(便于CPU切换后恢复)。
原文补充细节
- 类加载解析阶段:符号引用包含类和方法的信息签名,解析后转为直接引用(内存地址)。
- 堆内存分配:
- 伊甸区与Survivor区比例默认
8:1:1
。 - Survivor区放不下的对象直接进入老年代。
- 伊甸区与Survivor区比例默认
- 垃圾回收触发:
- 老年代空间不足时触发Full GC。
- JVM设计权衡:不同垃圾回收器适用于不同场景(如CMS注重低延迟,G1平衡吞吐和延迟)。
- OOM场景:
- 方法区(元空间):加载过多类。
- 堆:创建大对象或内存泄漏。
- 虚拟机栈:无限递归。
过知识点的总体流程:
从我写java代码开始,到被javac编译成字节码文件,会被类加载器加载到内存中
这里我们可以深究JVM的组成 1 : 类加载器,我们可能会联想 类加载器的加载过程,类加载器常见的有哪些?双亲委派机制等等?
还能深究JVM的组成2,内存,也是运行时数据区,我们可能又会联想到运行时数据区分那几部分? 每部分的主要存储啥?方法区不同JVM的实现? 堆?垃圾回收? 常见的垃圾回收算法? 年轻代里面为啥伊甸区:S0:S1常见比例为8:1:1? 哪几种情况对象会进入到老年代? 常见的垃圾回收器和特点等等? 我上面问的这些其实都是按流程顺序思考联想的问题。内存这块儿是内容最多的,所以产生的问题碰撞也是最多的,大家后续可以深究下。
字节码文件不能被操作系统直接运行,需要执行引擎解释一下成机器码,然后cpu去执行。这个期间会有其他第三方语言如C++提供的本地方法接口做辅助(驱动、地图制作等)。
其实这样在过JVM主要的组成部分的时候,其实就能把JVM大部分的内容都能过一遍了。按照这套流程走下来,基本上能够涵盖 JVM 的大部分核心知识点。在走这套无比丝滑,无比德芙的记忆流程的时候,你会发现所有关键的知识点都自然浮现出来。然后针对性去深究即可吊打面试官!
分享我的 JVM 复习流程,希望能够帮助到大家。如果觉得这篇文章对你有帮助,请不要吝惜你的点赞和关注,后续我还会继续分享更多类似的复习总结和实用技巧哦!