目录
[1. 区分JDK,JRE 和 JVM](#1. 区分JDK,JRE 和 JVM)
[1.1 JVM](#1.1 JVM)
[1.2 JRE](#1.2 JRE)
[1.3 JDK](#1.3 JDK)
[1.4 关系总结](#1.4 关系总结)
[2. 跨平台性](#2. 跨平台性)
[3. JVM中的内存划分](#3. JVM中的内存划分)
[4. JVM的类加载机制](#4. JVM的类加载机制)
[5. 双亲委派模型](#5. 双亲委派模型)
[6. 垃圾回收机制(GC)](#6. 垃圾回收机制(GC))
[6.1 识别垃圾](#6.1 识别垃圾)
[6.1.1 单个引用](#6.1.1 单个引用)
[6.1.2 多个引用](#6.1.2 多个引用)
[6.2 释放垃圾](#6.2 释放垃圾)
[6.2.1 标记---释放](#6.2.1 标记—释放)
[6.2.2 复制算法](#6.2.2 复制算法)
[6.2.3 标记---整理](#6.2.3 标记—整理)
[6.2.4 分代回收](#6.2.4 分代回收)
1. 区分JDK,JRE 和 JVM
1.1 JVM
JVM是 Java 程序运行的核心,负责执行 Java 字节码(.class文件),使其能在不同操作系统上运行。
特点:
- 可以将.class文件编译为机器码执行(跨平台能力)
- 自动垃圾回收机制(GC),管理堆、栈、方法区等内存区域。
- 不包含开发工具或核心类库,仅负责运行编译后的程序。
1.2 JRE
JRE 是 Java 程序运行的最小环境,包含 JVM 和运行所需的核心类库(如 java.lang、java.util、java.io)。
特点:
- 仅支持运行已编译的 Java 程序(.jar 或 .class 文件)。
- 不包含编译器(javac)或开发工具,无法用于开发。
1.3 JDK
JDK是Java开发的完整工具包,包含JRE和一些开发工具
特点:
- 支持编写、编译、调试和运行 Java 程序(.java → .class → 执行)。
- 提供开发工具如 javac(编译器,可以将 .java文件编译为 .class 文件),javadoc(文档生成),jbd(调试器)等。
1.4 关系总结
- JDK = JRE + 开发工具
- JRE = JVM + 核心类库
- JVM 是执行引擎,依赖 JRE 提供的类库运行程序
- JRE是Java程序运行的最小环境配置
2. 跨平台性
JVM类似一个翻译官,是实现跨平台性的关键
java文件先通过 javac 编译为 .class 文件,JVM又会将 .class文件转换为cpu可以识别的机器指令
因此,发布一个java程序,本质上发布 .class文件即可,JVM拿到 .class文件,就会自动进行转换
- windows上的JVM,就会把 .class文件转换为 windows 操作系统可以识别的机器指令
- Linux 上的JVM,就会把 .class文件转换为 Linux 操作系统可以识别的机器指令
- 不同操作系统上面的JVM是不同的
3. JVM中的内存划分
JVM在运行的过程中,相当于一个进程,进程在运行的过程中,需要从操作系统中申请一块空间(内存资源,CPU资源等),这些内存空间,支撑了后续java程序的执行,比如定义变量需要内存空间,这里获取的内存空间,并不是直接给操作系统要,而是JVM给的
JVM从操作系统中申请一大块内存空间,给java程序使用,这一大块内存空间又会根据实际的使用用途划分出不同的空间出来,这样的好处就是便于管理和提供调用效率

JVM将申请到的空间,进行区域划分,不同的区域具有不同的作用
堆
- 代码中new出来的对象,都存放在堆中
- 堆也是垃圾回收的主要区域
虚拟机栈
- 虚拟机栈的生命周期和线程相同,线程启动时创建,线程结束时销毁
- 负责管理Java方法的调用和执行
- 由栈帧组成,每个方法调用对应一个栈帧
- 栈帧中存储局部变量表、操作数 、动态链接、方法返回地址等信息。
栈帧
- 局部变量表:存放方法参数和局部变量。
- 操作数栈:执行方法时的工作区(先进后出)
- 动态链接:指向运⾏时常量池的方法引用(在方法中调用另一个方法)
- 方法返回地址:PC寄存器的地址
本地方法栈
和虚拟机栈的功能类似,只不过java虚拟机站是给JVM使用,本地方法栈是给本地方法使用。(常见的本地接口JNI调用非java代码)
程序计数器
只会占用较小的空间,用于存储下一条要执行的java指令的地址
元数据区
元数据一般指的是一些辅助性质的数据
元数据区:存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。
一个程序有哪些类,每个类中有哪些方法,每个方法包含哪些指令都会被记录在元数据区中
区分一个变量在那个内存区域中,主要根据变量的类型
- 成员变量------存储在堆中
- 静态成员变量------存储在元数据区中
- 局部变量------存储在虚拟机栈中
- 方法------存储在元数据区
- 方法在运行时------存储在虚拟机栈中
4. JVM的类加载机制

类加载:java进程在运行的时候,需要把 .class 文件从硬盘,读取到内存中,并进行一系列的校验解析,最终转换成可执行代码的过程的过程
类加载的过程大体分为5个步骤,加载、验证、准备、解析、初始化五个阶段
1)加载
- 将硬盘上的 .class文件找到并打开,读取文件中的内容(二进制字节流)
- 将字节流表示的静态数据结构转为方法区中的运行数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中访问这个类中各种数据的入⼝。
2)验证
保证读取到的数据是合法的,符合虚拟机的约束要求,确保这些数据被运行后不回危害虚拟机自身的安全
3)准备
给读取到的类对象(static变量),分配内存空间,这时候内存空间中,默认值都为0或null
4)解析
主要针对类中的字符串常量进行处理,主要是将符号引用转换为直接引用
- 在硬盘存储中,并没有地址这个概念,虽然没有地址这个概念,但是存在类似地址"偏移量"的概念,也可以表示数据在文件中的具体位置
- 当数据被加载到内存中,此时就会分配地址,这时候会从符号引用变为直接引用
5)初始化
Java虚拟机真正执行类中编写的Java程序代码,完成后续的类对象初始化(触发父类的加载,初始化静态成员,执行静态代码块等)
5. 双亲委派模型
双亲委派模型是Java类加载机制中的一种重要设计原则,它定义了类加载器如何协作加载类的规则。
在Java中,存在一个模块专门执行类加载操作,称为"类加载器",主要负责将.class文件加载到内存中,并转换为可执行的java.lang.Class类的实例
JVM中的类加载器默认存在三个,也可自定义一个
-
启动类加载器(Bootstrap ClassLoader):负责查找标准库的目录
-
扩展类加载器(Extension ClassLoader): 负责查找扩展库的目录
-
应用程序类加载器(Application/System ClassLoader):负责加载用户编写的程序代码和查找第三方库的目录
这三个类加载器,存在类似二叉树的父子关系(不是父子继承关系)

双亲委派模型的工作流程:
- 从应用程序类加载器作为入口,开始工作,先加载用户编写的程序代码,但是不会立即搜索自己负责的目录,会把搜索任务交给父亲(扩展类加载器)
- 代码进入扩展类加载器的范畴,但是也不会立即开始搜索任务,而是将自己的搜索任务交给父亲(启动类加载器)
- 代码进入启动类加载器的范畴,但是也不会立即开始搜索工作,而是将自己的搜索任务交给父亲
- 但是启动类加载器不存在父亲,那么就会真正的开始搜索工作,尝试在标准库中查找符合的.class文件,如果找到了,就进入后续的验证,解析等流程中,如何没有找到,就会回到孩子(扩展类加载器)中继续尝试加载
- 扩展类加载器收到父类返回的任务后,开始在扩展类中查找,如果找到了,就进入后续的验证,解析等流程中,如何没有找到,就会回到孩子(应用程序类加载器)中继续尝试加载
- 应用程序类加载器收到父类返回的任务,开始在第三方库中查找,如果找到了进入后续的流程中,如果没有找到,会继续到孩子(自定义类加载器)中继续进行加载,但是默认不存在孩子(自定义类加载器),这时候类加载过程就会失败,抛出ClassNotFoundException异常
双亲委派模型的优点:
- 避免重复加载类,这样严格规定查找的范围,避免重复加载类
- 安全性,使⽤双亲委派模型也可以保证了Java的核心API不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现⼀些问题(如果多个类加载器独立加载同一个类,会出现类冲突)
上述的规则是JVM中默认类加载器,遵守的规则,这样的规则,其实也可以被打破
如果我们使用自定义类加载器,指定某个目录进行查找并加载,自定义类加载器不和系统再带的类加载器进行关联(不调用默认类加载器的引用),那么自定义加载器就是独立的,不会涉及双亲委派算法
6. 垃圾回收机制(GC)
定义一个变量会使用一块内存空间,如果这个变量长时间用不到又不及时释放掉,那么内存空间会为占据,导致后面想申请内存申请不到,还有可能导致内存泄漏问题,所以垃圾回收本质上是回收内存空间,更准确的是回收对象
在JVM中存在多个区域,有些区域不需要垃圾回收,对于程序计数器、虚拟机栈、本地方法栈这三部分区域, 其⽣命周期与相关线程有关,线程销毁,会自动销毁,这里垃圾回收的主要区域是方法区和堆
6.1 识别垃圾
判断哪些对象后续还需要继续使用,那些不需要继续使用
6.1.1 单个引用
对象的使用一定伴随着引用,如果一个对象没有任何引用指向他,那么就可以视为垃圾。
匿名对象被使用完后应该自动销毁,也应该被视为垃圾。
6.1.2 多个引用
如果存在多个引用指向同一个对象,必须确保每一个对象的引用都被销毁了,才能将这个对象视为垃圾。
1)引用计数器
原理:
给每个对象再分配一个额外的空间,空间里面存储这个对象存在几个引用,可以设置一个专门的扫描线程,去获取每个对象的引用计数情况,如果发现对象的引用为0,表示这个对象可以被释放。
问题:
- 每个对象分配额外的空间,即使每个对象安排的计数器占很小的内存,如果程序中的对象数目很多,总消耗的空间也会很多。
- 如果出现循环引用问题,会导致引用计数器无法工作。
2)可达性分析
可达性分析会消耗更多的时间,但是不会产生循环引用这样的问题。

如果出现:6.right = null ,则会导致7不可达,7不可达也会导致8不可达。
原理:
- 从一些特别的变量作为起点进行遍历,沿着这些变量所持有的引用。逐层往下访问,能被访问到的对象就不是垃圾,如果访问不到就是垃圾。
- JVM中存在扫描线程会不断的对代码中已有的变量进行遍历,尽可能地在一定时间内访问到更多的对象。
特殊的变量:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
6.2 释放垃圾
6.2.1 标记---释放
在遍历的过程中检测到标志为垃圾的对象,直接释放掉,但是一般不采用这种方法,会出现内存空间碎片化问题。
6.2.2 复制算法

将标志为垃圾的对象不直接释放掉,而是将不是垃圾的对象复制到内存的另一半里,然后将垃圾的那一半空间整体释放掉。
特点:
- 可以解决内存碎片化问题。
- 但是可用的内存空间变少了。
- 如果复制的对象比较多,会导致复制的开销会很大。
6.2.3 标记---整理
类似于顺序表。删除掉标志为垃圾的对象,将正在使用的对象进行搬运,集中放在一端。
特点:
- 可以解决内存碎片化问题。
- 不会浪费太多的内存空间。
- 如果存活的对象很多,搬运的开销会很大。
6.2.4 分代回收
根据不同种类的对象,采用不同的方式

- JVM中引入了对象年龄的概念并存在专门的线程负责周期性的扫描
- 如果一个对象被扫描的一次并可达,那么年龄就加1(初始年龄都为0)
- JVM会根据年龄的差异将整个堆内存分为两个部分:新生代和老年代
1)当代码中new出一个新的对象,这个对象会先存储在伊甸区,创建出的新对象大部分都活不过第一轮GC,生命周期非常短。
- 第一轮GC扫描完成后,伊甸区中少数的对象会通过复制算法拷贝到生活区中,后续gc的扫描线程会持续进行扫描,生存区中的大部分对象也会在扫描中被标记为垃圾,少数存活的会继续使用复制算法拷贝到另一个生活区中,只要这个对象能在生活区中继续存活,就会被复制算法来回拷贝到另一半的生活区中,每经历一次GC扫描,对象的年龄都会加一
3)如果这个对象在生活区中经历了很多次GC仍然存活,JVM就会认为这个对象的生命周期会很长,然后通过复制算法从生活区拷贝到老年代。
4)老年代中的对象也会被GC扫描。但是扫描的频率会大大的降低。
5)老年代中的对象,如果被GC标记为垃圾,就会按照整理的方式释放内存。