目录
1. 区分JDK,JRE 和 JVM
1.1 JVM
1.2 JRE
1.3 JDK
1.4 关系总结
2. 跨平台性
3. JVM中的内存划分
4. JVM的类加载机制
5. 双亲委派模型
6. 垃圾回收机制(GC)
6.1 识别垃圾
6.1.1 单个引用
6.1.2 多个引用
6.2 释放垃圾
6.2.1 标记—释放
6.2.2 复制算法
6.2.3 标记—整理
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,生命周期非常短。
2) 第一轮GC扫描完成后,伊甸区中少数的对象会通过复制算法拷贝到生活区中,后续gc的扫描线程会持续进行扫描,生存区中的大部分对象也会在扫描中被标记为垃圾,少数存活的会继续使用复制算法拷贝到另一个生活区中,只要这个对象能在生活区中继续存活,就会被复制算法来回拷贝到另一半的生活区中,每经历一次GC扫描,对象的年龄都会加一
3)如果这个对象在生活区中经历了很多次GC仍然存活,JVM就会认为这个对象的生命周期会很长,然后通过复制算法从生活区拷贝到老年代。
4)老年代中的对象也会被GC扫描。但是扫描的频率会大大的降低。
5)老年代中的对象,如果被GC标记为垃圾,就会按照整理的方式释放内存。