目录
一.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。