JVM详解
目录
一.JVM的概念
1. 什么是JVM?
2.JVM用来干什么?
二JVM运行流程
JVM执⾏流程
2.1类加载机制
2.2类加载机制带来了哪些好处?
2.3类加载的过程是什么?
2.3.1加载
2.3.2验证
2.3.3准备阶段
2.3.4解析阶段
符号引⽤
直接引⽤
2.3.5初始化阶段
2.4类加载器
什么是双亲委派模型?
双亲委派模型的优点
破坏双亲委派模型
2.5运行时数据区
程序计数器
虚拟机栈
操作数栈
动态链接
方法的返回地址
本地方法栈
堆
.对象创建的过程了解吗?
能说⼀下对象的内存布局吗?
对象怎么定位?
元空间和方法区
运行时常量池
执行引擎
那为什么 JIT 就能提⾼程序的执⾏效率呢,解释器不也是将字节码翻译为机器码交给操作系统执⾏吗?
怎么样才会被认为是热点代码呢?
JIT的编译优化
逃逸分析
三.垃圾回收机制
垃圾回收的概念
3.1 那我们怎么知道哪些对象是否是垃圾?有哪些算法用来确定呢?
3.1.1引用计数法
3.1.2可达性分析算法
JVM栈引用的对象
本地方法栈中引用的对象
3.2 Stop The World
3.3 垃圾清除
3.3.1 标记清除算法
3.3.2复制算法
3.3.3标记整理算法
3.3.4分代收集算法
Eden区
Survivor区
3.4垃圾收集器
3.4.1CMS收集器
3.4.2 G1收集器
简述java的垃圾回收机制
垃圾回收的优点和原理。
内存溢出和内存泄漏是什么意思?
12.内存泄漏可能由哪些原因导致呢?
对象的引用有哪几种?
FULLGC触发的条件
一.JVM的概念
1. 什么是JVM?
JVM (Java Virtual Machine)意为java虚拟机. 虚拟机是指通过软件模拟的具有完整硬件功能的、运⾏在⼀个完全隔离的环境中的完整计算机系统
常⻅的虚拟机:JVM、VMwave、VirtualBox。
JVM和其他两个虚拟机的区别:
1). VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
2). JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进 ⾏了裁剪。
可以把JVM看成一个抽象的计算机,具有一个字节码指令集并使用不同的存储区域,还负责执行指令,管理内存,数据和寄存器
2.JVM用来干什么?
JVM是一个字节码的解释执行器,有了 Java 虚拟机的帮助,我们编写的 Java 源代码不必再根据不同平台编译成对应的机器码了,只需要⽣成⼀份字节码,然后再将字节码⽂件交由运⾏在不同平台上的 Java 虚拟机读取将字节码翻译成对应的底层系统机器码指令再交由CPU去执⾏,跨平台特性也就实现了.
二JVM运行流程
JVM执⾏流程
程序在执⾏之前先要把java代码转换成字节码(class⽂件),JVM⾸先需要把字节码通过⼀定的⽅式类加载器(ClassLoader)把⽂件加载到内存中运⾏时数据区(RuntimeDataArea),⽽字节码⽂ 件是JVM的⼀套指令集规范,并不能直接交个底层操作系统去执⾏,因此需要特定的命令解析器**执 ⾏引擎(ExecutionEngine)**将字节码翻译成底层系统指令再交由CPU去执⾏,⽽这个过程中需要 调⽤其他语⾔的接⼝本地库接⼝(NativeInterface)实现整个程序的功能,这就是这4个主要组成 部分的职责与功能。
总结来看,JVM主要通过分为以下4个部分,来执⾏Java程序的,它们分别是:
1. 类加载器(ClassLoader)
2. 运⾏时数据区(RuntimeDataArea)
3. 执⾏引擎(ExecutionEngine)
4. 本地库接⼝(NativeInterface)
2.1类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制.
2.2类加载机制带来了哪些好处?
与那些在编译时需要进行连接工作的语言不同,Java语言,类型的加载,连接和初始化都是在程序运行期间完成的,这虽然令类加载时增加一些开销,但为Java程序提供高度的灵活性,Java天生可以动态扩展的语言特性就是依赖运行时期动态加载和动态连接这个特点实现的.如:面向接口的应用程序时,在运行时再指定其实际的运行类;
2.3类加载的过程是什么?
类从被加载到 JVM 开始,到卸载出内存,整个⽣命周期分为七个阶段,分别是加载、验证、准备、解析、初始化、 使⽤和卸载。
其中验证、准备和解析这三个阶段统称为连接。 除去使⽤和卸载,就是 Java 的类加载过程。这 5 个阶段⼀般是顺序发⽣的,但在动态绑定的情况下,解析阶段发 ⽣在初始化阶段之后.如图:
2.3.1加载
"加载" 是"类加载"过程的一个阶段,在加载阶段,虚拟机需要完成3件事情:
1)通过一个类的限定名来获取定义此类的二进制字节流
2)将这个二进制字节流所代表的静态存储区域转化为方法区的运行时数据结构.
3)在内存中生成该类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口.
总的来说JVM 在该阶段的⽬的是将字节码从不同的数据源(可能是 class ⽂件、也可能是 jar 包,甚⾄⽹络)转化为⼆进制 字节流加载到内存中,并⽣成⼀个代表该类的 java.lang.Class 对象.不是存储在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊 ,他虽然是个对象,但是存放在方法区中.
加载阶段和连接阶段的部分内容是相互交叉进行的,可能加载阶段还没有完成,连接阶段就可能开始了,但依然遵守是加载阶段先开始.
2.3.2验证
JVM 会在该阶段对Class文件的字节流中包含的信息进行验证,只有符合 JVM 字节码规范的才能被 JVM 正确执⾏,该阶段是保证 JVM 安全的重要屏障.(
使用java编辑器编写的纯粹的代码是,无法做到诸如访问数据边界以外的地方,将一个对象转型为为实现的类型,使用为空的变量区访问方法等等,如果做了,编译器就会抛出异常,阻绝编译不会进入运行阶段,更不会进入验证这个地方.
而在加载阶段说过,获取字节码文件的方式并不一定是编译器编译出的字节码文件,可以使用任何途径来获取(如从ZIP包中读取,从网络中获取,运行时生成 动态代理技术java.lang.reflect.Proxy中)等,甚至包括使用十六进制编辑器直接编写字节码文件都可以,因此JVM如果不检查输入的字节流,很可能会因为载入有害的字节流而导致系统崩溃.
验证阶段大致上分为4哥阶段的检验动作:文件格式检验,元数据检验,字节码检验,符号引用检验.
1.文件格式检验.
验证的是字节流是否符合Class文件格式的规范,并且能被当前版本的JVM处理.
1)是否以魔数0xCAFEBABE开头
2)主,次版本号是否在当前JVM处理范围之内.
3)常量池的常量中是否有不被支持的常量类型(检查常量的tag标志)等等.
这个阶段主要保证输入的字节流能正确的解析并存储在方法区之中,格式上符合Java类型信息的要求,只有通过这个阶段字节流才会进入内存的方法区进行存储,后面3个阶段全部是基于方法区的存储结构进行的.
2.元数据验证.
对字节码描述的信息进行语义分析,主要是对类中的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息.
1)这个类是否有父类.
2)这个类是否继承了不允许被继承的类
3)如果这个类不是抽象类,是否实现了父类或者接口之中所有要实现的方法.
4)是否所有⽅法都遵守访问控制关键字的限定,protected、private 那些。 ⽅法调⽤的参数个数和类型是否正确。等等
3.字节码验证
主要目的是通过数据流和控制流,确定程序语义是否合法,符合逻辑.元数据验证主要对类的元数据信息中的数据类型验证,字节码验证对类的方法体进行校验分析,保证被校验类的方法在运行时不会危害JVM.
1)保证任意时刻操作数栈的数据类型与指令序列都能配合工作.
2)保证方法体中的类型转换是否有效的.
3)保证跳转指令不会跳转到方法体之外的字节码指令上.
4.符号引用验证
发生在JVM将符号引用转化为直接引用的时候,这个转化动作是在解析阶段发生.符号引用验证可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性校验.
1)符号引用中通过字符串的全限定名是否能找到对应的类.
2)符号引用指向的类,字段,方法的访问性是否可以被当前类访问.
符号引用验证目的确保解析阶段能正常执行
2.3.3准备阶段
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些变量使用的 内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的 内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆 中。
2.3.4解析阶段
解析 该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一 定在初始化动作完成之前,也有可能在初始化之后。
符号引⽤以⼀组符号(任何形式的字⾯量,只要在使⽤时能够⽆歧义的定位到⽬标即可)来描述所引⽤的⽬标。 在编译时,Java 类并不知道所引⽤的类的实际地址,因此只能使⽤符号引⽤来代替。⽐如 com.Wanger 类引⽤了 com.Chenmo 类,编译时 Wanger 类并不知道 Chenmo 类的实际内存地址,因此只能使⽤符号 com.Chenmo 。
直接引⽤通过对符号引⽤进⾏解析,找到引⽤的实际内存地址。
符号引⽤
定义:包含了类、字段、⽅法、接⼝等多种符号的全限定名。
特点:在编译时⽣成,存储在编译后的字节码⽂件的常量池中。
独⽴性:不依赖于具体的内存地址,提供了更好的灵活性。
直接引⽤
定义:直接指向⽬标的指针、相对偏移量或者能间接定位到⽬标的句柄。
特点:在运⾏时⽣成,依赖于具体的内存布局。
效率:由于直接指向了内存地址或者偏移量,所以通过直接引⽤访问对象的效率较⾼。
通过这种⽅式,Java 程序能够在编译时和运⾏时具有更⾼的灵活性和解耦性,同时在运⾏时也能获得更好的性能。
整个解析阶段主要做了下⾯⼏个⼯作: 类或接⼝的解析 类⽅法解析 接⼝⽅法解析 字段解析
2.3.5初始化阶段
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段 用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。
初始化阶段是执行类构造器<clinit>()方法的过程.
1) <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中语句自动合成并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定.如果类中没有静态语句快,变量赋值的操作,编译器可以不为这个类生成<clinit>()方法.接口中不能使用静态语句块,但仍然有变量赋值的操作,会生成<clinit>()方法.
2)JVM会保证子类的<clinit>()方法方法执行之前,父类的<clinit>()方法已经执行完毕,因此JVM第一个执行的<clinit>()方法肯定是Object.
3)父类<clinit>()方法先执行,因此父类的静态语句块优先于子类的静态语句块.
4)接口与类不同,执行接口<clinit>()方法方法不需要先执行父接口的<clinit>()方法,只有父接口的变量使用时,父接口才会初始化.
5)JVM会保证一个类的<clinit>()方法在多线程的环境下被正确的加锁,同步去完成.若有多个线程同时要去初始化这个类的<clinit>()方法,那么会只有一个线程会去初始化,其他线程进入阻塞状态.
2.4类加载器
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类 加载器。
对于任意⼀个类,都需要由它的类加载器和这个类本身⼀同确定其在 JVM 中的唯⼀性。也就是说,如果两个类的加 载器不同,即使两个类来源于同⼀个字节码⽂件,那这两个类就必定不相等(⽐如两个类的 Class 对象不 equals )。
提到类加载机制,不得不提的⼀个概念就是“双亲委派模型”。
站在Java虚拟机的⻆度来看,只存在两种不同的类加载器:
⼀种是启动类加载器(Bootstrap ClassLoader),这个类加载器使⽤C++语⾔实现,是虚拟机⾃⾝的⼀部分;
另外⼀种就是其他所有的 类加载器,这些类加载器都由Java语⾔实现,独⽴存在于虚拟机外部,并且全都继承⾃抽象类 java.lang.ClassLoader。
站在Java开发⼈员的⻆度来看,类加载器就应当划分得更细致⼀些。⾃JDK1.2以来,Java⼀直保 持着三层类加载器、双亲委派的类加载架构器。
什么是双亲委派模型?
双亲委派模型(Parent Delegation Model)是 Java 类加载器使⽤的⼀种机制,⽤于确保 Java 程序的稳定性和安 全性。
如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给 ⽗类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层 的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆法完成这个加载请求(它的搜索范围中没有找到所需 的类)时,⼦加载器才会尝试⾃⼰去完成加载。
①、引导类加载器(Bootstrap ClassLoader):负责加载 JVM 基础核⼼类库,如 rt.jar、sun.boot.class.path 路 径下的类。
②、扩展类加载器(Extension ClassLoader):负责加载 Java 扩展库中的类,例如 jre/lib/ext ⽬录下的类或由系 统属性 java.ext.dirs 指定位置的类。
③、系统(应⽤)类加载器(System ClassLoader):负责加载系统类路径 java.class.path 上指定的类库,通常 是你的应⽤类和第三⽅库。
④、⽤户⾃定义类加载器:Java 允许⽤户创建⾃⼰的类加载器,通过继承 java.lang.ClassLoader 类的⽅式实现。 这在需要动态加载资源、实现模块化框架或者特殊的类加载策略时⾮常有⽤。
双亲委派模型的优点
1. 避免重复加载类:⽐如A类和B类都有⼀个⽗类C类,那么当A启动时就会将C类加载起来,那 么在B类进⾏加载时就不需要在重复加载C类了。
2. 安全性:使⽤双亲委派模型也可以保证了Java的核⼼API不被篡.
例如类 j a v a . l a n g . O b j e c t,它存放在 rt . j a r 之中,通过双亲委派机制,保证最终都是委派给处于模型 最顶端的启动类加载器进⾏加载,保证 O b j e c t 的⼀致。
如果没有使⽤双亲委派模 型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的Object类,⽽有些Object类⼜是⽤⼾⾃ ⼰提供的因此安全性就不能得到保证了。
破坏双亲委派模型
第一次破坏
双亲委派模型的第⼀次“被破坏”其实发⽣在双亲委派模型出现之前——即 JDK 1.2 ⾯世以前的“远古”时代
由于双亲委派模型在 JDK 1.2 之后才被引⼊,但是类加载器的概念和抽象类java.lang.Class. Loader 则在 Java 的第⼀个版本中就已经存在,Class.Loader类中的抽象方法loadClass,它是类加载器 的对外统一的接口,内部强制实现了双亲委派模型,为了向下兼容旧代码,所以⽆法以技术⼿段避免 loadClass()被⼦类覆盖的可能性.这就破坏了双亲委派模型.
因为如果子类覆盖loadClass 可,能会在loaClass里面直接调用自己自定义的类加载器,而不是先交给父类加载器这就导致双亲委派模型失效,可能会使同一个类被不同的类加载器重复加载引发运行时异常等.
为了解决,只能在 JDK 1.2 之后的 java.lang.ClassLoader 中添加⼀个新的 protected ⽅法 findClass(),并引导⽤户编写的类加载逻辑时尽可能去重写这个⽅法,⽽不是在 loadClass()中编写代码。loadClass被调用时,首先会检查该类是否被加载过,没有加载,就会递归调用父类加载器.但如果父类加载器找不到,就会调用子类重写的findClass方法.由此重写的findClass()方法并不会改变递归调用父类加载器的过程,不管内部如何实现不会绕过父类加载器直接执行,保证双亲委派模型.
第二次破坏
双亲委派模型很好地解决了各个类加载器的基础类的统一问题(越基础的类越有上层类加载器加载),但如果基础类又要调用用户代码,那该怎么办?
各个⼚商各有不同的 JDBC 的实现,Java 在核⼼包\lib⾥定义了对应的 SPI,那么这个就毫⽆疑问由启动类加载器加载器加载。但是各个⼚商的实现,是没办法放在核⼼包⾥的,只能放在classpath⾥,只能被应⽤类加载器加载。那么,问题来了,启动类加载器它就加载不到⼚商提供的 SPI 服务代码。
这样"线程上下文类加载器"就产生了,它可以通过java.lang.Thread类setContextClassLoaser()方法进行设置,有了线程上下文类加载,在调⽤具体的类实现时,使⽤的是⼦类加载器 (线程上下⽂加载器Thread.currentThread().getContextClassLoader)来加载具体的数据库数据库 包(如mysql的jar包)..也就是父类加载器请求子类加载器去完成类加载的动作,也就破坏了双亲委派模型的一般性原则.
第三次破坏
双亲委派模型的第三次“被破坏”是由于⽤户对程序动态性的追求⽽导致的,例如代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。
OSGi 实现模块化热部署的关键是它⾃定义的类加载器机制的实现,每⼀个程序模块(OSGi 中称为 Bundle)都有⼀个⾃⼰的类加载器,当需要更换⼀个 Bundle 时,就把 Bundle 连同类加载器⼀起换掉以实现代码的热替换。在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,⽽是进⼀步发展为更加复杂的⽹状结构。
2.5运行时数据区
Java 源代码⽂件经过编译器编译后会⽣成字节码⽂件,经过加载器加载完毕后会交给执⾏引擎执 ⾏。在执⾏的过程中,JVM 会划出来⼀块空间来存储字节码指令信息和程序执⾏期间需要⽤到的数据,这块空间⼀般被称为运⾏时数 据区.
程序计数器
程序计数器(Program Counter Register)所占的内存空间不⼤,很⼩很⼩⼀块,可以看作是当前线程所执⾏的字 节码指令的⾏号指示器。字节码解释器会在⼯作的时候改变这个计数器的值来选取下⼀条需要执⾏的字节码指令, 像分⽀、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
在 JVM 中,多线程是通过线程轮流切换来获得 CPU 执⾏时间的,因此,在任⼀具体时刻,⼀个 CPU 的内核只会执 ⾏⼀条线程中的指令,因此,为了线程切换后能恢复到正确的执⾏位置,每个线程都需要有⼀个独⽴的程序计数 器,并且不能互相⼲扰,否则就会影响到程序的正常执⾏次序。 也就是说,我们要求程序计数器是线程私有的。
如果虚拟机中的当前线程执行的是 Java 的普通方法,那么 PC 寄存器中存储的是方法的第一条指令,当方法开始执行之后, PC 寄存器存储的是下一个字节码指令的地址。
如果虚拟机中的当前线程执行的是 native 方法,那么 PC 寄存器中的值为 undefined。
如果遇到判断分支、循环以及异常等不同的控制转移语句,PC 寄存器会被置为目标字节码指令的地址。
虚拟机栈
Java 虚拟机栈(JVM 栈)中是⼀个个栈帧,每个栈帧对应⼀个被调⽤的⽅法。当线程执⾏⼀个⽅法时,会创建⼀个 对应的栈帧,并将栈帧压⼊栈中。当⽅法执⾏完毕后,将栈帧从栈中移除
栈帧(Stack Frame)是运⾏时数据区中⽤于⽀持虚拟机进⾏⽅法调⽤和⽅法执⾏的数据结构。每⼀个⽅法从调⽤ 开始到执⾏完成,都对应着⼀个栈帧在虚拟机栈/本地⽅法栈⾥从⼊栈到出栈的过程。
每⼀个栈帧都包括了局部变量表、操作数栈、动态链接、⽅法返回地址和⼀些额外的附加信息。 在编译程序代码时,栈帧中需要多⼤的局部变量表,多深的操作数栈都已经完全确定了,并且写⼊到⽅法表的 Code 属性之中。
1.局部变量表
局部变量表(Local Variables Table)⽤来保存⽅法中的局部变量,以及⽅法参数。当 Java 源代码⽂件被编译成 class ⽂件的时候,局部变量表的最⼤容量就已经确定了。
看这段代码,在hello()中,有一个参数a,和一个变量b
public class Hello {public void hello(int a) {int b = 2;}public static void main(String[] args) {System.out.println("Hello World");}
}
用jclasslib工具查看一下编译后的字节码文件的LocalVaraiablesTable.class。可以看到 write() ⽅法的 Code 属性中,Maximum local variables(局部变量表的最⼤容量)的值为 3。
当⼀个成员⽅法(⾮静态⽅法)被调⽤时,第 0 个变量其实是调⽤这个成员⽅法的对象引⽤,也就是那个⼤名鼎鼎 的 this。调⽤⽅法 write(18) ,实际上是调⽤ write(this, 18) 。 点开 Code 属性,查看 LocalVaraiableTable 就可以看到详细的信息了。静态方法中没有this,因为静态方法由类来调用.如图
当然了,局部变量表的⼤⼩并不是⽅法中所有局部变量的数量之和,它与变量的类型和变量的作⽤域有关。当⼀个 局部变量的作⽤域结束了,它占⽤的局部变量表中的位置就被接下来的局部变量取代了.
局部变量表的容量以槽(slot)为最⼩单位,⼀个槽可以容纳⼀个 32 位的数据类型(⽐如说 int,当然了, 《Java 虚拟机规范》中没有明确指出⼀个槽应该占⽤的内存空间⼤⼩,但我认为这样更容易理解),像 float 和 double 这种明确占⽤ 64 位的数据类型会占⽤两个紧挨着的槽。
操作数栈
同局部变量表⼀样,操作数栈(Operand Stack)的最⼤深度也在编译的时候就确定了,被写⼊到了 Code 属性的 maximum stack size 中。当⼀个⽅法刚开始执⾏的时候,操作数栈是空的,在⽅法执⾏过程中,会有各种字节码 指令往操作数栈中写⼊和取出数据,也就是⼊栈和出栈操作。
动态链接
每个栈帧都包含了⼀个指向运⾏时常量池中该栈帧所属⽅法的引⽤,持有这个引⽤是为了⽀持⽅法调⽤过程中的动 态链接(Dynamic Linking)。
⽅法区是 JVM 的⼀个运⾏时内存区域,属于逻辑定义,不同版本的 JDK 都有不同的实现, 但主要的作⽤就是⽤于存储已被虚拟机加载的类信息、常量、静态变量,以及即时编译器编译后的代码等。
运⾏时常量池(Runtime Constant Pool)是⽅法区的⼀部分,⽤于存放编译期⽣成的各种字⾯量和符号引⽤ ——在类加载后进⼊运⾏时常量池。
从⾯向对象编程的⻆度,从多态的⻆度,我们多态的原理和运⾏结果是很好理解的,但站在 Java 虚拟机的⻆度,它是如何判 断调用的是子类的方法还是父类的方法?
还得从 invokevirtual 这个指令着⼿,看它是如何实现多态的。根据《Java 虚拟机规范》,invokevirtual 指令 在运⾏时的解析过程可以分为以下⼏步:
①、找到操作数栈顶的元素所指向的对象的实际类型,记作 C。
②、如果在类型 C 中找到与常量池中的描述符匹配的⽅法,则进⾏访问权限校验,如果通过则返回这个⽅法 的直接引⽤,查找结束;否则返回 java.lang.IllegalAccessError 异常。
③、否则,按照继承关系从下往上⼀次对 C 的各个⽗类进⾏第⼆步的搜索和验证。
④、如果始终没有找到合适的⽅法,则抛出 java.lang.AbstractMethodError 异常。
也就是说,invokevirtual 指令在第⼀步的时候就确定了运⾏时的实际类型,所以两次调⽤中的 invokevirtual 指令 并不是把常量池中⽅法的符号引⽤解析到直接引⽤上就结束了,还会根据⽅法接受者的实际类型来选择⽅法版本, 这个过程就是 Java 重写的本质。我们把这种在运⾏期根据实际类型确定⽅法执⾏版本的过程称为动态链接。
方法的返回地址
⽅法返回地址:记录⽅法结束后控制流应返回的位置。
⽅法退出的过程实际上等同于把当前栈帧出栈,因此接下来可能执⾏的操作有:恢复上层⽅法的局部变量表和操作 数栈,把返回值(如果有的话)压⼊调⽤者栈帧的操作数栈中,调整 PC 计数器的值,找到下⼀条要执⾏的指令 等。
本地方法栈
本地⽅法栈(Native Method Stack)与 Java 虚拟机栈类似,只不过 Java 虚拟机栈为虚拟机执⾏ Java ⽅法服务, ⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。
堆
堆是所有线程共享的⼀块内存区域,在 JVM 启动的时候创建,⽤来存储对象(数组也是⼀种对象)。 以前,Java 中“⼏乎”所有的对象都会在堆中分配,但随着 JIT 编译器的发展和逃逸技术的逐渐成熟,所有的对象都 分配到堆上渐渐变得不那么“绝对”了。从 JDK 7 开始,Java 虚拟机已经默认开启逃逸分析了,意味着如果某些⽅法 中的对象引⽤没有被返回或者未被外⾯使⽤(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
逃逸分析(Escape Analysis)是⼀种编译器优化技术,⽤于判断对象的作⽤域和⽣命周期。如果编译器确定⼀个对 象不会逃逸出⽅法或线程的范围,它可以选择在栈上分配这个对象,⽽不是在堆上。这样做可以减少垃圾回收的压 ⼒,并提⾼性能.
同步消除 线程同步本⾝是⼀个相对耗时的过程,如果逃逸分析能够确定⼀个变量不会逃逸出线程,⽆法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除
掉。
.对象创建的过程了解吗?
在 JVM 中对象的创建,我们从⼀个 new 指令开始:
⾸先检查这个指令的参数是否能在常量池中定位到⼀个类的符号引⽤
检查这个符号引⽤代表的类是否已被加载、解析和初始化过。如果没有,就先执⾏相应的类加载过程
类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。
内存分配完成之后,虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值。
接下来设置对象头,请求头⾥包含了对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。
能说⼀下对象的内存布局吗?
在 HotSpot 虚拟机⾥,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头主要由两部分组成:
第⼀部分存储对象⾃⾝的运⾏时数据:哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,官⽅称它为 Mark Word,它是个动态的结构,随着对象状态变化。
第⼆部分是类型指针,指向对象的类元数据类型(即对象代表哪个类)。
此外,如果对象是⼀个 Java 数组,那还应该有⼀块⽤于记录数组长度的数据
实例数据⽤来存储对象真正的有效信息,也就是我们在程序代码⾥所定义的各种类型的字段内容,⽆论是从⽗类继承的,还是⾃⼰定义的。
对象怎么定位?
java 程序会通过栈上的 reference 数据来操作堆上的具体对象。由于 reference 类型在《Java 虚拟机规范》⾥⾯只规定了它是⼀个指向对象的引⽤,并没有定义这个引⽤应该通过什么⽅式去定位、访问到堆中对象的具体位置,所以对象访问⽅式也是由虚拟机实现⽽定的,主流的访问⽅式主要有使⽤句柄和直接指针两种:
如果使⽤句柄访问的话,Java 堆中将可能会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃具体的地址信息
如果使⽤直接指针访问的话,Java 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 中存储的直接就是对象地址,如果只是访问对象本⾝的话,就不需要多⼀次间接访问的开销,如图所⽰:
元空间和方法区
⽅法区是 Java 虚拟机规范上的⼀个逻辑区域,在不同的 JDK 版本上有着不同的实现。在 JDK 7 的时候,⽅法区被 称为永久代(PermGen),⽽在 JDK 8 的时候,永久代被彻底移除,取⽽代之的是元空间。(这里直接借用二哥的图)
JDK 7 之前,只有常量池的概念,都在⽅法区中。
JDK 7 的时候,字符串常量池从⽅法区中拿出来放到了堆中,运⾏时常量池还在⽅法区中(也就是永久代中)。
JDK 8 的时候,HotSpot 移除了永久代,取⽽代之的是元空间。字符串常量池还在堆中,⽽运⾏时常量池跑到了元 空间。
元空间的⼤⼩不再受限于 JVM 启动时设置的最⼤堆⼤⼩,⽽是直接利⽤本地内存,也就是操作系统的内存。有效地 解决了 OutOfMemoryError 错误.
运行时常量池
就是在运⾏时期间,JVM 会将字节码⽂件中的常量池加载到内存中,存放在运⾏时常量 池中。 也就是说,常量池是在字节码⽂件中,⽽运⾏时常量池在元空间当中(JDK 8 及以后),讲的是⼀个东⻄,但形态 不⼀样,就好像⼀个是固态,⼀个是液态;或者⼀个是模⼦,⼀个是模⼦⾥的锅碗瓢盆。
它包括了编译器可知的数值字⾯ 量,以及运⾏期解析后才能获得的⽅法或字段的引⽤。简⽽⾔之,当⼀个⽅法或者变量被引⽤时,JVM 通过 运⾏时常量区来查找⽅法或者变量在内存⾥的实际地址。
执行引擎
JVM执行引擎包括解释器和JIT编译器.Java 代码⾸先被编译为字节码,JVM 在运⾏时通过解释器逐⾏解释字节码指令并执⾏,在解释执⾏的过程中,JVM 会对程序运⾏时的信息进⾏收集,在这些信 息的基础上,JIT 会逐渐发挥作⽤,它会把字节码编译成机器码,但不是所有的代码都会被编译,只有被 JVM 认定 为热点代码,才会被编译,,以此来提⾼程序的执⾏效率。
那为什么 JIT 就能提⾼程序的执⾏效率呢,解释器不也是将字节码翻译为机器码交给操作系统执⾏吗?
解释器在执⾏字节码文件的时,对于每⼀条字节码指令,都需要进⾏⼀次解释过程,然后执⾏相应的机器指令。这个过程不管代码对应的字节码指令以前是否执行过,他都会再次执行,因为解释器不会记住之前的解释结果。
与此相对,JIT 会将频繁执⾏的字节码编译成机器码。这个过程只发⽣⼀次。⼀旦字节码被编译成机器码,之后每 次执⾏这部分代码时,直接执⾏对应的机器码,⽆需再次解释。 除此之外,JIT 能够在运⾏时根据实际 情况对代码进⾏优化(如内联、循环展开、分⽀预测优化等),这些优化是在机器码级别上进⾏的,因此JIT ⽣成的机器码更接近底层,能够更有效地利⽤ CPU 和内存等资源,可以显著提升 执⾏效率。
怎么样才会被认为是热点代码呢?
JVM有一个阈值,当方法或者代码块在一定时间内调用的次数超过这个阈值时就会被认为是热点代码,然后编译存⼊ codeCache 中(在运行时常量池中)。当下次执⾏时,再遇到这段代码,就会从 codeCache 中直接读取机器码,然后执⾏,以此 来提升程序运⾏的性能。如图:
JIT的编译优化
即时编译器会对正在运⾏的程序进⾏⼀系列优化,包括:
1)字节码解析过程中的分析
2)根据编译过程中代码的⼀些中间形式来做局部优化
3)根据程序依赖图进⾏全局优化
最后才会⽣成机器码。
逃逸分析
逃逸分析是 JIT ⽤于优化内存管理和同步操作的重要技术。通过分析对象是否逃逸到⽅法或线程的外部,编译器可 以做出更智能的存储和同步决策。 逃逸分析通常是在⽅法内联的基础上进⾏的,JIT 可以根据逃逸分析的结果进⾏诸如锁消除、栈上分配以及标量替 换的优化。
三.垃圾回收机制
垃圾回收的概念
垃圾回收是对堆内存中的已经死亡的或者长时间没用的对象进行清除和回收,释放垃圾所占用的空间,防止内存的爆掉.
3.1 那我们怎么知道哪些对象是否是垃圾?有哪些算法用来确定呢?
有引用计数法,可达性分析算法.
3.1.1引用计数法
引用计数算法 就是在对象头中分配一个空间来保存该对象被引用的次数.
如果该对象被其它对象引⽤,则它的引⽤计数加 1,如果删除对该对象的引⽤,那么它的引⽤计数就减 1,当该对象的引⽤计数为 0 时,那么该对象就会被回收。
引⽤计数算法将垃圾回收分摊到整个应⽤程序的运⾏当中,⽽不是集中在垃圾收集时。因此,采⽤引⽤计数的垃圾 收集不属于严格意义上的"Stop-The-World"的垃圾收集机制.
引用计数算法有一个很严重的问题,就是不能解决循环引用的问题,当两个对象中的一些字段相互引用的时候,即使在外部引用这两个对象的所有引用都删除了,这两个对象也不会认为是垃圾,因为它们相互引⽤着对⽅,导致它们的引⽤计数永远都不会为 0,也就永远无法通知GC收集器进行回收.
3.1.2可达性分析算法
可达性分析算法 是通过一些称为GC Root 的对象作为起点,然后向下搜索,搜索走过的路径被称为引用链,当一个对象到 GC Roots 之间没有任何引用相连时,即从 GC Roots 到该对象节点不可达,则证明该对象是需要垃圾收集的。
通过可达性算法,成功解决了引⽤计数⽆法解决的问题-“循环依赖”,只要你⽆法与 GC Root 建⽴直接或间接的连 接,系统就会判定你为可回收对象。
作为GCRoot对象有四种,分别是 JVM栈中引用的对象,本地方法栈中引用的对象,类静态变量引用的对象,常量引用的对象.
JVM栈引用的对象
当局部变量不再指向任何对 象,或者变量本身离开了作⽤域,它指向的对象就可以被视为垃圾回收的候选对象.
本地方法栈中引用的对象
我们的java方法中调用本地方法时,如果传递一个对象到本地方法中,即使这个这个方法完成了对这个对象的使用,但只要本地方法还在执行且持有该对象的引用,这个对象就不可回收.
3.2 Stop The World
"Stop The World"是 Java 垃圾收集中的⼀个重要概念。在垃圾收集过程中,JVM 会暂停所有的⽤户线程,这种暂 停被称为"Stop The World"事件。
这么做的主要原因是为了防⽌在垃圾收集过程中,⽤户线程修改了堆中的对象,导致垃圾收集器⽆法准确地收集垃 圾。"Stop The World"事件会对 Java 应⽤的性能产⽣影响。如果停顿时间过⻓,就会导致应⽤的响应 时间变⻓,对于对实时性要求较⾼的应⽤,如交易系统、游戏服务器等,这种情况是不能接受的。
总的来说,"Stop The World"是 Java 垃圾收集中必须⾯对的⼀个挑战,其⽬标是在保证内存的有效利⽤和应⽤的 响应性能之间找到⼀个平衡。
3.3 垃圾清除
知道了那些对象是垃圾之后,,垃圾收集器要做的事情就是进⾏垃圾回收,但是这⾥⾯涉及到⼀个问题是:如何 ⾼效地进⾏垃圾回收.
3.3.1 标记清除算法
首先先把内存区域中的对象进行标记,如哪些是可回收垃圾,哪些是不可回收垃圾,哪些是空闲区域.然后对垃圾进行清除,但它存在 ⼀个很⼤的问题,那就是内存碎⽚。碎⽚太多可能会导致当程序运⾏过程中需要分配较⼤对象时,因⽆法找到⾜够 的连续内存⽽不得不提前触发新⼀轮的垃圾收集.
3.3.2复制算法
它将可⽤内存按 容量划分为⼤⼩相等的两块,每次只使⽤其中的⼀块。 当这⼀块的内存⽤完了,就将还存活着的对象复制到另外⼀块上⾯,然后再把已使⽤过的内存空间⼀次清理掉。这 样就保证了内存的连续性,逻辑清晰,运⾏⾼效。(简单来说 , 将内存区域划分成等大的两份,一份用来存储对象,一份在当作空闲区域,在进行垃圾清除的时候,用来存储对象的那份内存区域中不可回收的对象复制到空闲区域的那份内存中,两者相互互换.)
但复制算法也存在⼀个很明显的问题,合着我这 190 平的⼤四室,只能当 90 平⽶的⼩两室来居住?代价实在太 ⾼。
3.3.3标记整理算法
标记整理算法(Mark-Compact),标记过程仍然与标记清除算法⼀样,但后续步骤不是直接对可回收对象进⾏清 理,⽽是让所有存活的对象都向⼀端移动,再清理掉端边界以外的内存区域。
标记整理算法⼀⽅⾯在标记-清除算法上做了升级,解决了内存碎⽚的问题,也规避了复制算法只能利⽤⼀半内存 区域的弊端。看起来很美好,但内存变动更频繁,需要整理所有存活对象的引⽤地址,在效率上⽐复制算法差很 多。
3.3.4分代收集算法
分代收集算法是融合上述 3 种基础的算法思想, ⽽产⽣的针对Java堆中各个年代的特点采用最合适的算法.
由于对象存活的周期不同就将java堆中的内存区域划分为新生代和老年代.
在新⽣代中,每次垃圾收集时都发现有⼤批对象死去,只有少量存活,那就选⽤复制算法,只需要付出少量存活对 象的复制成本就可以完成收集。
⽼年代中因为对象存活率⾼、没有额外空间对它进⾏分配担保,就必须使⽤标记清理或者标记整理算法来进⾏回 收。
Eden区
有将近 98% 的对象是朝⽣夕死,所以针对这⼀现状,⼤多数情况下,对象会在新⽣ 代 Eden 区中进⾏分配,当 Eden 区没有⾜够空间进⾏分配时,JVM 会发起⼀次 Minor GC,Minor GC 相⽐ Major GC 更频繁,回收速度也更快。
这里讲一下Minor GC 和Major GC是什么有什么区别!
Minor GC 和Major GC是JVM垃圾回收机制中俩种关键的垃圾回收类型,主要区别在与作用区域和触发条件.
Minor GC 它是在Eden区没有足够内存进行分配时触发,仅清理E新生代(包括Eden区和Survivor区),频率很高,速度快,采用复制算法,将存活的对象复制到Survivor或者老年代,有时会引发短暂的STW暂停.
Major GC 作用区域是通常指清理Old区,有时又指Full GC清理整个堆和元空间.它会在老年代空间不足,元空间不足或者堆内存分配失败,和显示调用System.gc()时触发.它的频率低,耗时长,STW的时间也会很长,对性能影响显著.
Survivor区
Survivor区相当于是新生代和老年代的一个缓冲区.
但为什么Eden区要划分内存给Survivor区,为啥要这么复杂划分Eden区?
如果没有 Survivor 区,Eden 区每进⾏⼀次 Minor GC,存活的对象就会被送到⽼年代,⽼年代很快就会被填满。 ⽽有很多对象虽然⼀次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第⼆次,第三次就需要被清除.
Survivor 的存在意义就是减少被送到⽼年代的对象,进⽽减少 Major GC 的发⽣。Survivor 的预筛选保证, 只有经历 16 次 Minor GC 还能在新⽣代中存活的对象,才会被送到⽼年代。
Survivor 区为什么要划分成两个区from 和 to 这两个区.
设置两个 Survivor 区最⼤的好处就是解决内存碎⽚化.先假设Survivor只有一个区的话会怎么样?
Minor GC 执⾏后,Eden 区被清空,存活的对象放到了 Survivor 区,⽽之前 Survivor 区中的对象,可能也有⼀些 是需要被清除的。那么问题来了,这时候我们怎么清除它们? 在这种场景下,我们只能标记清除,⽽我们知道标记清除最⼤的问题就是内存碎⽚,在新⽣代这种经常会消亡的区 域,采⽤标记清除必然会让内存产⽣严重的碎⽚化。
我们可以借用复制算法,将Survivor区划分成两块,每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。再次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以 此反复,这样就永远有⼀个 Survivor space 是空的,另⼀个⾮空的 Survivor space 是⽆ 碎⽚的。
Old区
⽼年代占据着 2/3 的堆内存空间,只有在 Major GC 的时候才会进⾏清理,每次 GC 都会触发“Stop-The-World”。 内存越⼤,STW 的时间也越⻓,所以内存也不仅仅是越⼤就越好。
除了上述所说,在内存担保机制下,⽆法安置的对象会直接进到⽼年代,以下⼏种情况也会进⼊⽼年代。
1.大对象
⼤对象指需要⼤量连续内存空间的对象,这部分对象不管是不是“朝⽣夕死”,都会直接进到⽼年代。这样做主要是 为了避免在 Eden 区及 2 个 Survivor 区之间发⽣⼤量的内存复制。
2.长期存活的对象
虚拟机给每个对象定义了⼀个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区 之间移动,对象在 Survivor 区中每经历⼀次 Minor GC,年龄就增加 1 岁。当年龄增加到 15 岁时,这时候就会被 转移到⽼年代。
3.动态对象年龄
J VM 并不强制要求对象年龄必须到 15 岁才会放⼊⽼年区,如果 Survivor 空间中某个年龄段的对象总⼤⼩超过了 Survivor 空间的⼀半,那么该年龄段及以上年龄段的所有对象都会在下⼀次垃圾回收时被晋升到⽼年代,⽆需等你 “成年”。
4.空间分配担保
假如在 Young GC 之后,新⽣代仍然有⼤量对象存活,就需要⽼年代进⾏分配担保,把 Survivor ⽆法容纳的对象直接送⼊⽼年代。
3.4垃圾收集器
JVM 的垃圾收集器主要分为两⼤类:分代收集器和分区收集器,分代收集器的代表是 CMS,分区收 集器的代表是 G1 和 ZGC.
3.4.1CMS收集器
C MS收集器以获取最短回收停顿时间为⽬标,采⽤“标记-清除”算法,分 4 ⼤步进⾏垃圾收集,其中初始标记和重新标记会 STW,JDK 1.5 时引⼊,JDK9 被标记弃⽤,JDK14 被移除.
它是第一个关注减少GC时间(STW的时间)的收集器.
CMS 垃圾收集器之所以能够实现对 GC 停顿时间的控制,其本质来源于对「可达性分析算法」的改进,即三⾊标记 算法。CMS 垃圾收集器通过三⾊标记算法,实现了垃圾回收线程与⽤户线程的并发执⾏,从⽽极⼤地降低了系统响应时 间,提⾼了强交互应⽤程序的体验。它的运⾏过程分为 4 个步骤,包括:
1)初始标记. 2)并发标记 3)重新标记 4)标记清除.
初始标记,指的是寻找所有被 GCRoots 引⽤的对象,该阶段需要「Stop the World」。这个步骤仅仅只是标记⼀ 下 GC Roots 能直接关联到的对象,并不需要做整个引⽤的扫描,因此速度很快。
并发标记,指的是对「初始标记阶段」标记的对象进⾏整个引⽤链的扫描,该阶段不需要「Stop the World」。 对 整个引⽤链做扫描需要花费⾮常多的时间,因此通过垃圾回收线程与⽤户线程并发执⾏,可以降低垃圾回收的时 间。 这也是 CMS 能极⼤降低 GC 停顿时间的核⼼原因.
但这也带来了⼀些问题,即:并发标记的时候,引⽤可能发⽣ 变化,因此可能发⽣漏标(本应该回收的垃圾没有被回收)和多标(本不应该回收的垃圾被回收)了
重新标记,指的是对「并发标记」阶段出现的问题进⾏校正,该阶段需要「Stop the World」。正如并发标记阶段 说到的,由于垃圾回收算法和⽤户线程并发执⾏,虽然能降低响应时间,但是会发⽣漏标和多标的问题。所以对于 CMS 来说,它需要在这个阶段做⼀些校验,解决并发标记阶段发⽣的问题。
并发清除,指的是将标记为垃圾的对象进⾏清除,该阶段不需要「Stop the World」。 在这个阶段,垃圾回收线程 与⽤户线程可以并发执⾏,因此并不影响⽤户的响应时间。
缺点:
1)对 CPU 资源⾮常敏感,因此在 CPU 资源紧张的情况下,CMS 的性能会⼤打折扣。
2)CMS 采⽤的是「标记-清除」算法,会产⽣⼤量的内存碎⽚,导致空间不连续,当出现⼤对象⽆法找到连续的 内存空间时,就会触发⼀次 Full GC,这会导致系统的停顿时间变⻓。
3.4.2 G1收集器
在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。G1 有五个属性:分代、增量、并⾏、标记整理、STW。
1)分代:它将堆内存分为 多个⼤⼩相等的区域(Region),每个区域都可以是 Eden 区、Survivor 区或者 Old 区。G1 有专⻔分配⼤对象的 Region 叫 Humongous 区,⽽不是让⼤对象直接进⼊⽼年代的 Region 中。
G1 有专⻔分配⼤对象的 Region 叫 Humongous 区,⽽不是让⼤对象直接进⼊⽼年代的 Region 中。
2)增量: G1 可以以增量⽅式执⾏垃圾回收,这意味着它不需要⼀次性回收整个堆空间,⽽是可以逐步、增量地 清理。有助于控制停顿时间,尤其是在处理⼤型堆时。
3)并行:G1 垃圾回收器可以并⾏回收垃圾,这意味着它可以利⽤多个 CPU 来加速垃圾回收的速度,这⼀特性在 年轻代的垃圾回收(Minor GC)中特别明显,因为年轻代的回收通常涉及较多的对象和较⾼的回收速率。
4)标记整理: 在进⾏⽼年代的垃圾回收时,G1 使⽤标记-整理算法。这个过程分为两个阶段:标记存活的对象和 整理(压缩)堆空间。通过整理,G1 能够避免内存碎⽚化,提⾼内存利⽤率。
5)STW::G1 也是基于「标记-清除」算法,因此在进⾏垃圾回收的时候,仍然需要「Stop the World」。不过, G1 在停顿时间上添加了预测机制,⽤户可以指定期望停顿时间。
简述java的垃圾回收机制
在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而 是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低 优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当 前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象, 并将它们添加到要回收的集合中,进行回收。
垃圾回收的优点和原理。
使得 Java 程序员在 编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制, Java 中的对象不再有“作用域”的概念,只有对象的引用才有" 作用域"。垃圾回收可以有效的防止内存泄露,有效的使用可以使 用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行, 不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的 对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对 象或所有对象进行垃圾回收。
内存溢出和内存泄漏是什么意思?
内存泄露就是申请的内存空间没有被正确释放,导致内存被⽩⽩占⽤。内存溢出就是申请的内存超过了可⽤内存,内存不够了。
两者关系:内存泄露可能会导致内存溢出。
12.内存泄漏可能由哪些原因导致呢?
1.静态集合类引起内存泄漏
静态集合的⽣命周期和 JVM ⼀致,所以静态集合引⽤的对象不能被释放。
2.创建的连接不再使⽤时,需要调⽤ close ⽅法关闭连接,只有连接被关闭后,GC 才会回收对应的对象
(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,⽆法被 GC 回收
3对象 Hash 值改变,使⽤ HashMap、HashSet 等容器中时候,由于对象修改之后的 Hah 值和存储进容器时的 Hash 值不同,所以⽆法找到存⼊的对象,⾃然也⽆法单独删除了,这也会造成内存泄漏。说句题外话,这也是为什么 String 类型被设置成了不可变类型
对象的引用有哪几种?
Java 中的引⽤有四种,分为强引⽤(Strongly Reference)、软引⽤(Soft Reference)、弱引⽤(Weak Reference)和虚引⽤(Phantom Reference)4 种,这 4 种引⽤强度依次逐渐减弱。
强引⽤是最传统的引⽤的定义,是指在程序代码之中普遍存在的引⽤赋值,⽆论任何情况下,只要强引⽤关系还存在,垃圾收集器就永远不会回收掉被引⽤的对象。
软引⽤是⽤来描述⼀些还有⽤,但⾮必须的对象。只被软引⽤关联着的对象,在系统将要发⽣内存溢出异常前,会把这些对象列进回收范围之中进⾏第⼆次回收,如果这次回收还没有⾜够的内存, 才会抛出内存溢出异常。在 JDK 1.2 版之后提供了 SoftReference 类来实现软引⽤。
弱引⽤也是⽤来描述那些⾮必须对象,但是它的强度⽐软引⽤更弱⼀些,被弱引⽤关联的对象只能⽣存到下⼀次垃圾收集发⽣为⽌。当垃圾收集器开始⼯作,⽆论当前内存是否⾜够,都会回收掉只被弱引⽤关联的对象。在 JDK 1.2 版之后提供了 WeakReference 类来实现弱引⽤。
虚引⽤也称为“幽灵引⽤”或者“幻影引⽤”,它是最弱的⼀种引⽤关系。⼀个对象是否有虚引⽤的
存在,完全不会对其⽣存时间构成影响,也⽆法通过虚引⽤来取得⼀个对象实例。为⼀个对象设置虚引⽤关联的唯⼀⽬的只是为了能在这个对象被收集器回收时收到⼀个系统通知。在 JDK 1.2 版之后提供了 PhantomReference 类来实现虚引⽤。
FULLGC触发的条件
Young GC 之前检查⽼年代:在要进⾏ Young GC 的时候,发现⽼年代可⽤的连续内存空间 <新⽣代历次Young GC后升⼊⽼年代的对象总和的平均⼤⼩,说明本次 Young GC 后可能升⼊⽼年代的对象⼤⼩,可能超过了⽼年代当前可⽤内存空间,那就会触发 Full GC。
Young GC 之后⽼年代空间不⾜:执⾏ Young GC 之后有⼀批对象需要放⼊⽼年代,此时⽼年代就是没有⾜够的内存空间存放这些对象了,此时必须⽴即触发⼀次 Full GC
⽼年代空间不⾜,⽼年代内存使⽤率过⾼,达到⼀定⽐例,也会触发 Full GC。
空间分配担保失败( Promotion Failure),新⽣代的 To 区放不下从 Eden 和 From 拷贝过来对象,或者新⽣代对象 GC 年龄到达阈值需要晋升这两种情况,⽼年代如果放不下的话都会触发Full GC。
⽅法区内存空间不⾜:如果⽅法区由永久代实现,永久代空间不⾜ Full GC。
System.gc()等命令触发:System.gc()、jmap -dump 等命令会触发 full gc。
相关文章:
JVM详解
目录 一.JVM的概念 1. 什么是JVM? 2.JVM用来干什么? 二JVM运行流程 JVM执⾏流程 2.1类加载机制 2.2类加载机制带来了哪些好处? 2.3类加载的过程是什么? 2.3.1加载 2.3.2验证 2.3.3准备阶段 2.3.4解析阶段 符号引⽤ 直接引⽤ 2.3.5初始化阶段 2.4类加载器 什么…...
PCA(主成分分析)核心原理
一、PCA(主成分分析)核心原理 即主成分分析技术,又称主分量分析技术,旨在利用降维的思想,把多指标转化为少数几个综合指标。在统计学中,主成分分析PCA是一种简化数据集的技术。它是一个线性变换。这个变换…...
DeepSeek私有化部署6:openEuler 24.03-LTS-SP1安装Open WebUI
Open WebUI是一个 Open WebUI 是一个可扩展的、功能丰富、用户友好的自托管 AI 平台,专为完全离线运行而设计。 它支持多种 LLM 运行环境,包括 Ollama 和 OpenAI 兼容的 API,并内置了用于 RAG 的推理引擎,是一个强大的 AI 部署解决…...
【一文学会 HTML5】
目录 HTML概述基本概念HTML 发展历程HTML 基本结构 网页基本标签标题标签(<h1> - <h6>)段落标签(<p>)换行标签(<br>)水平线标签(<hr>)注释࿰…...
前端题目类型
HTMLCSS常见面试题 HTML标签有哪些行内元素 img、picture、span、input、textarea、select、label 说说你对元素语义化的理解 元素语义化就是用正确的元素做正确的事情。虽然理论上所有html元素都可通过css样式实现相同效果,但这样会使事情复杂化,所以需…...
nodejs学习——nodejs和npm安装与系统环境变量配置及国内加速
nodejs和npm安装与系统环境变量配置及国内加速 下载node-v22.14.0-x64.msi 建议修改为非C盘文件夹 其它步骤,下一步,下一步,完成。 打开CMD窗口查看安装详情 $ node -v v22.14.0 $ npm -v 10.9.2$ npm config list创建node_global和node_c…...
[视频编码]rkmpp 实现硬件编码
mpi_enc_test的命令参数描述说明 命令参数的描述说明如下: 命令参数 描述说明 -i 输入的图像文件。 -o 输出的码流文件。 -w 图像宽度,单位为像素。 -h 图像高度,单位为像素。 -hstride 垂直方向相邻两行之间的距离,单…...
Vue3实战学习(Vue3的基础语法学习与使用(超详细))(3)
目录 (1)Vue3工程环境准备、项目基础脚手架搭建详细教程。(博客链接) (2)Vue3的基础语法学习与使用。 (1)"{{}}"绑定数据。 <1>ref()函数定义变量——绑定数据。 <2>reactive({...})…...
基于multisim的花样彩灯循环控制电路设计与仿真
1 课程设计的任务与要求 (一)、设计内容: 设计一个8路移存型彩灯控制器,基本要求: 1. 8路彩灯能演示至少三种花型(花型自拟); 2. 彩灯用发光二极管LED模拟; 3. 选做…...
EasyRTC嵌入式视频通话SDK的跨平台适配,构建web浏览器、Linux、ARM、安卓等终端的低延迟音视频通信
1、技术背景 WebRTC是一项开源项目,旨在通过简单的API为浏览器和移动应用程序提供实时通信(RTC)功能。它允许在无需安装插件或软件的情况下,实现点对点的音频、视频和数据传输。 WebRTC由三个核心组件构成: GetUserM…...
【CSS】gap 属性详解
文章目录 一、什么是 gap 属性1. 定义2. 语法3. 默认值 二、gap 属性的基本用法1. 网格布局中的应用2. 弹性布局中的应用3. 单值和双值的区别 三、gap 属性的实际应用场景1. 表单布局优化2. 图片网格布局 四、gap 的注意事项1. 浏览器兼容性2. 替代 margin 的场景3. 不同布局的…...
【招聘精英】
我们公司是一个位于石家庄的一个科技型新型技术公司。主要做人力资源、用工、科技等方面。 有意向回石家庄的或者已经在石家庄的技术大咖、软件大牛、产品大佬、UI大神可以来了解一下。 现在招聘 高级前端开发 高级java开发 其他岗位也可以联系。 有意向的朋友可以私信我。 -…...
qt 操作多个sqlite文件
qt 操作多个sqlite文件 Chapter1 qt 操作多个sqlite文件1. 引入必要的头文件2. 创建并连接多个SQLite数据库3. 代码说明4. 注意事项 Chapter2 qt 多线程操作sqlite多文件1. 引入必要的头文件2. 创建数据库操作的工作线程类3. 在主线程中创建并启动多个工作线程4. 代码说明5. 运…...
【自学笔记】Numpy基础知识点总览-持续更新
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 Numpy基础知识点总览目录1. 简介Numpy是什么为什么使用Numpy 2. 数组对象(ndarray)创建数组数组的属性数组的形状操作 3. 数组的基本操作数组…...
DP 问题 -- LQR中的DP问题
深入地介绍线性二次调节问题(Linear Quadratic Regulator, LQR),并详细说明它作为动态规划(DP)的一个经典应用问题的求解过程。 📌 一、LQR问题定义(最优控制视角) LQR 问题是一种特…...
Win7重装不翻车!ISO镜像安全下载渠道+BIOS设置避雷手册
一、写在前面:为什么你需要这份教程? 当电脑频繁蓝屏、系统崩溃甚至无法开机时,重装系统可能是最后的救命稻草。但市面上的教程往往存在三大痛点: ⚠️ 镜像来源不明导致系统被植入后门 ⚠️ 启动盘制作失败反复折腾 ⚠️ 操作失…...
CEF在MFC上的示例工程
CEF 在 MFC 中的使用 工程配置 1、首先创建一个MFC对话框工程 创建完运行测试效果如下 2、MFC工程引入CEF库 将 CEF 目录下的 cef子目录下载解压后放到MFC工程中: 然后在VS中对工程右键 -> 属性 -> C/C -> 常规 -> 附加包含目录,添加“.\…...
#UVM# 关于 config_db 机制中的直线非直线设置和获取讲解
在 UVM 验证环境中,uvm_config_db 是一种强大的机制,用于在不同组件之间传递配置参数。实际应用中,我们经常使用直线和非直线的设置与获取。今天,着重回忆一下这些内容,希望实际中更加方便的使用。 UVM 树结构示例 假设 UVM 树结构如下: uvm_test_top ├── env │ …...
[PWNME 2025] PWN 复现
这种比赛得0也不容易,前边暖声还是能作的。 GOT 指针前溢出,可以溢出到GOT表,然后把后门写上就行 Einstein 这个拿到WP也没复现成,最后自己改了一下。 int __cdecl handle() {int offset; // [rsp8h] [rbp-38h] BYREFunsigne…...
Java网络编程,多线程,IO流综合项目一一ChatBoxes
Java网络编程,多线程,IO流综合小项目一一ChatBoxes 作者:blue 时间:2025.3.7 文章目录 Java网络编程,多线程,IO流综合小项目一一ChatBoxes1.项目介绍2.项目源码剖析2.1客户端源码2.2客户端Sender线程Runn…...
大数据、人工智能、云计算、物联网、区块链序言【大数据导论】
这里是阿川的博客,祝您变得更强 ✨ 个人主页:在线OJ的阿川 💖文章专栏:大数据入门到进阶 🌏代码仓库: 写在开头 现在您看到的是我的结论或想法,但在这背后凝结了大量的思考、经验和讨论 这是目…...
【算法 C/C++】一维前缀和
2025 - 03 - 08 - 第 68 篇 Author: 郑龙浩 / 仟濹 【一维前缀和】 文章目录 前缀和与差分 - 我的博客1 大体介绍2 计算某些区间的和( 不使用前缀和 )3 计算某些区间的和( 使用前缀和 ) 前缀和与差分 - 我的博客 一维前缀和 【算法 C/C】一维前缀和 一维差分 【算法 C/C】一维…...
【C++】:STL详解 —— 红黑树
目录 平衡二叉查找树 红黑树的概念 红黑树的五大性质 红黑树的效率 红黑树和AVL树的比较 插入与删除操作 内存与实现复杂度 经典性能数据对比 总结 对旋转的基本理解 旋转的作用 左旋(Left Rotation) 右旋(Right Rotation…...
【A2DP】SBC 编解码器互操作性要求详解
目录 一、SBC编解码器互操作性概述 二、编解码器特定信息元素(Codec Specific Information Elements) 2.1 采样频率(Sampling Frequency) 2.2 声道模式(Channel Mode) 2.3 块长度(Block Length) 2.4 子带数量(Subbands) 2.5 分配方法(Allocation Method) 2…...
Mysql的卸载安装配置以及简单使用
MySQL其它问题已经更新在:MySQL完善配置---可视化-CSDN博客 一、卸载 ①控制面板卸载 ②C盘隐藏项目>ProgramData>mysql相关文件夹,还有Program file下的MySQL文件夹 ③开始菜单栏搜索>服务,找到MySQL相关服务删除,如果再…...
Ubuntu 下 nginx-1.24.0 源码分析 (1)
main 函数在 src\core\nginx.c int ngx_cdecl main(int argc, char *const *argv) {ngx_buf_t *b;ngx_log_t *log;ngx_uint_t i;ngx_cycle_t *cycle, init_cycle;ngx_conf_dump_t *cd;ngx_core_conf_t *ccf;ngx_debug_init(); 进入 main 函数 最…...
驱动开发系列43 - Linux 显卡KMD驱动代码分析(四)- DRM设备操作
一:概述 DRM(Direct Rendering Manager)是Linux内核中的一个子系统,主要负责图形硬件的管理与图形渲染的加速。它为图形驱动提供了一个统一的接口,可以使用户空间程序与图形硬件进行直接交互,而无需通过X服务器或Wayland等显示管理器。DRM不仅用于管理显卡,还处理视频输…...
PAT乙级真题(2014·冬)
大纲 1031、查验身份证-(解析)-简单题 1032、挖掘机技术哪家强-(解析)-细节题(┬┬﹏┬┬),太抠细节了 1033、旧键盘打字-(解析)-输入格式!这才是重点(┬┬﹏┬┬),让…...
快速使用MASR V3版不能语音识别框架
前言 本文章主要介绍如何快速使用MASR语音识别框架训练和推理,本文将致力于最简单的方式去介绍使用,如果使用更进阶功能,还需要从源码去看文档。仅需三行代码即可实现训练和推理。 源码地址:https://github.com/yeyupiaoling/MA…...
学习笔记:Python网络编程初探之基本概念(一)
一、网络目的 让你设备上的数据和其他设备上进行共享,使用网络能够把多方链接在一起,然后可以进行数据传递。 网络编程就是,让在不同的电脑上的软件能够进行数据传递,即进程之间的通信。 二、IP地址的作用 用来标记唯一一台电脑…...
硬件基础(4):(2)认识ADC参考电压
文章目录 1. **ADC参考电压的定义**2. **如何影响采样值**3. **参考电压的选择**4. **如何选择参考电压**5. **总结** **ADC参考电压(Vref)**是用于定义ADC采样范围的一个重要参数,以下是对 ADC 参考电压的详细解释: 1. ADC参考电…...
项目中同时使用Redis(lettuce)和Redisson的报错
温馨提示:图片有点小,可以放大页面进行查看... 问题1:版本冲突 直接上图,这个错表示依赖版本不匹配问题,我本地SpringBoot用的是2.7,但是Redisson版本用的3.32.5。 我们通过点击 artifactId跟进去 发现它…...
工程化与框架系列(25)--低代码平台开发
低代码平台开发 🔧 引言 低代码开发平台是一种通过可视化配置和少量代码实现应用开发的技术方案。本文将深入探讨低代码平台的设计与实现,包括可视化编辑器、组件系统、数据流管理等关键主题,帮助开发者构建高效的低代码开发平台。 低代码…...
在CentOS系统上安装Conda的详细指南
前言 Conda 是一个开源的包管理系统和环境管理系统,广泛应用于数据科学和机器学习领域。本文将详细介绍如何在 CentOS 系统上安装 Conda,帮助您快速搭建开发环境。 准备工作 在开始安装之前,请确保您的 CentOS 系统已经满足以下条件&#x…...
系统思考—组织诊断
“未经过诊断的行动是盲目的。” — 托马斯爱迪生 最近和一家教育培训机构沟通时,发现他们面临一个有意思的问题:每年招生都挺不错,但教师的整体绩效一直提升缓慢,导致师生之间存在长期的不匹配。管理层试了很多办法,…...
项目实战--网页五子棋(对战功能)(9)
上期我们完成了websocket建立连接后的数据初始化,今天我们完成落子交互的具体代码: 这里我们先复习一下,之前约定好的落子请求与响应包含的字段: 1. 发送落子请求 我们在script.js文件中找到落子的相关方法,增加发送请…...
Ubuntu系统安装Apache2方法
Ubuntu系统安装Apache2方法 一、安装 Apache2更新软件包列表安装 Apache2启动服务验证安装 二、访问默认页面三、基本配置配置文件结构目录权限访问测试 四、故障排除五、总结 一、安装 Apache2 更新软件包列表 在安装任何软件之前,建议先更新系统的软件包列表&am…...
DeepSeek R1-32B医疗大模型的完整微调实战分析(全码版)
DeepSeek R1-32B微调实战指南 ├── 1. 环境准备 │ ├── 1.1 硬件配置 │ │ ├─ 全参数微调:4*A100 80GB │ │ └─ LoRA微调:单卡24GB │ ├── 1.2 软件依赖 │ │ ├─ PyTorch 2.1.2+CUDA │ │ └─ Unsloth/ColossalAI │ └── 1.3 模…...
基于springboot和spring-boot-starter-data-jpa快速操作mysql数据库
1、创建springboot项目 2、pom.xml文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http:…...
深度学习代码解读——自用
代码来自:GitHub - ChuHan89/WSSS-Tissue 借助了一些人工智能 2_generate_PM.py 功能总结 该代码用于 生成弱监督语义分割(WSSS)所需的伪掩码(Pseudo-Masks),是 Stage2 训练的前置步骤。其核心流程为&a…...
文件与目录权限
目录 文件权限 文件读权限(r) 文件写权限(w) 文件可执行权限(x) 目录权限 目录读权限(r) 目录写权限(w) 文件可执行权限(x)(与文件权限最大不同之处) 注意 在 Linux 系统中,…...
算法005——有效三角形个数
力扣——有效三角形个数点击链接跳转 判断三条边是否能组成三角形,大家第一时间想到的就是两边之和大于第三边 但是运用这个方法,我们需要判断三次,有一个更简单的方法,只需要判断一次 因为 C 已经是三边之中最大的了ÿ…...
Facebook 的隐私保护数据存储方案研究
Facebook 的隐私保护数据存储方案研究 在这个信息爆炸的时代,数据隐私保护已成为公众关注的热点。Facebook,作为全球最大的社交媒体平台之一,承载着海量用户数据,其隐私保护措施和数据存储方案对于维护用户隐私至关重要。本文将深…...
如何高效利用Spring中的@Cacheable注解?
在现代软件开发中,缓存是提升应用性能的重要手段。Spring框架提供了Cacheable注解,帮助开发者轻松实现缓存机制。今天我们就来详细聊聊Cacheable注解的使用,看看它是如何让我们的应用更加高效的! Cacheable注解的核心功能是缓存方…...
Qt 进度条与多线程应用、基于 Qt 的文件复制工具开发
练习1:Qt 进度条与多线程应用 题目描述 开发一个基于 Qt 的应用程序,该应用程序包含一个水平进度条(QSlider),并且需要通过多线程来更新进度条的值。请根据以下要求完成代码: 界面设计: 使用 QS…...
软件工程---构件
在软件工程中,构件是一个独立的、可复用的软件单元,它具有明确的功能、接口和行为,并且可以在不同的环境中加以集成和复用。构件的概念是软件架构和组件化开发的核心思想之一,其目的是促进软件系统的模块化、可维护性和可扩展性。…...
【算法 C/C++】二维差分
2025 - 03 - 08 - 第 71 篇 Author: 郑龙浩 / 仟濹 【二维差分】 文章目录 前缀和与差分 - 我的博客差分(二维)1 大体思路 | 一些区间加某数的最终结果2 二维差分原理3 例题 前缀和与差分 - 我的博客 一维前缀和 【算法 C/C】一维前缀和 一维差分 【算法 C/C】一维差分 二维前…...
灰色地带规避:知识产权校验API的商标库模糊匹配算法
在反向海淘或其他电商业务场景中,为了规避知识产权方面的灰色地带,开发知识产权校验 API 并运用商标库模糊匹配算法是很有必要的。以下将详细介绍商标库模糊匹配算法的设计与实现: 算法设计思路 商标库模糊匹配算法的核心目标是在给定一个待匹…...
LINUX网络基础 [五] - HTTP协议
目录 HTTP协议 预备知识 认识 URL 认识 urlencode 和 urldecode HTTP协议格式 HTTP请求协议格式 HTTP响应协议格式 HTTP的方法 HTTP的状态码 编辑HTTP常见Header HTTP实现代码 HttpServer.hpp HttpServer.cpp Socket.hpp log.hpp Makefile Web根目录 H…...
嵌入式人工智能应用-第6章 人脸检测
嵌入式人工智能应用 人脸检测 嵌入式人工智能应用1 人脸检测1.1 CNN 介绍1.2 人脸检测原理1.3 MTCNN介绍1.4 NCNN介绍2 系统安装2.1 安装依赖库NCNN2.2 运行对应的库3 总结1 人脸检测 1.1 CNN 介绍 卷积神经网络。卷积是什么意思呢?从数学上说,卷积是一种运算。它是我们学习…...