JVM = java Virtual Machine,java虚拟机。
JVM 包含两个子系统 和 两个组件。
组成 | 说明 |
---|---|
类加载系统 ClassLoader 子系统 |
根据全限定类名装载class文件到运行时数据区的方法区中 |
运行时数据区 Runtime Data Area 组件 |
也就是JVM内存,包含线程共享区(方法区、堆),线程独占区(本地方法栈、程序计数器、虚拟机栈) 把字节码加载到内存中,即运行时数据区的方法区 |
执行引擎 Execution Engine 子系统 |
就是解释器,负责解释class中的指令 将字节码翻译成底层系统指令,交由CPU去执行 |
本地方法接口 Native Interface 组件 |
连接本地方法库(native lib),是与其他编程语言交互的接口 在此过程中需要调用其他语言的本地库接口来实现整个程序的功能 |
扩展
java程序运行机制:
1 集成开发环境编写java程序,文件后缀名是.java
2 javac编译器,将.java文件编译为字节码文件(.class)
3 类加载器,将.class文件加载到运行时数据区中的方法区
4 执行引擎(解析器),将字节码文件翻译成底层系统指令,交由CPU执行
5 这个过程中,需要调用其他语言的本地库接口
2022/8/4
JVM将内存分为主内存和工作内存。
主内存 : 本的方法区 和 堆
Java内存模型规定了所有变量都存储在主内存中。
工作内存/本地内存:java虚拟机栈、本地方法栈、程序计数器
( 有时会将java虚拟机栈和本地方法栈合二为一 )
每个线程都有自己的工作内存,保存了该线程使用的变量,该变量是主内存中的共享变量的副本拷贝。
私有
条目 | 说明 |
---|---|
概念 | 较小的内存空间,可以看成是当前线程执行字节码文件的行号指示器 |
作用 | 字节码解释器就是需要通过改变程序计数器的值来获取下一条要执行的字节码指令 |
是私有的原因 | java虚拟机的多线程是通过线程轮流切换并分配CPU执行时间来实现的。 任何一个确定的时刻,一个处理器只会执行一个线程中的命令。 为了线程切换后能恢复到正确的执行位置,所以每个线程都需要一个独立的程序计数器,各个线程之间的计数器互不影响。 |
条目 | 说明 |
---|---|
概念 | 描述Java方法执行的内存模型 |
作用 | 每个方法在执行的同时都会创建一个栈帧(stack Frame),用于存储局部变量表、操作数栈、动态链表、方法出口等信息 |
解释 | 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程 |
性质 | 它是线程所私有的,生命周期与线程相同 |
异常 | 线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常; 扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常 |
局部变量表
存放了编译期可知的各种基本数据类型(boolean、byte、short、char、int、long、float、double),对象引用和returnAddress类型(指向了一条字节码指令的地址)。
空间单位是槽(Slot),64位的long和double类型会占用两个Slot。
局部变量表所需要内存空间在编译期完成分配,当进入一个方法时,该方法需要在帧中分配的局部变量表内存区域是确定的,在方法运行期间不会改变局部变量表的大小。
条目 | 说明 |
---|---|
概念 | 为虚拟机使用到的Native方法服务 (与java虚拟机栈类似) |
抛出异常 | StackOverflowError 和 OutOfMemoryError 异常 |
备注 | java虚拟机规范没有对本地方法栈中方法使用的语言、方式和数据结构做出强制规定,所以具体的虚拟机可以自由地实现它。 例如:Sun HotSpot虚拟机直接把Java虚拟机和本地方法栈合二为一。 |
共享
条目 | 说明 |
---|---|
概念 | 被所有线程所共享的一块内存区域,在虚拟机启动时创建 |
存在目的 | 存放对象实例,几乎所有的对象实例都在这里分配内存 |
又被称为GC堆 | java堆是垃圾收集器管理的主要区域。 从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以java堆又被细分为:新生代 和 旧生代。 从内存分配的角度,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allcation Buffer,TLAB),为了更好地回收内存和分配内存。 |
抛出异常 | 当在堆中没有完成实例分配,且堆无法扩展时,将会抛出OutOfMemoryError异常 |
内存区域规定 | java虚拟机规定,java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。 实现时,可以是固定大小的,也可以是可扩展的 |
条目 | 说明 |
---|---|
概念 | 各个线程共享的内存区域 |
作用 | 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 |
理解 | 虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但它有别名非堆,目的为了与java堆区分开来 |
特点 | 不需要连续的内存,可选固定大小,可扩展,可选择不实现垃圾收集(垃圾回收主要针对常量池、类型的卸载) |
异常 | 方法区无法满足内存分配需求时,抛出OutOfMemoryError异常 |
运行时常量池 Runtime Constant Pool
方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一些信息在常量池中,用于存放编译器生成的各种自变量和符号引用。该内容将在类加载后进入方法区的运行时常量池中存放。
区别 | 堆 | 栈 |
---|---|---|
存储内容不同 | 存储Java中的对象实例 成员变量、局部变量、类变量,它们指向的对象都存储在堆内存中 |
存储基本数据类型和方法引用 |
共享 or 私有 | 共享,其中对象对所有线程可见 且 可访问 | 私有,每个线程都有一个栈内存,存储的变量只能在所属线程中可见 栈内存可以理解为线程的私有内存 |
异常错误 | 没有空间,抛出OutOfMemoryError | 没有空间,抛出StackOverflowError |
空间大小 | 较大 | 较小 |
作用 | 存储单位 数据存储问题,数据该怎么放、放哪里 |
运行时单位 决定程序如何执行、如何处理数据 |
扩展
每一个线程都会有独立的线程栈,因为每个线程的执行逻辑不同。
栈 | 基本数据类型 和 对象的引用 |
---|---|
堆 | java中的对象实例 |
扩展
原因 | 具体介绍 |
---|---|
软件设计角度 | 分而治之的思想,栈代表处理逻辑、堆代表数据,使得处理逻辑更加清晰 |
为了实现共享 | 堆中的数据可以被多个栈共享 共享提供了有效的数据交换方式(共享内存);堆中的共享常量和缓存可以被所有线程访问,节省了空间 |
动态增长 | 栈只能向上增长,限制了栈存储内容的能力 堆中的对象可以根据需要动态增长 两者拆分,使动态增长成为可能性,栈中只需要记录一个地址即可 |
扩展
保存系统运行的上下文,需要进行地址段的划分。
栈中一般存放的是基本数据类型和java对象的引用,不存储java对象的实例。
因为栈内存空间较小,而java对象的实例大小不固定且可能会动态改变,存储在栈中可能会出现StackOverflowError。
主内存与工作内存中间的交互,具体表现是:一个变量如何从主内存中拷贝到本地内存、如果从本地内存同步回主内存。
主要涉及两个部分:主内存和本地内存,本地内存是主内存的拷贝。
java内存模型定义了8种操作,每一种都是原子的,不可再分的。
操作名 | 作用于 | 说明 |
---|---|---|
lock(锁定) | 主内存变量 | 把一个变量标识为一个线程独占的状态 |
unlock(解锁) | 主内存变量 | 把一个处于锁定状态的变量释放,释放后的变量才能被其他线程所访问 |
read(读取) | 主内存变量 | 把一个变量的值从主内存传输到本地内存中,以便load操作使用 |
load(载入) | 本地内存变量 | 把read操作从主内存中得到的变量值放入本地内存的变量副本中 |
use(使用) | 本地内存变量 | 将本地内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时,就执行执行这个操作 |
assgin(赋值) | 本地内存变量 | 将执行引擎接收的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 |
store(存储) | 本地内存变量 | 把本地内存中的一个变量的值传送到主内存中,以便随后的write操作使用 |
write(写入) | 主内存变量 | 把store操作从本地内存中得到的变量的值放入主内存的变量中 |
内存交互整体流程图如下所示:
2022/8/4 理解
字节码的执行引擎 = JVM运行时数据区的执行引擎,主要是将.class文件翻译成系统可以运行的指令。
执行引擎是解释器,而不是编译器。
编译器和解释器的区别:
区别 | 编译器compare | 解释器interpreter |
---|---|---|
都转换为机器码 | 在程序运行之前将代码转换成机器码 | 在程序运行时将代码转换成机器码 |
intput | 整个程序 | 每次读取一行 |
output | 生成中间代码 | 不生成中间代码 |
工作机制 | 编译在执行之前完成 | 编译和执行同时进行 |
存储 | 存储编译后的机器代码在机器上 | 不保存任何机器代码 |
修改 | 修改代码需要改源码,并重新编译 | 直接修改就可运行 |
需要满足的规则
主内存与工作内存变量传递
加锁与释放操作
值传递。
传递的参数是 基本数据类型,则传递的是具体数值;
传递的参数是 对象引用,则传递是堆中对象的地址,而对于对象引用的修改实际上修改的是对象引用所表示的对象,而不是引用本身。
扩展
java中没有指针的概念。
程序运行永远都是在栈中进行的,因而参数传递只存在传递基本数据类型和对象引用的问题,不会直接传递对象本身。
基本数据类型的大小是固定的;
非基本类型的java对象,其大小就值得商榷。
2022/8/4总结
java对象的大小 = 8的倍数、空的object对象大小是8byte
非基本数据类型大小
在java中,一个空Object对象的大小是8 byte,这个大小只是在堆中一个没有任何属性的对象的大小。
因为所有java非基本类型的对象都默认继承Object对象,则java对象的大小必须是大于 8 byte 。
java给对象分配内存的时候都是以8的整数倍来分的。
也就是一个空的java对象所使用的大小是4(对象引用)+8(对象)=12 byte。
Object ob = new Object();
该java对象所占内存为 4 byte + 8 byte。
4 byte 指的是java栈中保存引用所需的空间;
8 byte 则是对象在java堆中所需空间。
案例
class MaNong{
int count;
boolean flag;
Object obj;
}
MaNong类的大小为:空的Object对象(8 byte) + int 数据(4 byte) + Boolean 数据(1 byte) + 空Object对象在栈中的引用(4 byte) = 17 byte 。因为java给对象分配内存的时候都是以8的整数倍来划分的,所以大于17、满足是8的整数倍、且最小的数就是24,因此该对象MaNong的大小是 24 byte 。
内存泄露(Memory Leak),就是存在一些不会再被使用但却没有被回收的对象。
这些对象具有如下性质:
对象是可达的,即在有向图中,存在通路可以与其他相连;
对象是无用的,即程序以后不会再使用这些对象。
根本原因:
长生命周期对象 持有 短生命周期对象的引用 而导致其不能被回收。
内存泄露 : 无用对象(不再使用的对象)持续占有内存 或 无用对象的内存 得不到及时释放 。
具体情况 | 说明 |
---|---|
外部类引用内部类 | 发生在非静态内部类(匿名类)中,在类初始化时,内部类总是需要外部类的一个实例。 每个非静态内部类默认都持有外部类的隐式引用。如果在应用程序中使用该内部类的对象,即使外部类使用完毕,也不会对其进行垃圾回收。 |
未关闭的资源 | 创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如:数据库连接(dataSource.getConnetint())、网络连接(Socket) 和 IO连接,除非显式调用clone()方法将其连接关闭,否则不会自动被GC回收 |
静态属性 | java中静态属性的生命周期伴随着应用整个生命周期。 |
不当的equals和hashcode方法实现 | 定义一个新的类,往往需要重写equal和hashcode方法,重写不当,会造成内存泄露 案例:某A类对象存储入Map集合,map集合不能存储重复值,如果A类没有重写equals方法,执行put方法,Map会认为每次创建的对象都是新的对象,造成内存不断的增长 |
finalize() | 每当一个类的finalize()方法被重写时,该类的对象就不会被GC立即回收。GC会将它们放入队列进行最终确定,在以后的某个时间点进行回收。 如果finalize()方法重写的不合理或finalizer队列无法跟上Java垃圾回收器的速度,那么迟早,应用程序会出现OutOfMemoryError异常。 |
ThreadLocal | 提供线程本地变量,保证访问到的变量属于当前线程。 ThreadLocal的实现中,每个Thread维护一个ThreadLocalMap映射表,key是ThreadLocal实例本身,value是真正需要存储的Object。 ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。 如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。 |
单例模式 | 不正确使用单例模式是引起内存泄露的常见原因。 单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄露 |
集合内对象 remove | 集合里面的对象属性被修改,remove操作不起作用了。 一个集合中存储的对象是Student类对象,当Student类中的成员属性被修改时,对集合对象的删除remove操作就不起作用了。 |
扩展
参考
解决方法 | 说明 |
---|---|
尽早释放无用对象的引用 | 最好在使用临时变量的时候,让引用变量在退出活动期后自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露 |
进行字符串处理时,避免使用String,而使用StringBuilder | 因为每一个String对象都会独占内存一块区域 |
尽量少用静态变量 | 静态变量属于全局,GC不会回收 少使用静态变量,减少生命周期 |
避免集中创建对象尤其是大对象,如果可以的话尽量使用流操作 | JVM在突然需要大量内存时,会触发GC优化系统内存环境 |
尽量运用对象池技术以提高系统性能 | 生命周期长的对象拥有生命周期短的对象时 容易引发内存泄露。 例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后一块处理结束释放一块 |
不要在经常调用的方法中创建对象,忌讳在循环中创建对象 | 可以适当使用hashtable、vector创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃。 |
基类 析构函数 | 在需要的时候将基类的析构函数定义为虚函数 |
释放数组 | 释放数组使用delete [] |
扩展
虚函数
java的普通成员函数(没有被static、native等关键字修饰)就是虚函数,即每个非静态方法,因为它本身就实现虚函数实现的功能(多态)。
虚函数的性质是:多态。
如果java中不希望某个函数具有虚函数特性,可以加上final关键字变成非虚函数。
析构函数
作用:用于撤销对象前,完成一些清理工作。
常见的异常包括:
堆用于存储对象实例,如果不断创建对象,并且Root GC根节点与这些节点之间有通路从而避免垃圾回收机制来回收这些对象,就可能会导致堆内存溢出;
虚拟机栈和本地方法栈溢出;
原因:线程请求的栈深度大于虚拟机允许的栈深度抛出stackOverflowError;
虚拟机的栈允许动态扩展,当扩展栈容量无法申请到足够的内存时,抛出OutOfMemoryError;
方法区和运行时常量池溢出;
运行时常量区是方法区的一部分;
方法区的内存溢出;
方法区用于存放类型的相关信息,比如类型、访问修饰符、常量池、字段描述、方法描述等;
该区域内存溢出通过是运行时产生大量的类去填满了方法区。
直接内存 = 不属于虚拟机运行时数据区;是由操作系统直接管理的内存,又称为堆外内存;
可以使用Unsafe 或 ByteBuffer分配直接内存。
java栈、系统运行时的寄存器
查找过程:
以栈或系统运行时寄存器为起点,找到堆中的对象,从这些对象找到堆中其他对象的引用,最终以null引用或基本类型结束,形成了一棵以Java栈中引用所对应的对象为根节点的一棵对象树。(可达性分析法)
当前树上的节点不被垃圾回收;剩余对象节点,会被当做垃圾回收。
引用计数法:对象有引用计数器、无法得到循环引用;可达性分析法:从GC节点开始,找引用节点
在JVM中,判断垃圾可以回收的方法有:引用计数法 和 可达性分析法
引用计数法 Reference Counting
理解:a对象 引用对象b(引用计数器:统计引用该对象的对象的数量)
当a对象实例被回收时,它引用的任何对象实例的引用计数器减1。
当a对象被创建时,就给该对象实例分配一个变量,该变量计数设置为0;
当其他对象中的变量被赋值为a对象的引用时,a对象的计数值加1;
当a对象实例的某个引用超过了生命周期或者被设置为新的值时,a对象实例的引用计数器减1;
优点:
可以快速执行,算法简单易于实现;
对程序需要不被长时间打断的实时环境比较有利。
缺点:
无法检测出循环引用。即父对象有一个对子对象的引用,子对象反过来引用父对象。这样他们的引用计数永远不可能到0。
可达性分析算法 Reachability Analysis
优点:
可以解决循环引用的问题;
缺点:
多线程访问的环境下,其他线程可能会重复访问已回收的对象。
GC Root节点的选取
Java虚拟机栈中引用的对象(栈帧中的局部变量表);
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(Java Native Interface,java本地接口)引用的对象。
.
不会,两次标记、finalize方法、F-Queue队列
即使可达性分析算法中的不可达对象,也并非是“非死不可”。
要判定一个对象死亡,至少要经历两次标记过程。
第一次标记后,会判断是否可以执行finalize()方法。不能执行该方法(没有覆盖finalize方法或者虚拟机已经调用过了),可以执行(将对象放在F-Queue队列中,随后finalizer线程去执行它);然后在F-Queue队列中进行小规模的标记,如果通过可达性对其算法来说还是没有引用链和GCRoot关联,则会被回收掉。
这里的执行 = 虚拟机会触发这个方法,但并不承诺会等待它运行结束。这是为了防止一个对象在finalize()方法中执行缓慢,或者发生了死循环,这时可能会导致F-Queue中的其他对象永久处于等待状态,从而导致整个内存回收系统崩溃。
对象要在finalize()中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可。
finalize() 方法是一次性的免死金牌,只能免死一次。
垃圾收集算法有:标记清除算法、标记整理算法、复制算法、分代收集算法等。
标记清除算法(Mark-Sweep)
条目 | 说明 |
---|---|
概念 | 算法分为“标记” 和 “清除” 两个阶段: ①标记出所有需要回收的对象; ②标记完成后统一回收所有被标记的对象; |
优点 | 无须移动对象,算法简单 |
缺点 | 效率问题:标记和清除的过程效率都不高; 会产生大量的碎片空间:可能会造成在申请大块内存的时候没有足够的连续空间导致再次GC。 |
标记整理算法(Mark-Compact)
条目 | 说明 |
---|---|
概念 | 算法分为“标记” 和 “整理”两个阶段: ①标记出所有存活的对象; ②将所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。 遍历两次,一次标记,一次压缩清理 |
优点 | 堆的使用率高,无内存碎片; |
缺点 | 暂停时间更长,对缓存不友好(对象移动后顺序关系不存在) |
复制算法(Copy)
条目 | 说明 |
---|---|
概念 | 将内存分成两块,每次申请内存时都是用其中一块。 当内存不够时,将这一块内存中所有存活的对象复制到另一块内存中,然后再把已使用的内存整个清理掉。 |
内存划分 | 将内存区域分成相等的两部分:分别用两个指针from 和 to 来维护,分配内存只使用from指针指向的内存区域 |
优点 | ①吞吐量大(一次能收集整个内存的一半空间); ②分配效率高(对象可以连续分配); ③没有内存碎片 |
缺点 | ①每次申请内存时,只能使用一半的内存空间; ②内存利用率严重不足。 |
分代收集算法(Generational Collection)
条目 | 说明 |
---|---|
概念 | 根据对象存活周期的不同将内存划分成几块。一般Java堆分成新生代和老生代。 |
优点 | 组合算法,分配效率高、堆的使用率高 |
缺点 | 算法复杂 |
条目 | 说明 |
---|---|
新生代 | java对象是朝生暮死的,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集; 新生代又可分为伊甸区和两个幸存区。 伊甸区和两个幸存区的划分比例是:8:1:1 |
老生代 | 对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记清理”或“标记整理”算法进行回收。 |
对象划分的依据 | 对象的生存周期或者说经历GC的次数。 对象创建时在新生代申请内存,当经历一个GC还存活,那么对象的年龄+1; 当年龄超过一定值(默认15,通过MaxTenuringThreshold设置),如果对象还存活,则该对象进入老生代。 |
新/老生代划分 | 新生代:老年代所占内存比例是1:2,也就是新生代占据1/3的内存,老生代占据2/3的内存 |
总结2022/8/5
说明 | 伊甸区 | 幸存区A | 幸存区B | 老年代 |
---|---|---|---|---|
新对象 | √ | – | – | – |
熬过一个生命周期 | – | √ | – | – |
A区中对象经过一个GC | – | – | √ | – |
B区中对象年龄超过固定值 | – | – | – | √ |
当对象占用内存很大 | – | – | – | √ |
具体图示
备注2022/8/5
新生代 和 老生代 比例图画错了,应该是 1:2。
原因:
①分代收集算法:针对不同生命周期的对象采用不同的收集方式,以便提高垃圾回收效率。
②分代垃圾回收采用分治的思想。进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最合适它的垃圾回收方法进行回收。这样减少了遍历存活对象所消耗的时间。每次对整个堆空间中的所有存活对象进行遍历,所花费时间都很长,而具体针对生命周期长的对象来说这种遍历是没有效果的。因为不管多少次遍历,它们依旧存在。
扩展
目前而言,业界各种商业虚拟机堆内存的垃圾收集机制基本都采用分代收集算法。
新生代(年轻代)主要以复制算法为主。
老生代(老年代)主要以标记整理为主。
备注:2022/8/5
垃圾回收被分为:Minor GC、Major GC、Full GC;
浮动垃圾:
应用程序运行的同时进行垃圾回收,所以有些垃圾可能会在垃圾回收进行的过程中产生,这些垃圾被称为浮动垃圾。
此次垃圾回收不会回收浮动垃圾,它们需要等到下次垃圾回收时才能处理。
内存碎片:
不同Java对象存活时间不一定是相同的。在程序运行一段时候后,垃圾回收机制会回收某些对象,这时就会产生零散的内存碎片。
解决方式:
内存碎片导致的最直接的问题就是:无法分配大块的内存空间,进而使得程序运行效率降低。
可以采用垃圾回收算法解决,比如“复制算法”、“标记-整理”等。
常见的垃圾收集器有10种:
垃圾收集器 | 分代情况 | 使用情况 | 采用算法 |
---|---|---|---|
Serial | 物理/逻辑都分代 | 年轻代 | 停止-复制算法 |
SerialOld | 物理/逻辑都分代 | 老年代 | 标记-整理算法 |
ParallelScavenge | 物理/逻辑都分代 | 年轻代 | 停止-复制算法 |
ParallelOld | 物理/逻辑都分代 | 老年代 | 标记-整理算法 |
ParNew | 物理/逻辑都分代 | 年轻代 | 停止-复制算法 |
CMS | 物理/逻辑都分代 | 老年代 | 标记-清除算法 |
G1 | 逻辑分代,物理不分代 | – | 分代收集算法 |
ZGC | 物理/逻辑不分代 | – | – |
Shenandoah | 物理/逻辑不分代 | – | – |
Epsilon | – | jdk11提出debug使用的,不考虑 | – |
垃圾回收器组合
Serial + SerialOld
Parallel Scavenge + ParallelOld
ParNew + CMS
Serial、Parallel Scavenge、Parallel New
介绍 | Serial | Parallel Scavenge | Parallel New |
---|---|---|---|
概念 | 单线程新生代复制算法的垃圾回收器 | 多线程新生代复制算法的垃圾回收器 | 多线程新生代复制算法的垃圾回收器 |
理解 | 让所有线程找到一个安全点然后停止工作,这种行为称之为STW(stop-the-world); 所有线程都停止,此时单线程的Serial开始进行垃圾清理。 当Serial工作时间过长的时候,那么STW的时间就会加长,用户的响应时间也会加长。 |
是一种高效的多线程复制算法;在Serial基础上的; | 与Parallel Scavenge 类似,但更注重吞吐量 |
垃圾回收算法 | 复制算法 | 复制算法 | 复制算法 |
复制算法: 将内存分成两块区域area0、area1,每次对象内存的分配都在area0中,当该区域满之后,将该区域的存活变量复制到area1中,然后清理掉area0区域的对象,最后将area1的内容复制给area0.
SerialOld、Parallel Old、CMS
介绍 | SerialOld | Parallel Old | CMS |
---|---|---|---|
概念 | 单线程老生代标记整理算法 | 多线程老生代标记整理算法 | 多线程老生代标记清理算法 |
理解 | 存在STW状态,所有线程都停止后,此时单线程的SerialOld开始执行 适用场景是存活对象较多的情况 |
一样存在STW,可以看做是SerialOld的多线程版本 | 老年代垃圾回收器,在老年代分配不下时,触发CMS |
垃圾回收算法 | 标记-整理算法 | 标记-整理算法 | 标记-清理算法 |
CMS = Mostly Concurrenct Mark and Sweep Garbage Collector”(最大-并发-标记-清除-垃圾收集器)。
标记-整理算法 :把所有存活的对象标记出来,然后将它们向一端移动,然后直接将边界以外的部分清理掉。
标记-清理算法:把所有需要清理的对象标记出来,垃圾回收机制统一将其清理掉。
ZGC
JVM垃圾回收机制
JDK版本与垃圾收集器的对应广关系
JDK7 = Parallel Scavenge + Parallel Old
JDK8 = Parallel Scavenge + Parallel Old
JDK11 = G1
概念:
CMS = Concurrent Mark-Sweep、并发标记清理垃圾回收器。
具体实现过程:
有五个阶段——初始标记阶段、并发标记阶段、并发预清理阶段、重新标记阶段、并发清理阶段。
阶段 | 介绍 |
---|---|
初始标记阶段 | 暂停所有线程,此时是CMS的第一个STW(Stop the world,所有用户线程暂停) 标记所有GC Roots直接关联的对象 + 被存活的年轻代所直接引用的老年代对象 |
并发标记阶段 | 此时GC线程和用户线程同时存在,会记录所有可达对象; 此过程结束之后由于用户线程一直在运行则还会产生新的引用更新,即需要下一步; 改变:①GC Root 对老生代对象引用的改变 ② 新生代对象对老生代对象的引用改变 ③老生代中对象之间引用的改变 ④ 老生代出现的新生对象 |
并发预清理阶段 | 由于并发标记阶段没有停止用户线程,老年代的引用可能发生变化 + 新生代也可能有新的引用指向老年代(都需要重新标记),该过程可能发生Minor GC(新生代垃圾清理机制)来减少扫描时间。 引用的改变:老年代引用改变、新生代有新的引用指向老年代 |
重新标记阶段 | 停止用户线程(第二次STW),将上一步并发标记过程中用户线程引起的更新进行修正,时间会比初始标记时间长、比并发标记时间短; |
并发清理阶段 | 在所有需要清理的对象都被标记完成后就会执行最后一步清理操作;GC线程执行同时用户线程可以执行,GC线程只会清理标记的区域。 |
具体执行过程图示:
备注2022/8/7
GC Root选取方式:java虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈的JNI(java native interface)java本地接口引用的对象;
而垃圾回收的对象 是java堆中的内容。
概述
G1垃圾回收器整体过程
分为四个阶段:初始标记阶段、并发标记阶段、最终标记阶段、筛选回收阶段。
阶段 | 说明 |
---|---|
初始标记阶段 | 通过可达性分析标记GC Roots的直接关联对象,该阶段需要STW 标记:GC Root的直接关联对象 |
并发标记阶段 | 通过GC Roots找存活对象,该阶段GC线程和用户线程同时运行, 标记时间比初始标记时间长 标记:关联对象 |
最终标记阶段 | 重新标记,修正并发标记过程中因用户线程继续运行而导致新的引用更正,该阶段需要STW |
筛选回收阶段 | 对每个Region的回收成本进行排序,根据用户期望的停顿时间来制定回收计划, 即体现可预测停顿时间 该阶段GC线程和用户线程同时运行 |
优势
优势 | 说明 |
---|---|
并发收集 | 充分使用CPU、多核环境下的硬件优势 |
空间整合 | 整体上看是基于“标记-整理”算法、局部(Region之间)基于“复制”算法 |
分代收集 | 保留新生代和老生代 |
可预测的停顿 | 除了追求低停顿外,还建立可预测的停顿时间模型 能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒 |
参考
JVM内存可以分为堆内存和非堆内存。堆内存分为年轻代和老年代,年轻代又分为Eden(伊甸)区和Survivor(幸存)区。
垃圾回收策略或垃圾回收时机 分为 Minor/Scavenge GC、Major GC、Full GC三种。
Minor / Scavenge GC
Major GC
Full GC
Java堆分代管理2022/8/7
永久代:Permanent Generation,方法区/元空间。
内存分配:
具体 | 说明 |
---|---|
对象优先在Eden存放 | 大多数情况对象在新生代Eden区分配,空间不够发起Minor GC |
大对象直接进入老年代 | 避免存储在Eden和Survivor区导致大量的复制操作 |
长期存活的对象将进入老年代 | 对象的年龄计数器,对象在Eden出现并经过Minor GC依然存活,并将其移动到Survivor,那么年龄就增加1岁,增加到一定年龄将其移动到老年代 |
动态对象年龄判定 | 为了适应不同程序的内存情况,虚拟机不再限定到达某个值才进入老年代,而是找到Survivor空间中相同年龄所有对象大小的总和 大于 Survivor空间一半 的年龄,那么该年龄就作为分界线,Survivor空间中年龄大于或等于该分界线的对象 就可以进入老年代,无需等到MaxTenuringThreshold要求的年龄 |
空间分配担保 | 发生Minor GC前,会先判断老年代是否有最大的连续空间大于新生代所有对象空间,有则认为是安全的 不成立,则查看HandlePromotionFailure设置值是否允许担保失败,允许则继续检查老年代最大可用的连续空间 是否大于 经过晋升到老年代对象的平均大小,大于则再次进行Minor GC;如果小于,或者HandlePromotionFailure设置不允许冒险,则执行Full GC |
对象的访问定位的方式是:句柄访问 和 直接指针访问 。
句柄访问
概念 | Java堆会划分出来一部分内存来作为句柄池,reference中存储的就是对象的句柄地址 |
---|---|
reference | 存储的是对象的句柄地址 |
句柄 | 包含 对象实例数据的地址(Java对象) 和 对象类型数据的具体地址(基本数据) |
好处 | reference中存储着稳定的句柄地址,当对象移动之后(垃圾收集时移动对象是非常普遍的行为),只需要改变句柄中的对象实例地址即可,reference不用修改 |
案例:
Object obj = new Object();
Object obj 表示一个本地引用,存储在java栈的本地变量表中,表示一个reference类型的数据。
new Object() 作为实例对象存放在java堆中,
同时java堆中还存储了Object类的信息(对象类型、实现接口、方法等)的具体地址,
这些信息所执行的数据类型存储在方法区中。
直接指针访问
概念 | Java堆对象的布局中就必须考虑如何放置访问类型的相关信息(如对象的类型、实现的接口、方法、父类、field等) |
---|---|
reference | 存储的是对象的地址 |
好处 | 访问速度快,它减少了一次指针定位的时间开销 |
扩展
java程序需要通过栈上的reference数据来操作堆上的具体对象。
reference类就是Java为引用类型定义的类,且是与Java垃圾回收机制密切相关的类。
Java虚拟机规范里面规定了reference类型是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问堆中对象的具体位置。
java中的引用有四种类型:强引用 、 软引用 、 弱引用 、 虚引用 。
类型 | 概念 |
---|---|
强引用 strong reference |
只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象 Java引用默认是强引用,对任何一个对象的赋值操作就产生了对这个对象的强引用 类似 Object obj = new Object(); |
软引用 soft reference |
用来描述一些还有用但并非必须的对象 只有在内存不足的情况下,被引用的对象才会被回收 使用SoftReference类创建软引用 |
弱引用 weak reference |
被弱引用关联的对象只要垃圾回收机制执行,就会被回收。 使用WeakReference类创建弱引用 |
虚引用 phantomReference |
幽灵引用 或 幻影引用 , 是最弱的一种引用 用于跟踪垃圾回收器收集对象的活动,如果发现该对象,GC会将引用放到ReferenceQueue(队列=存储待回收对象引用),程序员自己处理,执行ReferenceQueue.poll()方法,将引用从该队列移除,该引用变成inactive状态,表示可以回收 |
扩展
java中引用相关的内容:
通过引用计数算法判断对象的引用数量;通过可达性分析算法判断对象是否在引用链上;判定对象是否存活;
案例
# 强引用 strong reference
Object obj = new Object();
// new了一个新的对象,并将其赋值给obj,那么obj就是new Object()的强引用;
# 软引用 SoftReference
// SoftReference = public class SoftReference extends Reference
// 构造函数 = public SoftReference(T referent) 参数软引用对象、public SoftReference(T referent,ReferenceQueue super T> Q)是用来存储封装的待回收Reference对象的,ReferenceQueue中的对象是由Reference类中的ReferenceHandler内部类进行处理的。
Object obj = new Object();
SoftReference<Object> soft = new SoftReference<>(obj);
obj = null;
# 弱引用 WeakReference
// public WeakReference(T referent);public WeakReference(T referent, ReferenceQueue super T> q);与软引用类似
Object obj = new Object();
WeakReference<Object> weak = new WeakReference<Object>();
obj = null;
# 虚引用 phantomReference
// 构造函数 public PhantomReference(T referent, ReferenceQueue super T> q)
ReferenceQueue<Object> rp = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj,rq);
obj = null;
Reference
Reference是一个抽象类,每个Reference都有一个指向的对象,在Reference中有5个非常重要的属性:referent,next,discovered,pending,queue。
每个Reference都可以看成是一个节点,多个Reference通过next,discovered和pending这三个属性进行关联。
属性名 | 含义 |
---|---|
referent | Reference实际引用的对象 |
next | 创建ReferenceQueue 存放待回收的对象引用 |
discovered | 构建 Discovered List |
pending | 构建 Pending List |
Reference的四大状态
对于虚引用而言,GC会先将该引用放入ReferenceQueue队列中,程序员自行处理,若调用了ReferenceQueue.pull()后,该引用从队列中退出,此时引用的状态变成inactive状态。
状态 | 说明 |
---|---|
active | 如果改变状态,会变成inactive或pending状态 |
pending | 表示等待进入Queue,Reference内部有个ReferenceHandler,会调用enqueue方法,将Pending对象入到Queue中。 |
enqueued | 进入Queue对象的状态 |
inactive | 该状态下的Reference不能改变,会等待GC回收 enqueued状态的对象 通过poll方法弹出ReferenceQueue,该引用变成inactive状态 |
Reference 涉及到的三个queue或list
ReferenceQueue,本质是由Reference中的next连接而成,用来存储GC待回收的对象。
pendingList,待进入ReferenceQueue的list。
discovered List,在pending状态,就等于pending list;在Active状态的时候,discovered List实际上维持的是一个引用链。通过这个引用链,可以获得引用的链式结构,当某个Reference状态不再是Active状态时,需要将这个Reference从discovered List中删除。