JVM 一文详解
目录
JVM 简介
JVM 中的内存区域划分
1. 堆(一个进程只有一份 ------ 线程共享)
2. 栈(一个进程可以有 N 份 ------ 线程私有)
Java 虚拟机栈:
本机方法栈:
3. 程序计数器(一个线程可以有 N 份 -------- 线程私有)
4. 元数据区(一个线程只有一份 -------- 线程共享)
一道经典的笔试题:
内存布局中的异常问题
Java 堆溢出
虚拟机栈和本地方法栈溢出
JVM 类加载
类加载的过程
1. 加载
2. 验证
3. 准备
4. 解析
5. 初始化
双亲委派模型(加载环节)
双亲委派模型工作流程:
双亲委派模型的作用:
垃圾回收机制(GC)
引入
垃圾回收,是回收内存
垃圾回收的过程:
第一步:识别出垃圾
1. 引用计数算法
引用计数算法的描述
引用计数的缺点
2. 可达性分析
第二步:把标记为垃圾的对象的内存空间进行释放
a. 标记 - 清除
b. 复制算法
c. 标记 - 整理
JVM 中使用的方案 --- 分代回收(依据不同种类的对象,采取不同的方案)
总结:一个对象的一生
JMM
1)主内存与工作内存
2)内存间交互操作
完!!!
JVM 简介
JVM 是 Java Virtual Machine 的简称,意为 Java 虚拟机。
虚拟机是指通过软件模拟的,具有完整硬件功能的,运行在一个完全隔离的环境中的完整计算机系统。
我们在学习 Java SE 的时候,简单了解过 JVM,还有两个相关的概念,jdk,Java 开发工具包,jre,Java 运行时环境,其中,jvm 虚拟机包括在 jre 其中,而 jre 又包含在 jdk 其中。所以我们在编写 Java 代码中,下载 jdk 即可在记事本中编写代码,然后通过命令行运行进程。
编译语言,在以前大致可以分为两种:
编译型语言,在程序执行前,需要通过编译器将源代码一次性编译成目标机器的机器码,之后就可以直接运行生成的可执行文件。
解释型语言,在程序运行时,由解释器逐行读取源代码,并将其解释成目标机器能够理解的指令后立即执行。
上述的说法,如今其实已经不适用了,如果按照上述经典的划分方法,Java 属于是“半编译,半解释型”语言。
Java 这么做的最主要目的,是为了实现“跨平台”!!!
C++ 这样的语言,是直接编译成了二进制的机器指令,但需要注意的是,不同的 CPU 中,支持的指令是不一样的,而且,生成的可执行程序,在不同的系统上也由不同的格式。
Java 不想在不同的 CPU 中进行重新编译,而是期望能够直接执行~~
还记得我们最开始用记事本写的 hello world 吗???
创建一个记事本,写出代码
将记事本的后缀 .text 改为 .java
然后在对应目录下的命令行中先运行 javac,将 java 文件 ==》 .class 文件
然后再输入 java 即可运行~~
上面的 .class 文件,是字节码文件,包含的就是 Java 字节码(是 Java 自己搞的一套“CPU 指令“),然后再某个具体的系统平台上执行,此时再通过 jvm,把上述的字节码转换成对应的 CPU 能识别的机器指令。(在这个过程中,jvm 相当于一个”翻译官“的角色)。
因此,我们编写和发布一个 Java 程序,其实就只需要发布 .class 文件即可,jvm 拿到 .class 文件,就知道如何进行转换了~~~
windows 上的 jvm 就可以把 .class 转换成 windows 上支持的可执行指令了
linus 上的 jvm 就可以把 .class 转换成 linux 上支持的可执行指令了
..................................
不同平台的 jvm 是存在差异的,不是同一个~~~
补充:
jvm 也是由许多许多版本的,,目前 HotSpot VM 是占用绝的市场地位,称霸武林~~~所以我们下面的内容中,默认都是使用 HotSpot 的~~
JVM 本身的一个非常复杂的东西,涉及到很多和底层密切相关的内容,我们这里主要关注三个话题:
1) JVM 中的内存区域划分
2) JVM 中的类加载机制
3) JVM 中的垃圾回收机算法
JVM 中的内存区域划分
JVM 其实也是一个进程,我们可以随便运行一个之前的多线程代码不结束,在任务管理器中,就可以看到 Java 进程。
进程运行的过程中,需要从操作系统中申请一些资源(内存就是其中的典型资源),这些内存空间,就支撑了后续 Java 程序的执行。比如,在 Java 中定义变量,就会申请内存,内存,其实是 jvm 从系统这边申请到的内存~~
jvm 从系统中申请到了一大块内存,这一大块内存给 Java 的程序所使用,但也会根据实际的使用用途来分出不同的空间 ==》 也就是所谓的区域划分~
就类似于我们的我们的学校,占有一大块土地面积,要把整个空间分成不同的区域:
类似的,JVM 申请到的空间,也会划分出几个区域,每个区域都有不同的作用。
1. 堆(一个进程只有一份 ------ 线程共享)
我们代码中 new 出来的对象,都是在堆里面的。对象中持有的非静态成员变量,也是在堆里面的。
我们常见的 JVM 参数设置 -Xms10ms 最小启动内存就是针对堆的,-Xmx10m 最大运行内容也是针对堆的(ms 是 memory start 的简称,mx 是 memory max 的简称)
2. 栈(一个进程可以有 N 份 ------ 线程私有)
栈分为 本地方法栈 和 Java 虚拟机栈
Java 虚拟机栈:
通过 C++ 实现的代码,调用关系和局部变量。Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法指向的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态连接,方法出口等信息。我们经常所说的堆内存,栈内存,其中,栈内存指的是就算虚拟机栈。
1. 局部变量表,存放了编译器可知的各种基本数据类型(8 大基本数据类型),对象引用。局部变量所需的内存空间在编译间完成分配。当进入一个方法的时候,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表的大小。简单来说,这里存放方法参数和局部变量。
2. 操作帧:每个方法会生成一个先出后进的操作帧。
3. 动态连接:指向运行时常量池的方法引用。
4. 方法返回地址:PC 寄存器的地址。
什么是线程私有???
由于 JVM 的多线程是通过线程轮流切换并分配处理器的执行时间来实现,因此在任何一个确定的时刻,一个处理器(多核处理器的话,则指的是其中的一个内核)都只会执行线程中的一条指令。因此为了切换县城后能恢复到正确的位置,每个线程都需要独立的程序计数器,各个线程之间的计数器互不影响,独立存储。我们就把类似这样的区域,称之为”线程私有“的内存~~
本机方法栈:
本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈,则是记录了 Java 代码的调用关系和 Java 代码中的局部变量~~
我们一般不是很关注本地方法栈,一般谈说到栈,默认指的是虚拟机栈。
3. 程序计数器(一个线程可以有 N 份 -------- 线程私有)
记录当前线程所执行的字节码指令的地址。在多线程环境中,每个线程都有独立的程序计数器,用于确保线程切换之后能够恢复到正确的执行位置。当线程执行到 Java 方法的时候,程序计数器记录的是正在执行的虚拟机字节码指令的地址。而当线程执行本地(Native)方法的时候,程序计数器的值为空(Undefined)。(本地方法指的是使用 Java 意外的语言编写的方法)
4. 元数据区(一个线程只有一份 -------- 线程共享)
(以前的 Java 版本中,也叫做”方法区“,从 1.8 开始,改为了元数据区~~)
”元数据“是计算机中的一个常见术语(meta data),往往指的是一些辅助性质的,描述性质的属性~~
比如我们的硬盘,硬盘中不仅仅要存储文件的数据本体,还需要存储一些辅助信息,比如:文件的大小,文件的位置,文件的拥有者,文件的修改时间,文件的权限信息等等.....这些辅助信息统称为”辅助信息“。
JVM 中的元数据区:存储被虚拟机加载的类信息,方法的信息,常量,静态变量。即一个程序有那些类,每个类中有那些方法,每个方法的里面都要包含那些指令,都会记录在元数据区中。
我们写的 Java 代码,if while for 等等各种逻辑运算,这些操作最终都会被转换成 java 字节码 ==》 javac 就会完成上述操作~~
此时,这些字节码在程序运行的时候,就会被 JVM 加载到内存中,放到元数据(方法)区中。
此时,当前程序要如何执行,要做那些事情,就会按照上述元数据区里面记录的字节码依次执行了。
一道经典的笔试题:
有伪代码如下:
class Test{private int n;private static int m;
}main() {Test t = new Test();
}
问:上述代码中,t n m 各自都存储在 JVM 内存中的那个区域???
n 是 Test 类中的非静态成员变量,是处于 堆 上的。
t 是一个引用类型的局部变量,本身是在 栈 上的。
而 m 是带有 static 修饰的变量,是在类对象中,也就是在 元数据区 中。
static 修饰的变量,称为 类属性。
static 修饰的方法,称为 类方法。
非 static 的变量,称为 实例属性。
非 static 的方法,称为实例方法。
类对象,我们前面提到过 ==》 类名.class。即例子中的 Test.class,JVM 把 .class 文件加载到内存之后,就会把这里的信息使用对象来表示,此时这样的对象就是类对象。类对象里面包含了一些信息,包括但不限于:类的名称,类继承自那个类,实现了那些接口,都有那些属性,都叫什么名字,都是什么类型,都是什么权限。都有那些方法,都叫什么名字,都需要什么参数,都是什么权限...... .java 文件中涉及到的信息,都会在 .class 中有所体现(注释不会包含~~)
总结:
内存布局中的异常问题
Java 堆溢出
Java 堆用于存储对象实例,只要不断创建对象,并且保证 GC(垃圾回收) Roots 到 对象之间有可达路径,来避免 GC 清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。
我们前面已经讲到,可以设置 JVM 参数 -Xms 设置堆的最小值,-Xmx 设置堆的最大值。我们下面来看一个 Java 堆 OOM(OutOfMemoryError 的缩写,表示内存溢出错误)的测试。测试一下代码之前,我们可以先设置 idea 的启动参数:
JVM 参数为 -Xmx20m-Xms20m-XX:+HeapDumpOnOutOfMemoryError
-Xmx20m :设定 JVM 堆内存的最大可用空间为 20 MB。当 Java 程序在运行过程中需要更多的堆内存,而当前使用的堆内存已经达到这个最大值时,无法进行有效的垃圾回收以释放内存,就会抛出 OutOfMemoryError 异常
-Xms20m:设定 JVM 堆内存的初始大小为 20 MB,JVM 在启动时会为堆内存分配这么大的空间。
-XX:+HeapDumpOnOutOfMemoryError:-XX 是 JVM 的高级参数前缀,用于设定一些特定的 JVM 行为。+ 表示弃用该参数对应的功能,这里的 HeapDumpOnOutOfMemoryError 表示当 JVM 抛出 OutOfMemoryError 异常的时候,自动生成堆转储文件(Heap Dump),堆转储文件包含了当时堆内存中所有对象的信息,是分析内存泄漏和性能问题的重要依据。
main 方法里面创建了一个 ArrayList 集合,接着通过一个无限循环不断的往集合中添加 OOMObject 对象,由于这些对象都被集合引用着,垃圾回收器无法回收他们,随着对象数量的持续增加,堆内存最终被耗尽,从而触发 OutOfMemoryError 异常
Java 堆内存的 OOM 异常是实际应用中最常见的内存溢出情况。当出现 Java 堆内存溢出的时候,异常堆信息“java.lang.OutOfMemoryError”会进一步的提示“Java heap space”。当出现“Java heap space”的时候,就是很明确的告知我们,OOM 发生在堆上。
此时要对 Dump 出来的文件进行分析(会 Dump 出一个 hprof 文件,可以使用工具 VisualVM进行分析)。分析问题的产生到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
内存泄漏:泄漏的对象无法被 GC
内存溢出:内存对象确实还应该存活。此时要根据 JVM 堆参数与物理内存相比较,检查是否还应该将 JVM 的内存调大一些,或者检查对象的生命周期是否过长。
虚拟机栈和本地方法栈溢出
HotSpot 虚拟机是将虚拟机栈与本地方法栈合二为一,因此对于 HotSpot 来说,栈容量只需要由 -Xss 参数设置。
关于虚拟机栈会产生的两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出 StackOverFlow 异常
如果虚拟机在拓展栈无法申请到足够的内存空间,则会抛出 OOM 异常。
补充:栈深度:
栈帧:JVM 中方法调用和执行的基本数据结构,每个方法调用都会创建一个对应的栈帧。当一个方法被调用时,JVM 会对该方法对应的栈帧压入调用栈的栈顶,当方法执行完毕返回时,栈帧会从栈顶弹出。比如,方法 methodA 中调用了方法 methodB,此时 methodB 创建一个新的栈帧并压入栈顶,位于 methodA 栈帧之上。
栈深度:就是当前线程的调用栈中的栈帧的数量。例如:主线程先调用 methodA,methodA 调用 methodB,methodB 中又调用 methodC,此时调用栈中就依次存在:主线程,methodA,methodB,methodC 的栈帧,栈深度为 4。
限制:JVM 对栈深度有一定的限制,一般可以通过 -Xss 参数来调整(-Xss1m 表示将每个线程的栈大小设置为 1MB,栈大小会间接的影响栈深度~~)
举例:观察 StackOverFlow 异常(单线程环境下)
上面的 Java 代码通过一个递归方法不断调用自身,每次调用时增加计数器 stackLength 的值,以此来模拟栈帧的不断入栈过程,当站深度超过了 JVM 所允许的最大深度,会抛出 StackOverFlowError 异常,捕获该异常后输出当前的栈深度。
出现 StackOverflowError 异常的时候,有错误堆栈可以阅读,比较好找到问题所在。如果使用虚拟机的默认参数,栈深度一般情况下可以到达 1000 - 2000,对于正常的方法调用(包括递归)。完全够用了~~
如果是因为多线程导致的内存溢出问题,在不能减少线程数的情况下,只能减少最大堆和减少栈容量的方式来换取更多线程。
举例:观察多线程下的内存溢出异常
该 Java 代码的核心目的是演示因创建大量线程而导致内存溢出的问题。当不断创建新线程时,每个线程都会占用一定的栈内存。由于系统的内存资源是有限的,当创建的线程数量过多,使得所有线程的栈内存综合超过了系统所能提供的内存时,就会抛出 OutOfMemoryError 异常,提示”unable to create native thread“,也就是无法创建新的本地线程。
注意:不要轻易尝试上述代码~~
JVM 类加载
类加载,指的是 Java 进程运行的时候,需要把 .class 文件从硬盘读取到内存,并进行一系列校验解析的过程。 .class 文件 ==》 类对象
类加载的过程
类加载的过程其实是 Java 官方文档中给出的说明
跳转网址如下:Java SE Specifications
红色圈住的表示,该版本的 Java 语言规范(语法是什么样的)
蓝色圈主的表示,该版本的 Java 虚拟机规范(虚拟机是什么样的)
其实正常来说,我们作为程序员来说,是不需要关注这些具体的加载过程的,需要了解的时候直接来翻一翻文档即可,但是面试可能要考~~
对于一个类来说,他的生命周期是这样的:
其中前 5 步是固定的顺序,同时也是累加载的过程,中间 3 步都属于连接过程,所以对于类加载来说,总共分为如下的几个步骤:
1. 加载 2. 连接(a. 验证 b. 准备 c. 解析)3. 初始化
1. 加载
加载(Loading)阶段是整个类加载(Class Loading)过程中的一个阶段,和类加载是有所不同的,注意不要把二者混为一谈~~
把硬盘上的 .class 文件,找到,打开文件,读取到文件内容(认为读取到的是二进制的数据)
(找到硬盘上的 .class 文件这一步还有一些事项注意,我们后面介绍)
2. 验证
验证是连接阶段的第一步,这一阶段的目的是要确保,读到的 .class 文件中的字节流中包含的信息,是符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后,不会危害虚拟机自身的安全~~
上述为 Java 类文件结构的描述,这里的描述方式,类似于 C 语言的结构体。
u4 表示 四个字节的无符号整数,u2 表示 两个字节的无符号整数。Java 中,int 就是四个字节,short 就是两个字节,但是 C++ 并不是,在 C++ 中程序员往往就会字节通过 typedef 定义出一些类型,往往就是 u2 u4 之类的~~
第一个 magic,也叫做 magic number -- 魔幻数字,广泛应用于二进制文件格式中,用来表示当前二进制文件的格式是那种类型的。
2,3 用来表示版本号,我们平时说的 Java 8 Java 17 Java 23 什么的,是我们使用的版本,实际上 JVM 开发还有内部的版本。JVM 执行 .class 文件就会验证版本是否符合要求,一般来说,高版本的 JVM 是可以运行低版本的 .class 的~~
剩下的就是一些类具体的信息啦~~
3. 准备
正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并且设置类变量初始值的阶段,但此时申请到的内存空间,里面的默认值都是全 0 的。
比如有这样的代码: public static int value = 123;
此时初始化 value 的 int 值为 0,而不是 123
4. 解析
Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。
上面这句话似乎非常拗口难懂,我们可以举个栗子来解释一下:
女神电脑坏了,给我们打电话让我们上门服务,我靠,好机会呀。
1. 符号引用
我们拿着女神的地址:XX小区 X 号楼 X 单元 XXX 室就去了~~~
此时,女神给我们的地址,就是符号引用。
2. 解析过程
我们有了地址,但我们是第一次去女神家,打开了地图 APP,输入地址 XX小区 X 号楼 X 单元 XXX 室,地图通过数据库查询,将地址转换为了 经纬度坐标(北纬 XX.X°,东经 XX.X°),并规划从当前位置到目的地址的路线。这就类似于虚拟机查找常量池,将符号引用绑定到实际内存地址。
3. 直接引用
地图最终会给我们显示具体的路线:沿人民路直行 500 米然后右转,我们就可以直接按照直接路线找到女神家啦~~
那 Java 虚拟机中是怎么做的呢???
假如有如下代码:
class Test {private String s = "hello";
}
我们上面的代码中,是很明确的知道,s 变量里面相当于保存了 “hello”字符串常量的地址。
但是,在文件中,是不存在“地址”这样的概念的,地址是内存的地址,我们是文件,是在硬盘中的,没有地址这个概念~~
虽然没有地址,我们可以存储一个类似于地址“偏移量”这样的概念,此时文件中和填充给 s 的“hello”的偏移量,就可以认为是“符号引用”。
接下来,把 .class 问价加载到内存中,就会先把 “hello” 这个字符串加载到内存中,加载到内存中后,“hello” 就有地址了,接下来,s 里面的值就可以替换成当前“hello”真实的地址了,也就是“直接引用”了。
5. 初始化
初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 代码。同时也针对类对象完成后续的初始化,还要执行静态代码块的逻辑,也可能会触发父类的初始化。
双亲委派模型(加载环节)
我们在加载的时候,说要在硬盘上找到 .class 文件打开,这个双亲委派模型就描述了如何查找 .class 文件的策略。
JVM 中进行类加载的操作,是有一个专门的模块,称为“类加载器”(ClassLoader)。
类加载器的作用:给他一个“全限定类名”(带有报名的类名),例如 java.lang.String ==》 给定全限定类名之后,能找到对应的 .class 文件。
全限定类名(Fully Qualified Class Name)是指包括包名在内的类的完整名称,用于在程序中唯一的标识一个类,可以避免类名冲突的问题。
JVM 中的类加载器默认是有 三个 的。(也可以进行自定义)
BootstrapClassLoader --------- 负责查找标准库的目录
ExtensionClassLoader --------- 负责查找扩展库的目录
(Java 语法规范里面描述了标准库里面应该有那些功能,但是实现 JVM 的厂商,会在标准库的基础上再扩充一些额外的功能~~~不同的厂商扩展可能不太一样,上古时期用处较大,现在极少用)
ApplicationClassLoader --------- 负责查找当前项目的代码目录以及第三方库的目录
上面的三个类加载器,存在“父子关系”(不是面向对象中的,父类 子类之间的继承关系),是类似于“二叉树”,有一个指针(引用)parent ,指向自己的“父”类加载器。
启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即 JAVA_HOME/lib 目录。
扩展类加载器:加载 lib/ext 目录的类
应用户程序类加载器:加载我们写的应用程序
自定义类加载器:根据自己的需求定制类加载器
双亲委派模型,描述了上述类加载器之间是如何配合工作的。
双亲委派模型工作流程:
1. 从 ApplicationClassLoader 作为入口,先开始工作。
2. ApplicationClassLoader 不会立即搜索自己负责的目录,会把搜索的任务交给自己的父亲。
3. 代码进入到 ExtensionClassLoader 范畴了,ExtensionClassLoader 也不会立即搜索自己负责的目录,而是也把搜索的任务交给自己的父亲。
4. 代码就进入到了 BootstrapClassLoader 范畴了,BootstrapClassLoader 也不想立即搜索自己负责的目录,也要把搜索的任务交给自己的父亲。
5. BootstrapClassLoader 发现自己没有父亲了,才会真正的搜索负责的目录(标准库目录),通过全限定类名,尝试在标准目录中找到符合要求的 .class 文件。
即双亲委派模型,会先以 ApplicationClassLoader 为入口,一点点先向上找父亲:
到最上面的 BootstrapClassLoader ,如果他找到了,接下来就直接进入到打开文件 / 读文件等流程中。
如果没找到,就会回到孩子这一辈的类加载器中,继续尝试加载
6. ExtensionClassLoader 收到父亲交回给他的任务之后,就开始进行搜索自己负责的目录(扩展库的目录)
如果找到了,就进入到后续的流程中。
如果没找到,也是回到孩子这一辈的类加载器中继续尝试加载。
7. ApplicationClassLoader 收到父亲交回给他的任务之后,就开始搜索自己负责的目录(当前项目目录 / 第三方库目录)
如果周到了,接下来进入后续流程。
如果没找到,也是回到孩子这一辈的类加载器中继续尝试加载,由于默认情况下,ApplicationClassLoader 没有孩子了。此时就说明类加载的过程失败了!!!就会抛出 ClassNotFoundException 的异常了。
则整个工作流程图为:
双亲委派模型的作用:
确保类的唯一性,避免重复加载:
当类加载器收到加载类的请求时,先将请求委派给父类加载器。若父类加载器已经加载过该类,就不会重复加载,直接返回已经加载的类对象。
例如:在一个大型 Java 项目中,多个模块可能都依赖同一个类,通过双亲委派模型,这个类只会被加载一次,节省了内存资源。防止了同一个类被不同类加载器加载多次到 JVM 中。
保障核心类库安全,防止核心类被篡改:
Java 核心类库(如 java.lang 包下面的类)由启动类加载器加载。即使有程序员编写了与核心类库同名的类,由于双亲委派机制,自定义类加载器在接到加载请求时候,会向上委派给父类加载器,最终由启动类加载器优先加载核心类库中的类,而不是加载程序员自定义的同名类,保证了 JVM 运行的安全性和一致性。
提供更好的模块化支持,实现模块隔离:
在 Java 应用程序中,不同模块可能存在同名类。双亲委派模型使得不同模块的同名类由不同的类加载器(类加载器是由层级关系的),这些类在内存中是相互隔离的。例如,在 Web 应用服务器中,不同 Web 应用的类加载通过双亲委派模型实现隔离,每个 Web 应用的类加载器加载到自己应用路径下的类,同时共享服务器的公共库。
上述这一系列规则,只是 JVM 自带的类加载器默认遵守的规则。如果我们自己写类加载器,也可以打破上述规则~~
垃圾回收机制(GC)
引入
我们在 C 语言中,学习过动态内存管理,malloc 函数申请内存,free 释放内存。在 malloc 中,申请到的内存,生命周期是跟随整个进程的。这一带你对于 7 * 24 的服务器程序是非常不友好的。服务器每个请求都去 malloc 一块内存,如果不 free 释放,就会导致申请的内存越来越多,后续要向申请内存就没得申请了 ==》 内存泄漏问题。
而我们在实际开发中,的确很容易出现一不小心就忘记调用 free 了,或者是因为一些情况,比如 if -> return 导致 free 没有被执行到的情况~~
我们能否让释放内存的操作,让程序自动负责完成,而不是依赖于程序员的手工释放呢???
Java 就属于早期支持 垃圾回收 的语言。
引入垃圾回收这样的机制,就不需要手动来进行释放了,程序会自动判定,某个内存是否会继续使用,如果内存后续不使用了,就会自动释放掉。
后世的各种编程语言,大部分都是带有垃圾回收机制的~~~
垃圾回收机制中还有一个很重要的问题:STW(stop the world)问题。即触发垃圾回收的时候,很可能会使当前程序的其他的业务逻辑被暂停。
但是随着 Java 语言这么多年的发展,这么多大佬的不断风险,GC 的技术积累也越来越强大,有办法将 STW 的时间控制在 1ms 之内~~
垃圾回收,是回收内存
对于程序计数器,虚拟机栈,本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性。元数据区,一般都是涉及到“类加载”,很少涉及到“类卸载”。因此我们这里所讲的内存分配和回收重点关注的是 Java 堆这个区域,这个区域也是 GC 的主要战场。
这里的垃圾回收,说是回收内存,其实更准确的说是“回收对象”。每次垃圾回收的时候,释放的若干个对象(实际的单位都是对象)。
垃圾回收,具体是怎样进行展开的,大致分为两步:
1)识别出垃圾,那些对象是垃圾(不再进行使用),那些对象不是垃圾
2)把标记为垃圾的对象的内存空间进行释放。
垃圾回收的过程:
第一步:识别出垃圾
即判定这个对象后续是否还要继续使用,在 Java 中,使用对象,就一定需要通过引用的方式来使用(当然,有一个例外是 匿名对象,即 new MyThread().start(); 但是,当这行代码执行完毕之后,对应的 MyThread 对象就会被当作垃圾~~)
如果一个对象没有任何引用指向他,我们就视为无法被代码中使用了,就可以作为垃圾了~~
void fun() {Test t = new Test();t.testFun();
}
有上述代码,Test t = new Test()。通过 new Test 就是在对上创建了对象。
与此同时,因为创建了类型为 Test 的局部变量 t,所以 t 会在栈上有空间,存储 0x1002 这个地址
当代码执行到 } 这个右花括弧的时候,此时局部变量 t 就直接被释放了。此时再进一步,上述的 new Test() 对象,也就没有引用再指向他了。此时,这个代码就无法再访问使用这个对象了,这个 对象就可以被认为是垃圾了~~
如果代码更加复杂一些,这里的判定过程也就更加麻烦了~~
Test t1 = new Test();
Test t2 = t1;
Test t3 = t2;
Test t4 = t3;
此时就会有很多的引用指向 new Test() 同一个对象了,也就是此时有很多的引用,都保存了 Test 对象的地址。
此时通过任意的引用都能够访问 Test 对象,需要确保所有的指向都销毁了,才能把 Test 对象视为垃圾。
如果代码更加复杂,上述这些引用的生命周期各不相同,此时情况就不好办了~~
1. 引用计数算法
引用计数算法的描述
给对象增加一个引用计数器,每当有一个地方引用它的时候,计数器就 +1,当引失效时,计数器就 -1,任何时刻计数器为 0 的对象就是不能再使用的,即对象已“死”。
代码如下:
Test a = new Test();
当 new Test() 的时候,还是在堆上有 Test 对象的位置,此时还没有引用变量指向这个 Test 对象,所以计数器的值为 0.
当创建一个 Test 类型的局部变量 a 的时候,Test 对象的前面的引用计数器就会变为 1
此时如果有 Test b = a;的代码,栈和堆就会产生如下的变化:
当 有代码 a = null;栈和堆就会产生如下的变化:
同样的,再把 null 赋值给 b,就会有如下变化:
此时就就可把 Test 对象视为垃圾了~~
在垃圾回收机制,会有专门的扫描线程,去获取到当前每个对象的引用计数的情况,发现对象的引用计数为 0,说明这个对象就可以释放了。
引用计数的缺点
引用计数算法实现是非常简单的,判定的效率也十分高,在大部分情况下,都是一个不错的算法,比如 Python 语言就采用引用计数法来进行内存管理。但是,主流的 JVM 中并没有选用引用计数法来管理内存。
问题一:消耗额外的内存空间
要给每个对象都安排一个计数器(如果计数器按照 2 个字节算),如果整个程序中的对象数目非常多,计数器总的消耗的空间也会非常多。尤其是如果每个对象体积比较小(假设每个对象 4 个字节),计数器消耗的空间,已经达到对象的空间的一般了~~
问题二: 引用计数器可能会产生“循环引用”的问题,此时,引用计数器就无法正确工作了。
例如有如下代码:Test 类中有 Test 类型的成员变量 t。
class Test {Test t;
}Test a = new Test();
Test b = new Test();a.t = b;
b.t = a;a = null;
b = null;
Test a = new Test(); 和 Test b = new Test();代码执行完毕后,栈和堆上的状态如下:
当执行 a.t = b;这行代码后,栈和堆上的状态如下:
同样的,当实行 b.t = a;这行代码后,栈和堆上的状态如下:
但是再执行到 a = null;这行代码,堆和栈上的状态如下:
再执行 b = null;这行代码,堆和栈上的状态如下:
此时代码就出现问题了,此时的两个 Test 对象,无法被使用,没有引用指向他们。但与此同时,他们的引用计数器却都不是 0!!!
2. 可达性分析
JVM 中使用的就是可达性分析来识别出垃圾~~
这种算法,其实本质上是使用“时间”来换取“空间”的。相比于引用计数,可达性分析需要消耗更多的额外时间,但是总体来说,来是可控的~~· 不会产生类似于“循环引用”这样的问题。
我们在写代码的过程中,会定义很多的变量。
比如,栈上的局部变量/方法区中的静态类型的变量/常量池中引用的对象...
可以从这些变量为起点,尝试去进行“遍历”,所谓比的遍历,就是沿着这些变量中持有的引用类型的成员,再进一步的往下进行访问。
所有能被访问到的对象,自然就不是垃圾,剩下的遍历一圈也找不到的对象,自然就是垃圾~~
比如有如下代码:
class Node {char val;Node left;Node right;
}Node buildTree() {Node a = new Node();Node b = new Node();Node c = new Node();Node d = new Node();Node e = new Node();Node f = new Node();Node g = new Node();a.left = b;a.right = c;b.left = d;b.right = e;e.left = g;c.right = f;return a;
}Node root = buildTree();
会创建出如下图的二叉树:
最后一行代码 Node root = buildTree();虽然这个代码中,只有一个 root 这样的引用了,但是,实际上上述 7 个节点对象都是“可达的”。
JVM 中存在扫描线程,会不停的尝试对代码中已有的这些变量去进行这些遍历,尽可能多的去访问到对象。
如果代码中出现: root.right = null;此时,c 就不可达了,由于 f 访问必须要通过 c,c 不可达,就会造成 f 也不可达,此时就会认为 c 和 f 都是垃圾了~~
第二步:把标记为垃圾的对象的内存空间进行释放
具体如何对标记为垃圾的对象进行释放,还有一些说法~~
具体的释放方式有三种
a. 标记 - 清除
把标记为垃圾的对象,直接释放掉(最朴素的做法)
但一般不会使用这个方案,因为存在 内存碎片化问题,比较致命~~
如上图,此时就是把标记为垃圾的对象对应的内存空间直接释放掉 ==》 会产生很多 小的 并且是 离散的空闲内存空间 ==》 就会导致后续申请内存失败!!!
比如:内存申请,都是一次申请一个连续的内存空间。申请 1M 的内存空间,此时,1M 字节,都是连续的。如果存在很多内存碎片,就可能导致,总的空闲空间,远远超过 1M,但是并不存在比 1M 大的连续的空间,此时,虽然有空闲空间,但是我们去申请空间就会失败~~
注意:我们这里说的是,总的空闲空间比 1M 大,比如此时有 1000 个碎片,每个碎片的大小是 10K,此时总的空闲空间是 10M,但是由于每个碎片最大都是 10K,没有超过 1M 的,所以我们申请 1M 连续的空闲空间会失败~~
b. 复制算法
复制算法是为了解决标记 - 清理的效率问题。它会将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收的时候,会将区域中还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。
这样做的好处是:每次都是对整个半区进行内存回收,内存分配时也就不需要考虑到内存碎片等复杂情况,只需要移动堆顶指针,按照顺序分配即可~~
但缺点也很明显:1. 总的可用内存变少了(豆浆买两碗,吃一碗倒一碗)~~~ 2. 如果每次要复制的对象比较多,此时复制的开销也就很大了。需要是再当前这一轮 GC 的过程中,大部分对象都释放,少数对象都存活的情况下,适合使用复制算法。
c. 标记 - 整理
这个算法的标记过程于 标记 - 清除 过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存即可。
举个例子理解:想象有一排座位,座位上有的人在(对应存活对象),有的人离开(对应可回收对象)。现在要对作为进行整理,把还在座位上的人往一端集中。比如都往最左边集中,这样原本分散在各处的人就都挨在一起了。在这个过程中,每个人要清楚的知道自己的新的位置在哪里,并且其他人如果和自己有联系(类似对象间的引用关系),也要知道自己的新位置。等所有人都集中到一端后,右边空出来的作为(对应内存空间)就可以重新安排使用了~~
这个过程,也能有效的解决内存碎片的问题,并且,这个算法,也不会像复制算法一样,需要浪费过多的内存空间。
但是!这里因为要进行移动对象,搬运内存的开销也会很大。
因此,JVM 也没有直接采用这种方案,而是结合上面的思想,搞出了一种“综合性”的方案,取长补短~~~
JVM 中使用的方案 --- 分代回收(依据不同种类的对象,采取不同的方案)
在这种方案中,引入了一个概念 -- 对象的年龄
JVM 中有专门的线程负责周期性的扫描/释放。
一个对象,如果被线程扫描到了一次,可达了(不是垃圾),年龄就 +1(初始年龄相当于是 0)
JVM 中就会根据对象年龄的差异,把整个堆内存分成两个大的部分
==》
新生代(年龄小的对象) / 老年代(年龄大的对象)
在新生代中,又分出三块区域,一块称为 伊甸区,另外两块都称为 生存区/幸存区(两块大小相等的空间)
1)当代码中 new 出一个新的对象,这个对象就是被创建在伊甸区的。伊甸区中就会有很多的对象。
一个经验规律:伊甸区中的对象,大部分是活不过第一轮 GC 的。这些对象都是“朝生夕死”的,生命周期非常短!!!
2)第一轮 GC 扫描完成之后,少数伊甸区中幸存的对象,就会通过复制算法,拷贝到幸存区。
后续 GC 的扫描线程还会继续进行扫描,不仅要扫描伊甸区,也要扫描幸存区的对象。幸存区中的大部分对象也会在扫描中被标记为垃圾,少数存活的,就会再继续使用复制算法,拷贝到另外一个幸存区中去。
只要这个对象能够在幸村区中继续存活,就会被复制算法继续拷贝到另一半的幸存区中。
每次经历一轮 GC 扫描,对象的年龄都会 +1
3)如果这个对象在幸存区中,经历了若干轮 GC 仍然健在~~~
JVM 就会认为,这个对象的生命周期大概率很长,就会把这个对象从幸存区,拷贝到老年代~~~
4)老年代的对象,当然也要被 GC 扫描,但是,扫描的频次就会大大降低了。
5)对象在老年代“寿终正寝”,此时 JVM 就会按照标记整理的方式,释放内存~~
即,新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高,就采用“标记 - 清理”或者“标记 - 整理”算法。
上述的分代回收是 JVM 中 GC 的核心思想,但是 JVM 实际的垃圾回收的实现细节上,还会有一定的优化~~~
总结:一个对象的一生
我是一个普通的 Java 对象,出生在 Eden 区,在 Eden 区,我还看到了很多和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间的。有一天 Eden 区中的人实在是太多了,我就被迫区了 Survivoir 的 “From” 区(S0 区),自从去了 Survivor 区,我就开始飘飘然了,有时候在 Survivor 的“From” 区,有时候在 Survivor 的“To”区(S1 区),居无定所。知道我 18 岁那年,爸爸说我成年了,该到社会上闯荡一下了。于是我就去了老年代那边,老年代里面,人很多,并且年龄都挺大的,我也在这里认识了很多人。在老年代里面,我生活了很多年(每次 GC 加一岁),最终被回收了~~~
补充:
JMM
JVM 定义了一种 Java 内存模型(Java Memory Model ==》 JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能一次编译到处访问。
在此之前,C/C++ 是直接使用物理硬件和操作系统的内存模型,因此,由于不同平台下的内存模型的差异,有可能导致程序在一套平台上并发完全正常,却在另一台平台上并发访问经常出错。
1)主内存与工作内存
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在 JVM 中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包括 实例字段,静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后两者是线程私有的,不会被线程共享。
Java 内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取 赋值...),都必须在工作内存中进行,而不能直接读取主内存中的变量。
不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
其关系如下图所示:
2)内存间交互操作
关于主内存与⼯作内存之间的具体交互协议,即⼀个变量如何从主内存中拷⻉到⼯作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了如下8种操作来完成。
JVM实现时必须保证下面提及的每⼀种操作的原⼦的、不可再分的。
- lock(锁定):作⽤于主内存的变量,它把⼀个变量标识为⼀条线程独占的状态。
- unlock(解锁):作⽤于主内存的变量,它把⼀个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作⽤于主内存的变量,它把⼀个变量的值从主内存传输到线程的⼯作内存中,以便随 后的load动作使⽤。
- load(载⼊):作⽤于⼯作内存的变量,它把read操作从主内存中得到的变量值放⼊⼯作内存的变量 副本中。
- use(使⽤):作⽤于⼯作内存的变量,它把⼯作内存中⼀个变量的值传递给执⾏引擎。
- assign(赋值):作⽤于⼯作内存的变量,它把⼀个从执⾏引擎接收到的值赋给⼯作内存的变量。
- store(存储):作⽤于⼯作内存的变量,它把⼯作内存中⼀个变量的值传送到主内存中,以便后续的 write操作使⽤。
- write(写⼊):作⽤于主内存的变量,它把store操作从⼯作内存中得到的变量的值放⼊主内存的变量 中。
Java 内存模型的三大特性:
- 原⼦性:由Java内存模型来直接保证的原⼦性变量操作包括read、load、assign、use、store和 read。⼤致可以认为,基本数据类型的访问读写是具备原⼦性的。如若需要更⼤范围的原⼦性,需 要synchronized关键字约束。(即⼀个操作或者多个操作要么全部执⾏并且执⾏的过程不会被任何 因素打断,要么就都不执⾏。
- 可⻅性:可⻅性是指当⼀个线程修改了共享变量的值,其他线程能够⽴即得知这个修改。volatile、 synchronized、final三个关键字可以实现可⻅性。
- 有序性:如果在本线程内观察,所有的操作都是有序的;如果在线程中观察另外⼀个线程,所有的 操作都是⽆序的。前半句是指"线程内表现为串⾏",后半句是指"指令重排序"和"⼯作内存与主内存同步延迟"现象。
完!!!
相关文章:
JVM 一文详解
目录 JVM 简介 JVM 中的内存区域划分 1. 堆(一个进程只有一份 ------ 线程共享) 2. 栈(一个进程可以有 N 份 ------ 线程私有) Java 虚拟机栈: 本机方法栈: 3. 程序计数器(一个线程可以…...
PVD中断检测掉电
文章目录 概述配置掉电擦写注意 概述 STM32 PVD功能具体可以检测到上电、掉电瞬间,其处理方式有中断响应及事件响应。掉电设置为上升沿触发,上电为下降沿触发 配置 1.开启PVD中断并设置其优先级 2.配置响应中断或事件的阈值电压 3.配置响应模式 生成…...
Nginx — 防盗链配置
防盗链简述 防盗链是一种保护网络资源所有者权益的技术手段,旨在防止未经授权的用户或网站通过直接链接的方式盗用资源,以下是关于防盗链的简述: 原理 基于请求头验证:服务器通过检查请求头中的特定字段,如Referer字…...
题解:P2485 [SDOI2011] 计算器
### 思路 本题是一个比较模板化的题目。 #### 一操作 考虑使用快速幂。 快速幂,只需要把 $k$ 变成二进制即可实现 $\Theta(\log k)$ 的时间复杂度。 实现方法: cpp long long qmi(long long a,long long k,long long p){ long long res 1; …...
【算法刷题笔记day one】滑动窗口(定长基础版)
前言 hello大家好呀 好久不见,上次更新是去年12月份的事情了。这段时间好好沉淀了一下,打了几场比赛,论文也写了一些,也收集了不少信息,对未来方向也有了不一样的计划。 这个算法系列可以说是接着我之前的数据结构系…...
Redis从入门到实战实战篇2
面试重点:本篇包含悲观锁,乐观锁,多线程以及分布式锁的知识 目录 3.优惠卷秒杀 3.1 -全局唯一ID 3.2 -Redis实现全局唯一Id 3.3 添加优惠卷 3.4 实现秒杀下单 3.5 库存超卖问题分析 3.6 乐观锁解决超卖问题 3.7 优惠券秒杀-一人一单 …...
代码随想录算法训练营Day43
力扣300.最长递增子序列 力扣674.最长连续递增子序列【easy】 力扣1143.最长公共子序列【medium】 力扣718.最长重复子数组【medium】 一、力扣300.最长递增子序列【medium】 题目链接:力扣300.最长递增子序列 视频链接:代码随想录 题解链接:…...
Scrapy框架之【settings.py文件】详解
settings.py 文件的主要作用是对 Scrapy 项目的全局设置进行集中管理。借助修改这个文件中的配置项,你可以对爬虫的行为、性能、数据处理等方面进行灵活调整,而无需修改爬虫代码。 ①默认英文注释settings.py # Scrapy settings for douban project # …...
Nginx发布Vue(ElementPlus),与.NETCore对接(腾讯云)
案例资料链接:https://download.csdn.net/download/ly1h1/90745660 1.逻辑说明 1.1 逻辑示意图 # 前端请求处理逻辑图浏览器请求流程: 1. 浏览器发起请求├─ 开发环境(DEV)│ ├─ 请求URL: http://192.168.0.102:3000/api/xxx│ └─ 被Vite代理处理└─ 生产…...
深入探索 AAC 编码原理与 ADTS 格式:音频世界的智慧结晶
在数字音频的广阔领域中,AAC 编码及其相关的 ADTS 格式扮演着至关重要的角色。无论是在我们日常使用的音乐 APP,还是高清视频中的音频部分,都能看到它们的身影。今天,就让我们深入探索 AAC 编码原理与 ADTS 格式的奥秘,…...
深度学习核心架构:探明四种基础神经网络
摘要 本文对多层感知机(MLP)、卷积神经网络(CNN)、循环神经网络(RNN)和注意力机制等深度学习核心架构的内部运作机制进行可视化分析。通过展示参数学习过程、激活映射和注意力分布等关键特征,揭示了"黑箱"模型的内部工作原理,为模型可解释性研…...
解析机器人 2.0.2 | 支持超过50种短视频平台的链接解析,无水印提取,多功能下载工具
解析机器人是一款功能强大的工具软件,登录即可解锁会员特权。它支持超过50种短视频平台的链接解析,包括抖音、快手、西瓜、bilibili等,并能实现无水印提取。此外,还提供P2P下载、磁力链等多种下载方式,确保用户能够快速…...
【漫话机器学习系列】237. TSS总平方和
深度理解 TSS(总平方和):公式、意义与应用 在机器学习与统计建模领域,评价模型好坏的重要指标之一就是方差与误差分析。其中,TSS(Total Sum of Squares,总平方和)扮演着非常关键的角…...
flutter3.29 build.gradle.kts设置安卓签名
1、在android目录下创建key.properties文件 storePassword密码 keyPassword密码 keyAlias别名 storeFilejks文件完整路径 2、修改android/app/build.gradle.kts 顶部插入import java.util.Properties import java.io.FileInputStreamval keystoreProperties Properties() v…...
<servlet-class>和</url-pattern>的作用
在 SpringMVC 的 web.xml 配置中,<servlet-class> 和 <url-pattern> 是两个关键配置项,分别用于指定处理请求的 Servlet 类和定义该 Servlet 拦截的请求路径规则。以下是它们的具体作用及原理分析: 一、<servlet-class> 的…...
linux部署的mysql数据库修改表名为小写配置
背景: 使用ruoyi-flowable框架初始化流程表结构时, 执行的sql语句创建的表名是大写。但mysql执行sql时大小写是敏感的 删除大写表 处理配置 使用mysql 8.0.41配置表名大小写敏感配置,需要初始化数据库 在MySQL 8.0及以上版本中,lower_case_table_names参…...
【Hot 100】94. 二叉树的中序遍历
目录 引言二叉树的中序遍历我的解题代码优化更清晰的表述建议: 🙋♂️ 作者:海码007📜 专栏:算法专栏💥 标题:【Hot 100】94. 二叉树的中序遍历❣️ 寄语:书到用时方恨少ÿ…...
基于D-Mixer与TransXNet的YOLOv8改进—融合全局-局部特征与空间降维注意力机制的CNN-ViT混合架构
随着目标检测任务对精度与效率要求的不断提升,传统的卷积神经网络(CNN)在建模长程依赖和复杂语义关系方面逐渐暴露出其局限性。而视觉Transformer(ViT)虽然在全局信息建模上表现优异,却因计算开销大、局部细节感知能力不足,在实时检测任务中难以直接部署。本文提出一种面向Y…...
《算法导论(第4版)》阅读笔记:p2-p3
《算法导论(第4版)》学习第 2 天,p2-p3 总结,总计 2 页。 一、技术总结 无。 二、英语总结(生词:1) 1.incremental (1) increase: in-(“in”) crescere “to grow” (2)increment (3)incremental: increment -al adj. incremental…...
基于Qlearning强化学习的电梯群控系统高效调度策略matlab仿真
目录 1.算法仿真效果 2.算法涉及理论知识概要 2.1 Q-learning强化学习原理 2.2 基于Q-learning的电梯群控系统建模 3.MATLAB核心程序 4.完整算法代码文件获得 1.算法仿真效果 matlab2022a仿真结果如下(完整代码运行后无水印): 仿真操作…...
嵌入式硬件篇---STM32F103C8T6STM32F103RCT6
文章目录 前言一、相同点内核与主频基础外设开发环境 二、不同点1. 存储容量2. 外设资源3. 封装与引脚 三、代码移植注意事项1. 内存与 Flash 限制Flash差异RAM调整 2. 外设差异外设缺失:GPIO 映射: 3. 中断向量表中断向量偏移 4. 时钟与总线配置APB分频…...
rhce第二次作业
任务目标 1.配置ssh实现A,B主机互相免密登录 2.配置nginx服务,通过多ip区分多网站 任务一 关闭防火墙 [rootlocalhost ~]# setenforce 0 [rootlocalhost ~]# systemctl stop firewalld.service A主机免密登录B主机 ### A主机生成密钥 [rootlocalh…...
Linux第20节 --- inode和文件系统
一、没有被打开的文件 如果一个文件没有被打开,那么该文件存储在哪里? 该文件是存储在磁盘当中的! 文件 文件内容 文件属性! 文件的内容是按照数据块存储的;文件的属性其实就是inode(是一个128字节的…...
LeetCode - 19.删除链表的倒数第N个结点
目录 题目 解法一 双指针算法 核心思想 执行流程 具体例子 代码 解法二 两次遍历法 核心思想 执行流程 具体例子 代码 题目 19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode) 解法一 双指针算法 核心思想 利用双指针间隔固定距离(n1)&a…...
在 Ubuntu 上安装 cPanel
开始之前,请确保拥有一台 Ubuntu 服务器,推荐使用 Ubuntu 22.04 LTS。如果没有,可以查看免费服务器: 11个免费 VPS,够用一辈子了!(2025最新)Top 11 免费VPS推荐平台对比(…...
《Linux macOS :GCC升级方法》
GCC(GNU Compiler Collection)是广泛使用的编译器套件,升级到9以上版本可以获得更好的C17/20支持和性能优化。以下是不同Linux发行版和macOS的升级方法: Ubuntu/Debian 系统 添加工具链源 sudo apt update sudo apt install soft…...
C++ STL vector容器详解:从原理到实践
引言 亲爱的小伙伴们,今天我要和大家分享一个C编程中的"神器"——vector容器!作为STL(标准模板库)中最常用的容器之一,vector就像是一个"超级数组",既有数组的高效随机访问特性&#…...
[计算机网络]数据链路层
0 概论:数据链路层都干什么事,提供啥功能 比物理层再高一层就是数据链路层,咱们上一篇讲物理层,物理层直接接触传输介质,现在数据链路层是使用物理层的传输服务,然后实现更多的功能。物理层是只管把比特流…...
基于 vue-flow 实现可视化流程图
vue-flow 是一个基于 Vue.js 的强大且灵活的可视化流程图库,它允许开发者轻松创建交互式的流程图、工作流图、节点图等。 主要特点 易于使用 :提供了简洁的 API 和组件,开发者可以快速上手并创建复杂的流程图。高度可定制 :支持…...
【网络编程】HTTP(超文本传输协议)详解
🦄个人主页:修修修也 🎏所属专栏:网络编程 ⚙️操作环境:Visual Studio 2022 目录 📌HTTP定义 📌HTTP工作原理 1.客户端发起请求: 2.服务器处理请求: 3.客户端处理响应: 📌HTTP关键特性 🎏HTTP请求方法 &am…...
NuttX 与 PX4 系统开发全流程详解
NuttX 与 PX4 系统开发全流程详解 目录 1. NuttX 构建与使用2. NuttX 启动流程解析3. BootLoader 源码分析4. GPIO 驱动机制5. I2C 驱动分析6. PX4 系统架构简析7. uORB 消息机制8. PX4 应用开发示例9. 串口及 GPS 驱动解析10. MAVLink 协议与 PX4 交互 1. NuttX 构建与使用 …...
【Mytais系列】Myatis的设计模式
目录 设计模式 1. 工厂模式(Factory Pattern) 2. 建造者模式(Builder Pattern) 3. 动态代理模式(Dynamic Proxy Pattern) 4. 模板方法模式(Template Method Pattern) 5. 策略模…...
Linux:进程优先级及环境
一:孤儿进程 在Linux系统中,当一个进程创建了子进程后,如果父进程执行完毕或者提前退出而子进程还在运行,那么子进程就会成为孤儿进程。子进程就会被systemd(系统)进程收养,其pid为1 myproces…...
网络编程初识
注:此博文为本人学习过程中的笔记 1.socket api 这是操作系统提供的一组api,由传输层向应用层提供。 2.传输层的两个核心协议 传输层的两个核心协议分别是TCP协议和UDP协议,它们的差别非常大,编写代码的风格也不同,…...
疾病传播模拟 ——python实操
1、需求 疾病传播模拟 定义一个Infection类,包含初始感染人数、每日感染率等属性,以及一个simulate_spread方法用于模拟疾病传播过程。 使用numpy随机生成初始感染人数(范围1-100)和每日感染率(范围0.01-0.1)。 创建Infection对象,模拟10天的疾病传播过程,每天计算感染…...
用docker ffmpeg测试视频vmaf分数,很快不用编译
之前测试vmaf要自己编译libvmaf,自己编译ffmpeg,巨麻烦,或者用老旧不再维护的docker仓库,最近在docker hub上发现了编译了libvmaf的ffmpeg的docker,而且镜像很小,适合直接运行。 # dest.mp4 评分视频&…...
【浅学】Windows下ffmpeg+nginx+flv将本地视频推流在本地搭建的Web前端页面中播放,超详细步骤
Nginx安装和配置 下载nginx-1.19.3-http-flv 模块预编译包并解压放在d盘,路径就跟安装步骤里说的一样(如下图),不然会有其他问题出现。 打开conf/nginx.conf,查看RTMP和http相关的配置,确认端口号和路由名称 ffpemg推流视频…...
SQL笔记——左连接、右连接、内连接
前言:总是忘记表连接的区别,在面试的时候也容易被问到,因此就好记性不如烂笔头吧 集合运算 有并集、交集、差集 联合查询*(针对行合并的)* union为关键字,就是将两个select的结果求并集(此时重…...
iOS启动优化:从原理到实践
前言 在iOS应用开发中,启动速度是影响用户体验的重要因素之一。研究表明,启动时间每增加1秒,用户留存率就会下降约7%。本文将深入探讨iOS启动优化的各个方面,从底层原理到具体实践,帮助开发者打造更快的应用启动体验。…...
202553-sql
目录 一、196. 删除重复的电子邮箱 - 力扣(LeetCode) 二、602. 好友申请 II :谁有最多的好友 - 力扣(LeetCode) 三、176. 第二高的薪水 - 力扣(LeetCode) 一、196. 删除重复的电子邮箱 - 力扣…...
Socket-TCP
在TCP/ip协议中,用源IP、源端口号、目的IP、目的端口号、协议号这样一个五元组来标识一个通信! 端口号范围划分 0 - 1023: 知名端口号,HTTP,FTP,SSH 等这些广为使用的应用层协议,他们的端口号都是固定的。…...
BOSS的收入 - 华为OD机试(A卷,C++题解)
华为OD机试题库《C》限时优惠 9.9 华为OD机试题库《Python》限时优惠 9.9 华为OD机试题库《JavaScript》限时优惠 9.9 代码不懂有疑问欢迎留言或私我们的VX:code5bug。 题目描述 一个 XX 产品行销总公司,只有一个 boss,其有若干一级分销&…...
神经网络的基本概念与深度解析——基于生物机制的仿生建模与工程实现
广义上讲,神经网络是泛指生物神经网络与人工神经网络这两个方面。所谓生物神经网络是指由中枢神经系统(脑和脊髓)及周围神经系统(感觉神经、运动神经、交感神经、副交感神经等)所构成的错综复杂的神经网络,…...
JavaScript基础-运算符优先级
在JavaScript编程中,理解运算符的优先级是编写正确且高效代码的关键之一。当一个表达式包含多个运算符时,JavaScript会根据运算符的优先级来决定执行顺序。如果不了解这些规则,可能会导致意外的结果。本文将详细介绍JavaScript中的运算符优先…...
【RocketMQ NameServer】- NameServer 启动源码
文章目录 1. 前言2. RocketMQ 通信架构3. NameServer 启动流程3.1 创建 NameServerController3.2 启动 NameServerController3.3 NamesrvController#initialize3.3.1 Netty 通信的整体流程3.3.2 创建 NettyRemotingServer 3.4 this.remotingServer.start()3.4.1 this.remotingS…...
Learning vtkjs之WindowedSincPolyDataFilter
过滤器 模型简化(光滑处理) 介绍 像是对模型进行特征信息的简化(光滑处理) 效果 核心代码 主要流程 const fullScreenRenderer vtkFullScreenRenderWindow.newInstance({background: [0, 0, 0],rootContainer: vtkContainerR…...
C++ - 数据容器之 forward_list(创建与初始化、元素访问、容量判断、元素遍历、添加元素、删除元素)
一、创建与初始化 引入 <forward_list> 并使用 std 命名空间 #include <forward_list>using namespace std;创建一个空 forward_list forward_list<int> fl;创建一个包含 5 个元素,每个元素初始化为 0 的 forward_list forward_list<int&g…...
ES6/ES11知识点
ES 全称ECMAScript ,是脚本语言的规范,javascript是ES的一种实现。 作用域链 在 JavaScript 中,作用域链是一个非常重要的概念,它决定了变量和函数的访问顺序。掌握作用域链有助于深入理解执行上下文、闭包和变量查找等概念。 …...
力扣面试150题--二叉树的最大深度
Day 40 题目描述 做法 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right…...
360驱动大师v2.0(含网卡版)驱动工具软件下载及安装教程
1.软件名称:360驱动大师 2.软件版本:2.0 3.软件大小:218 MB 4.安装环境:win7/win10/win11 5.下载地址: https://www.kdocs.cn/l/cdZMwizD2ZL1?RL1MvMTM%3D 提示:先转存后下载,防止资源丢失&…...