前言
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
JVM的使用场景在实际开发中是很少的,但是在面试中就会涉及到相关的问题,作为Java程序员,学习一些Java底层的知识,也是能说的过去的🐵
这篇博客会解析JVM的跨平台原理、内存分布、类加载、双亲委派模型、垃圾回收机制,整体来看,抽象的知识不是很多,一些地方大概了解下即可
🎆个人主页:洋不写bug的博客
🎆所属专栏:JavaEE学习
🎆铁汁们对于JavaEE的各种常用核心语法,都可以在上面的JavaEE专栏学习,专栏正在持续更新中🏀,有问题可以写在评论区或者私信我哦~
1,JVM简介
初学Java时,我们就了解到了Java的三个核心组件:
- JDK(Java Development Kit,Java 开发工具包),写/编译Java代码需要用到
- JVM(Java Virtual Machine,Java 虚拟机)
- JRE(Java Runtime Environment,Java 运行环境),JRE由JVM + Java 核心类库组成
这三者是包含的关系,如下图:

学习Linux时,Linux系统是很多bug的,总不能为了写Linux代码,就把计算机重装系统,重装为Linux系统,学习时就会安装一些虚拟机软件,例如VMWare、Virtual Box、Hyper-V、KVM
那就可以通过这些虚拟机软件,构造出一个虚拟的计算机,在虚拟的计算机上安装Linux系统
博主学习用的是VMWare,如下图,安装了两套Linux系统,一个国产的openEuler系统,一个是老牌企业版的CentOS7,这两个系统就可以同时开机,互不干扰

创建新的虚拟机时,就需要选择虚拟机的指定处理器信息和内存,如下图


VMware 虚拟出来的虚拟 CPU、虚拟内存、虚拟磁盘,并不是真的凭空创造出新硬件,只是软件做了一层 “包装、分配、隔离”给虚拟机分配 2G 内存,本质就是从你电脑真实物理内存里划走 2G,虚拟机运行期间,这块内存就被它占用了
Java虚拟机相比VMWare这种虚拟机,虚拟化程度就弱了很多,VMware是系统级虚拟机,可以在里面安装完整操作系统;JVM则是应用层上的虚拟机,是宿主计算机上的一个进程,它在这个进程内部,虚拟了一套独立的计算机模型
2,跨平台
JVM 是先把.java文件编译成.class文件,再通过JVM把.class转化为CPU可以执行的机器指令,这种做法是比较低效的;像C/C++/Go,编译器就是直接把源码编译成CPU可执行的机器指令,效率就更高
但是Java这种做有个好处,就是可以做到“跨平台”:
- 同一个程序,可以放到不同的操作系统上执行
- 同一个程序,可以放到不同架构的cpu上执行
跨平台对于C++来说,就是特别费劲的:
- 不同系统提供的api不同,兼容不同的系统,就得在关键逻辑处替换成不同的api
- 不同架构的cpu,支持的指令是不同的,就需要有不同的编译器,完成对应指令的生成
计算机/服务器的cpu架构是x64(基于x86),手机cpu架构就是ARM架构(高通、联发科、麒麟),苹果计算机M系列的cpu架构也是ARM架构(没有用英特尔的 CPU, 用的自己研发的 CPU)
Java官方就提供了多个JVM版本,例如Windows X86、Windows arm、Linux X86、Linux arm、macOS arm…,这些JVM对于上层是统一要求的,这样程序员只要写一套代码,就可以兼容各种环境
这个就类似于出国旅游,要不自己学习外语,这样能直接跟别人沟通,效率就会高一些;要不就找个翻译官,虽然跟别人沟通要经过翻译官的翻译,但就学习外语
JVM就是翻译官,虽然翻译的过程降低了程序运行的效率,但是提升了开发效率
Java程序的运行流程:
- 编写代码,就编写成了.java文本文件
- 通过javac编译器把.java编译成.class文件(在IDEA中,普通项目.class文件就在out目录中,Maven项目.class文件就在target目录中)
一个.java文件中可以写多个类(只能有一个类是public类型的),一个类是一个.class文件,因此一个.java文件中可以包含多个.class文件 - 通过java命令行工具,运行.class文件,java命令行工具对应到一个java进程,这个java进程就可以理解成是一个Java虚拟机,就是这个进程对.class文件进行解释执行
- 在IDEA中写代码时,每敲几个字符,IDE 会后台调用编译检查逻辑,实时扫描代码,也就是在后台频繁执行简化版 javac 检查,在运行代码前就显示出的异常就是受查异常(
IOException、FileNotFoundException),受查异常如果必须抛出或者catch,否则后续运行,整体编译成.class文件时,就会编译失败 - 运行时异常(空指、数组越界),这些是javac检查不出来的,javac把.java编译成.class文件,JVM在执行.class文件时就会抛出异常
3,JVM内存分布
JVM就对应着一个java进程,这个java进程会从操作系统中申请一大块内存空间,再把内存空间分成多个区域,分别赋予不同的功能
内存主要被分成四部分:
- 程序计数器(Program Counter,简称PC)
- 元数据区(jdk8之前,称为方法区)
- 堆
- 栈
.class文件中存储的内容就是Java字节码,程序计数器就是描述当前Java程序要运行的下个字节码指令的位置,程序计数器只需要保存一个空间地址,因此占用的内存空间是很小的
每个线程上都有一个程序计数器来记录,Java程序默认是顺序执行的,但是可能会遇到if、while、for、方法调用、抛出异常…,这些情况就需要修改PC的值,跳到别的字节码位置继续执行
操作系统中也有类似的设定,在CPU上有个寄存器,用这个寄存器表示“下一条指令执行的地址”,线程是并发执行的,一个线程可能会被调度走,后续再调度回来,这时候就需要保存“上下文”,PC(Program Counter,程序计数器)就是上下文中的一个关键信息
在Java代码中,一个核心的操作就是创建类,再基于类来创建对象
JVM 运行代码的过程中,随时要查询类信息,如果每次要用类的信息,都反复去读取.class 文件(存储在硬盘中),速度会极慢。
JVM 加载.class字节码文件时,会解析字节码里的类信息,直接把这个类的信息载入元数据区中,这样后续读取就是直接读取内存,速度会更快,保存的时候是通过“类对象”的方式把类的信息加载到元数据区
类对象是一个特殊的对象,这个对象中保存了类的基本信息:
- 类的名称
- 类的继承关系(父类是什么)
- 实现的接口
- 类中的属性(属性的名称、类型、public/private)
- 类中的方法(方法名称、参数列表、返回值、public/private)
…
反射就是通过类名.class的方法,获取到类对象,就是从元数据区中拿到的类的信息
JVM中的栈跟数据结构中的栈是不同的概念,JVM中的栈表示的不是数据组织规则,表示的是一片内存空间,这个内存空间中保存了方法的调用关系
如下图,a方法中调用b方法,b方法执行完成后,还需要返回上一层,从调用位置继续执行

JVM中的栈里面存放栈帧,方法调用时栈帧压入、方法返回栈帧弹出,栈帧的管理方式刚好是 LIFO(后进先出),也就是数据结构栈的规则
栈帧表示一次方法调用,栈帧主要信息:
- 方法的参数是什么
- 方法中的局部变量有哪些
- 方法执行结束后,返回值的结果
- 方法执行结束后,跳转回的地址
在IDEA中写个斐波那契计算函数,打上断点,来调试一下,按F9一步步走,左边部分就能看到栈帧,一个栈帧,对应一次方法调用,保存这个方法的局部变量、参数、返回地址


堆空间中就存放了new出来的对象,下面就写段代码,分析下这个代码中的每个部分都存储在JVM内存空间的哪个地方
public class Test {
private int n;
private static int m;
public void test1(){
}
public void test2(){
}
public static void main(String[] args) {
Test t = new Test();
}
}
1.n为实例(非静态)成员变量,就在堆中
2. m为静态成员变量,在元数据区中
3. 方法test1和方法test2的指令在元数据区中
4. t就属于是局部变量,在栈上
5. t只是这个对象的引用,new Test()是储存在堆上的,堆上存储了对象的各个信息,包括实例属性、加锁情况等
6. Test.class(类对象)就储存在元数据区中

一些铁汁在初学Java就会觉得是内置类型在堆上,引用类型在栈上,这个其实是不太对的;实际是局部变量在栈上、实例成员变量在堆上、静态成员变量在元数据区中
在JVM内存中,各个区域的内存大小分布:
- 堆是最大的,如果计算机的内存比较大,堆空间可以达到几十个G
- 栈空间比较小,取决于代码有多少个线程和每个线程调用方法的层次,如果代码中出现无限递归,就会抛出StackOverFlow(栈溢出异常)
- 元数据区的大小中等,取决于代码的规模(类、类的静态成员属性、类的方法的个数)
- 程序计数器的空间最小,在64位平台下,程序计数器的大小就是8个字节
4,类加载
类加载就是JVM从最开始读取.class文件,到最终构造完成“类对象”的过程,.class文件是储存在硬盘中的,类对象是储存在元数据区中的(内存),因此类加载就是一个把类从硬盘加载到内存中的过程
类加载的步骤有两个版本,一个版本是3个环节,一个版本是5个环节,本质上是一样的,3个环节的版本把第二个步骤拆分为三个小步骤,就变成了5个环节的版本,这里就按5个环节的版本来解析
1,加载
环节一就是加载,加载分为3小步:
- 根据代码中的“全限定类名”(包名 + 类名,例如java.lang.String、java.util.ArrayList),找到.class文件(使用的是”双亲委派模型“,属于面试中的一个高频考点,会在后面单独解析)
- 打开文件,读取文件内容到内存中
- 数据格式的解析
找到.class文件,读取文件内容到内存中很好理解,那这个数据格式的解析是什么意思呢,就要结合官方文档来理解
官方文档链接如下,每个版本都有对应的Java语言规范(语法的权威定义)和Java虚拟机规范(JVM的底层逻辑)
每6个月会进行一次版本更新,版本之间的差异不是很大(因为新的版本可能会有bug,所以实际开发中一般都不会用最新的版本,现在最常用的三个版本就是21、17、8)
官方文档链接

这里就看21版本,点一下Java虚拟机规范中的html,在网页中查看Java虚拟机规范

点开后就是一个目录,往下翻,这个第四部分都是class文件的格式,点进去看一下,4.1中就有个class文件的结构图

数据格式的解析就是JVM读取硬盘上的
.class二进制字节流,然后按照下图里这套ClassFile结构去解析,这个.class组织数据的结构铁汁们大概了解下即可,Java开发是不会接触到这个的
左边一列就表示数据类型,右边一列就是数据名称(表示的含义)
u2就表示无符号的2字节的整数,也就是unsigned short;u4表示无符号的4字节的整数,也就是unsigned int;像cp_info、filed_info就属于是复合结构体
属性含义如下:
- 大部分标准结构化的二进制文件都会带有magic(魔幻数字),magic的大小是四个字节,标识当前二进制文件的类型是什么(.exe、.class、.image、.zip)
- minor_version和major_version分别表示.class的版本号和Java内部的版本号
- constant_pool_count是常量池计数器,记录常量池中有多少数据;constant_pool是常量池
- access_flag记录这个类是什么类型的(public/private),可以叠加记录多个属性,记录是不是abstract,是不是interface
- this_class和super_class分别表示当前类的编号和父类的编号
- interfaces_count和interfaces分别表示实现的接口数和实现了哪些接口
- fields_count和fields分别表示类中的成员变量个数和类中都有哪些成员变量(实例成员变量和静态成员变量)
- methods_count和methods分别表示类中的方法个数和都有哪些方法
- attribute部分就是辅助的信息

.class文件是二进制文件,不是文本文件,如下图,记事本打开.class文件
在IDEA中打开.class文件,看起来是Java代码,这是因为IDEA中带有反编译器,能把二进制字节码还原成 Java 源码样子,还原出的跟实际的.java代码会有一些差异(例如增强 for 循环编译后会转成迭代器)

2,验证
验证步骤就是校验.class读出来的内容,是否是合法的,例如拿一个图片二进制文件,把后缀名改为.class,让JVM运行,那在验证这一步就能检测出文件格式是有问题的
如果发现有问题,JVM就会及时报错,通知程序员
3,准备
类加载最终要得到的结果,就是内存中的类对象,准备步骤就是给“类对象”分配内存空间
大多数情况下,在Java中申请的内存空间,空间上的每个字节,比特位都是0,这也称为零值初始化
而在C++中就不是这样,申请的内存是上次释放的时候,内存中残留的值
C++这种做法效率会更高一些,但是这样就容易出现bug
4,解析
常量是一个字符串,然后建立这个字符串中和内存的地址之间的映射关系,后续访问到这个常量的时候直接从内存地址中取出相应的值
那也就是建立这个字符串跟内存地址的映射关系
5,初始化
初始化就进入到用户写代码的环节了,加载代码中的静态代码块、静态成员变量,以及加载父类和要实现的接口
加载父类的时候,需要判断父类是否已经加载过了,如果父类已经加载过(一个类加载一次即可),就需要再加载了
类加载步骤总结如下:
- 加载:找到.class文件,打开并且读取到内存中,解析.class中的二进制结构文件
- 验证:对读到的内容进行校验
- 准备:给类对象分配内存空间(全0的空间)
- 解析:针对常量进行加载
- 初始化:初始化类对象、静态成员变量,执行静态代码块
JVM并不是一启动就加载所有的.class,而是第一次用到某个类时,才触发加载,也就是”懒汉模式“
”用到某个类“也就是下面这几种情况:
- 构造某个类的实例
- 调用类的静态方法/使用类的静态成员
- 使用子类的时候,就会触发父类的加载
5,双亲委派模型
类加载的第一步就是根据全限定类名(包名 + 类名)找到.class文件,就用到了”双亲委派模型“
JVM在进行类加载的时候,需要依赖内部的模块——”类加载器“(class loader),JVM自带了三种类加载器:
- BootStrap ClassLoader:负责在Java的标准库(Java自带的,像Random、Scanner、Thread这些就是标准库中的类)中进行查找
- Extension ClassLoader:负责在Java的扩展库(JDK 附带的可选附加工具,不像标准库那样是运行必需的)中进行查找
- Application ClassLoader:负责在Java的第三方库(其他的公司/个人写的,JDBC编程中下载的驱动就是Oracle厂商开发的第三方库)/当前项目中进行查找
这三个类加载器之间,就存在父子的关系(并不是父类子类的那种继承关系),每个类加载器都有一个parent属性,来保存自己的父亲是谁
加载流程如下:
- 类加载器会从Application ClassLoader作为入口,开始调用,但是Application ClassLoader不会执行查找,把这个任务抛给其父亲Extension ClassLoader;Extension ClassLoader收到任务后也不会执行查找,而是把任务抛给其父亲BootStrap ClassLoader来进行查找
- 任务抛到BootStrap ClassLoader已经是最后一层了(BootStrap的parent属性的值为null), BootStrap ClassLoader就根据全限定类名在标准库目录中扫描,看有没有对的上的
- BootStrap ClassLoader如果在标准库中查到了,那查询过程就到此为止,如果没查到,再把任务抛给其儿子Extension ClassLoader;Extension ClassLoader如果如果也没查到,就把任务抛给其儿子Application ClassLoader,让其扫描第三方库和当前项目目录
- 如果Application ClassLoader还是没找到,就说明写了个不存在的类名,就抛出ClassNotFoundException

这个双亲委派模型是翻译的问题,准确来说应该是父亲委派模型,也就是先委托父亲先尝试加载,父亲加载失败,自己才加载
对于一个类名,最先在标准库中匹配,再在扩展库中匹配,最后在第三方库和项目目录中匹配
如果我们自己写个java包,java包里面放个lang包,lang包里面再写个String类,那全限定类名就是java.lang.String,但是创建String对象时还是用的是标准库中的String类,这是因为先扫描的是标准库
如果自己实现库/框架时,也可以自己实现类加载器,不遵守双亲委派模型(但是一般Java开发很少会涉及到这样做)
6,垃圾回收机制(GC)
1,简介
垃圾回收机制(Garbage Collection)简称GC,Java中就具有垃圾回收机制,而在C/C++中是没有的,一些内存在申请后,是需要手动进行释放的
例如在C语言中动态申请malloc,用完后就需要通过free进行释放;在C++中动态申请new,用完后要通过delete来释放,这样的释放操作是容易遗忘的,就算不遗忘,free代码前面的代码可能return,可能抛出异常,这些都会影响释放内存
因此,内存泄漏问题也是C++程序员的一大“职业杀手”,如果代码中存在内存泄露,可能程序经过测试没问题,上线用了一段时间还是没问题,可能突然某一天(这个受泄露速度和内存大小的影响)突然出现内存泄露问题,程序就崩溃了
很多主流的编程语言都引入了垃圾回收机制,例如Java、Python、Go、PHP、JS等,引入垃圾回收机制后,就会有专门的逻辑来帮助进行自动释放,提升了程序员的开发效率(但是垃圾回收就引入了额外的开销,会降低程序的运行效率)
C++之所以不引入垃圾回收机制,C++ 是一门是能够把性能压到硬件极限的语言,对性能要求很高的地方就会使用C++(游戏引擎,系统操作内核)
JVM的内存空间划分为四个区域:程序计数区、元数据区、栈、堆
程序计数器和栈内存不需要回收,线程结束后,就销毁了;元数据区也不会进行回收,一般只要类加载进去,就不会释放,因为后面可能还会用到这个类;堆上存储new出来的对象,JVM垃圾回收的主要区域就是堆
GC释放内存时并不是以字节为单位,而是以对象为单位
如下图,这个分为三种情况,有的对象的内存正在使用中,就不能回收;还有的对象内存不再使用了,就进行回收;有的对象一半内存使用,一半内存不使用,也不能回收

用对象作为单位,只要对象有内存被使用,就不回收这个对象,如果是按照字节维度,对每个对象都得描述出哪部分要回收,哪部分不要回收,是很麻烦的
那JVM具体是如何区分出哪些对象是垃圾,需要释放内存呢?
在Java中,使用对象,都是通过引用来进行的,无论是使用对象的属性还是方法,都要通过引用.的形式来访问,如果一个对象没有任何的引用指向它了,那就注定这个对象就无法再被使用了,就需要释放这个对象的内存
就用代码来举个例子,下面有个Dog类,创建两个Dog对象,这两个dog对象的引用分别是dog和dog2,这两个引用是在栈空间的,对象的具体信息储存在堆空间中,引用中存储的数据就是对象在堆中存储的地址
如果让dog和dog2指向null,也就没有引用指向堆空间中的内存了,对象也就无法被访问到了
public class Dog {
public String name;
public String category;
public int age;
public void bark(){
System.out.println("汪汪汪");
}
public void eat(String food){
System.out.println(name + "正在吃 +" + food);
}
public void sleep(){
System.out.println("正在睡觉");
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "大黄";
System.out.println(dog.name);
dog.bark();
dog.eat("狗粮");
Dog dog2 = new Dog();
dog2.name = "毛毛";
dog2.eat("肉");
}
}
}

总结下,JVM就是判断是否有引用指向对象,来判断这个对象是不是垃圾,是否需要回收,就算引用变量存在,但是后续代码不再读取这个变量,JVM 可以认为引用失效
判断是否有引用指向某个对象,有两种方案:
- 引用计数(Python、PHP使用的方案)
- 可达性分析(Java使用的方案)
2,引用计数
引用计数是一种简单粗暴的方案,就是给每个对象分配一个计数器,计数器跟对象一起,储存在堆空间上,有引用指向这个对象了,计数器的值就加1(创建对象时就会有一个引用指向计数器,可以理解为计数器的初始值就是1)
计数器记录的就是指向对象的引用的个数,当计数器的值为0时,此时对象就没有任何的引用指向了,对象就是垃圾了

引用计数有两个弊端:
- 消耗的额外内存空间较大,Python64 位平台的计数器大小是8个字节,如果对象比较小(属性、方法少),那这个额外空间占用的比例就是比较大的
- 循环引用问题,类似于死锁
如下图,Test对象中的属性t,还可以存一个Test引用
现在创建两个Test对象,分别用a引用和b引用指向它们,每个对象中还存储了另一个对象的引用(通过属性t)
这样每个对象就有两个引用指向,计数器里的值就是2

后续a和b引用值变为null,两个对象计数器的值就会变成1,并不会被销毁,但是铁汁们就会发现一个问题:外部永远没有办法访问去这两个对象了,但是每个对象中还保存着另一个对象的引用,导致另一个对象空间不会被释放

也就是这两个对象应该都是垃圾,空间应该被释放掉,因为没办法通过任何的引用来访问到它们,但是它们互相证明对方不是垃圾(存储对方的引用)
就类似于玩狼人杀,如果不是狼狼互踩的话,狼人A在发言中认为狼人B应该是好人,狼人B在发言中认为狼人A是好人,它们两个互相证明对方是好人,其实两个都是狼人😂
3,可达性分析
JVM使用的方案就是可达性分析,如果对象通过一系列引用操作访问到,那这个对象就是可达的
如下图,就拿二叉树来分析,只要拿到root(a结点),那么二叉树中的每个结点就都是“可达的”,例如b结点可以通过root.left拿到,g结点可以通过root.left.right.left来拿到

如果让b的右子树引用改为null,也就是让root.left.right = null,那也就没有引用指向e结点,e结点的内存空间就释放了,e结点释放后,也就没有引用指向g结点了,g结点的内存空间就也释放掉了

JVM会安排专门的线程,对引用进行扫描,扫描下面三个部分中的引用变量,以这些引用变量为起点,尽可能的往里面访问,但凡被访问到的对象,都会被标记为“可达对象”,剩下的就是垃圾了,需要释放内存
- 栈上的局部变量(引用类型)
- 常量池里指向的对象(final修饰的,引用类型)
- 元数据区(静态成员,引用类型)
可达性分析完成后,找出“不可达”的对象,如果直接释放这些对象的空间的话,还会涉及到一个内存碎片问题
一个对象在堆上存储,占用的是一块连续的内存,如果直接释放这些对象的空间的话,那在内存中就会出现很多不连续的空间
如下图,对ACEG对象释放内存,这部分内存的空间就是断断续续的,这样就会出现一个问题:一片内存区域空闲空间大小是4MB,但尝试申请2MB的空闲空间时,就会失败,因为可能空闲空间是断断续续的8个0.5MB

解决内存碎片问题,有多个方案,第一个方案就是复制算法
把内存空间平均分成两部分,申请空间时在一边申请,另一边空间不存储东西,当需要释放对象空间时,把不是垃圾的对象复制到另一边,再把这部分空间整个都释放掉
如下图,释放2、4、6、8对象后,把1、3、5、7对象移动到另一侧,这样就不会出现内存碎片问题了

复制算法有两个弊端:
- 每次只使用了内存的一半空间,内存浪费比较多
- 如果存活的对象比较多,对象比较大,那复制的开销也是不小的
还可以使用标记-整理算法,就类似于顺序表删除元素时,后面的元素往前搬运,这里对象的内存空间释放时,后面的对象就往前填充,如下图:

这个方案可以解决内存碎片化问题,也不会像复制算法那样浪费一半的空间,但是搬运对象的成本仍然是比较高的
JVM使用的是“分代回收”方案,也就是把复制和标记-整理结合起来,进行优化,扬长避短
JVM会不停的使用扫描对象是不是垃圾,每个对象都有个年龄属性,年龄表示的就是一个线程经过垃圾回收扫描线程的轮次,如果一个对象扫描一次,不是垃圾,那这样对象的年龄就 + 1
在堆空间中,分为了“新生代”和“年老代”,分别存放年轻对象和年老对象,年龄超过15时,就是年老对象了,年老对象是不容易成为垃圾的,年轻对象是更容易成为垃圾的,可以理解为“要死早就死了”,既然在十几轮线程扫描中都不是垃圾,就说明这个对象需要用到,那突然变成垃圾的概率就比较小
如下图,内存空间中分为老年代和新生代,新生代中有一个伊甸区和两个幸存区,“分代回收”流程如下:
- 刚创建出的对象,就放到伊甸区(伊甸在基督教中是生命诞生的地方)中,如果对象活过一轮GC,就进入到幸存区中
- 两个幸存区同一时刻只使用一个,每次经过一轮GC,都会淘汰掉伊甸区和幸存区中的一大部分对象,就会把幸存区和伊甸区中存活的对象移动到另一个幸存区中(类似于复制算法)
- 如果对象能够在新生代中存活多轮,那就会进入老年代,老年代的对象因为生命周期大概率很长,所以就没有必要去频繁扫描
- 如果一个对象比较大,在新生代中复制开销就比较大,就直接进入老年代,老年代释放空间采用的就是标记-整理算法

绝大多数对象都是临时对象,只在一次方法调用内使用,方法执行完毕对象就不再被引用,所以生命周期很短,这就是JVM分代回收的理论基础
使用可达性分析会有个STW(Stop The World)时间,JVM 在执行GC 操作时,暂停所有用户应用线程,只保留 GC 后台线程运行;等 GC 这部分工作完成后,再恢复用户线程,这个暂停的过程就是STW
如果进行GC时不暂停所有用户进程,那就会一边 GC 标记对象,一边业务代码修改引用:新增引用、删掉引用、移动对象,对象的情况又在改变,这样就会出现标记出错的情况
最新的JDK使用的GC算法已经可以优化到将STW时间控制在1ms之内,这个对程序的影响还是比较小的
结语💕💕
- 这篇博客是JavaEE初阶专栏的最后一篇博客,JavaEE学习之旅这个专栏,到这里就已经完结了,后续还会更新JavaEE进阶专栏,对Java知识进行更加深入的解析
- 这个专栏里面解析了线程、进程、文件IO、网络通信、网络编程、网络原理、HTTP相关知识,如果铁汁们发现内容错误,或者有一些知识点没写清楚,都可以私信博主,博主看到后一定回复🐵感谢大家对博主的支持,希望这些博客能够真正的对大家有用💪💪💪
以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

本文系统阐述JVM跨平台原理(字节码+虚拟机翻译机制)、内存区域划分(程序计数器、元数据区、栈、堆)、类加载五阶段流程(加载、验证、准备、解析、初始化)、双亲委派模型(Bootstrap/Extension/Application类加载器协作机制)及垃圾回收机制(可达性分析、分代回收、STW、新生代/老年代管理)。重点聚焦JVM底层运行逻辑与内存管理关键技术。
🎆个人主页:
368

被折叠的 条评论
为什么被折叠?



