JVM-JVM的垃圾回收机制

一,JVM的垃圾回收机制

IDEA 控制台输出JVM的GC日志,在 VM options 添加 -XX:+PrintGCDetails 即可

1.1 如何判定垃圾对象
1.1.1 引用计数法

​ 在每个对象都维护着一个内存字段来统计它被多少”部分”使用—引用计数器,每当有一个新的引用指向该对象时,引用计数器就+1 ,每当指向该引用对象失效时该计数器就-1 ,当引用数量为0的时候,则说明对象没有被任何引用指向,可以认定是”垃圾”对象.

案例:

public static void main(String[] args) {
        GcObject obj1 = new GcObject(); //Step1
        GcObject obj2 = new GcObject();//Step2
        obj1.instance = obj2; //Step3
        obj2.instance = obj1;// //Step4
        obj1 = null; //Step5
        obj2 = null; //Step6
    }

第一步:GcObject实例1被obj1引用,所以它的引用数+1,为1

第二步:GcObject实例2被obj2引用,所以它的引用数+1,为1

第三步:obj1的instance属性指向obj2,而obj2指向GcObject实例2,故GcObject实例2引用+1,为2

第四步:obj2的instance属性指向obj1,而obj1指向GcOjbect实例1,故GcObject实例1引用+1,为2

到此前4步, GcOjbect实例1和GcOjbect实例2的引用数量均为2,此时结果图如下:

第五步:obj1不再指向GcOjbect实例1,其引用计数减1,结果为1.

第六步:obj2不再指向GcOjbect实例2,其引用计数减1,结果为1.

​ 到此,发现GcObject实例1和实例2的计数引用都不为0,他们的成员便令之间相互引用,但是外界没有办法访问,如果采用的引用计数算法的话,因为它们的计数引用都不为0,那么这两个实例所占的内存将得不到释放,这便产生了内存泄露。

1.1.2 可达性分析法

​ 可达性分析法基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

案例:

如上图所示,object1~object4对GC Root都是可达的,说明不可被回收,object5和object6对GC Root节点不可达,说明其可以被回收。

可作为GC Root的对象:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象
1.2 如何回收垃圾对象
1.2.1 标记清除算法(基础)

思想:顾名思义,本算法需要先标记出所有需要回收的对象,待标记完成后,再统一回收所有被标记的对象。

缺点:效率问题,清除效率不高;空间问题,会产生内存碎片

1.2.2 复制算法(针对新生代)

​ 为了方便垃圾回收,将java堆分为新生代,老年代;将新生代划分为Eden(伊甸园),Survivor(存活区2块From和To);

​ 一开始就会将可用内存分为两块,from域和to域, 每次只是使用from域和to域中的一个域。当From域或者To内存不够了,开始执行GC操作,这个时候,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理。

  1. 当Eden区满的时候,会触发第一次young gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发young gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
  2. 当后续Eden又发生young gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
  3. 可见部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认值和最大值都是15(jvm对象头中GC标记是4bit)),最终如果还是存活,就存入到老年代。
1.2.3 标记整理算法(针对老年代)

​ 标记整理算法和标记清除算法很像,唯一不同的是,当标记完成后,不是清理掉需要回收的对象,而是将所有存活的对象向一端移动,然后将边界以外的内存全部清理掉,这样可以有效避免空间碎片的产生。

1.2.4 分代收集算法

​ 分代算法(Generational Collection),现代的虚拟机大都采用了这种gc回收算法,通过将内存根据存活时间的不同划分为不同代,来选择最合适的对象回收算法,一般来说是分为新生代和老年代。

  • 新生代的对象更新很快,朝生夕死,所以使用复制算法.
  • 老年代的对象存活率很高,所以采用标记整理算法.

二,其它

2.1 如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

类需要同时满足下面3个条件才能算是 “无用的类” :

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
2.2 常见引用类型

java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用

①,强引用:普通的变量引用

public static User user = new User(); 

②,软引用:软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。软引用可用来实现内存敏感的高速缓存。

 public static SoftReference user = new SoftReference(new User()); 

③,弱引用:弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收,将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用 。

public static WeakReference user = new WeakReference(new User()); 

④,虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用。

2.3 内存担保

​ 新生代Minor GC后剩余存活对象太多,无法放入Survivor区中,此时由老年代做内存担保,将这些存活对象直接转移到老年代去。

  1. 执行任何一次Minor GC之前,JVM会先检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,因为在极端情况下,可能新生代Minor GC之后,新生代所有对象都需要存活,那就会造成新生代所有对象全部要进入老年代;
  2. 如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起一次Minor GC,因为Minor GC之后即使所有对象都存活,Survivor区放不下了,也可以转移到老年代去;
  3. 如果老年代的可用空间已经小于新生代的全部对象总大小,那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小.
  4. 如果判断发现老年代的内存大小,大于之前每一次Minor GC后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下Minor GC,但是此时真的可能有风险,那就是Minor GC过后,剩余的存活对象的大小,大于Survivor空间的大小,也大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次“Full GC”;

​ 内存担保机制通过,冒险尝试一下Minor GC,Minor GC过后,剩余的存活对象的大小,如果小于老年代可用空间,可以减少一次full GC,所以老年代空间分配担保机制的目的,也是为了避免频繁进行Full GC.

2.4 记忆集与卡表

​ 在垃圾收集过程中,会存在一种现象,即跨代引用,在G1垃圾收集器中,又叫跨Region引用。如果是年轻代指向老年代的引用我们不用关心,因为即使Minor GC把年轻代的对象清理掉了,程序依然能正常运行,而断掉引用链的老年代对象,老年代对象会被后续的Major GC回收,但是如果是老年代指向年轻代的引用,年轻代的对象在Minor GC阶段是不能被回收掉的,那如何解决这个问题呢?

​ 首先,跨代引用相对于同代引用来说仅占极少数。原因是跨代引用的对象应该倾向于同时生存或者同时死亡的,例如:如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,变成同代引用,这时跨代引用也随即被消除了。

​ 依据上面说所,就不应再为了少量的跨代引用去扫描整个老年代中的每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构,该结构被称为记忆集。通过老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用,此后当发生Minor GC时,通过判断标识,将标识为存在跨代引用的小块内存里的对象,加入到GCRoots进行扫描,虽然这种方法需要在对象改变引用关系如老年代中对象某个属性赋值更改时,需要维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

​ 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。如果我们不考虑效率和成本的话,最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构,如下面代码所示:

//以对象指针来实现记忆集的伪代码
Class RememberedSet {
	Object[] set[OBJECT_INTERGENERATIONAL_REFERENCE_SIZE]; 
}

​ 这种记录全部含跨代引用对象,无论是空间占用还是维护成本都相当高昂,而在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。

  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。

  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

    “卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集,这也是目前最常用的记忆集的实现形式

HotSpot虚拟机定义的卡表只是一个字节数组。以下这行代码是HotSpot默认的卡表标记逻辑:

CARD_TABLE [this address >> 9] = 0;

​ 字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作“卡页”(Card Page)。一般来说,卡页大小都是以2的N次幂的字节数,通过上面代码可 以看出HotSpot中使用的卡页是2的9次幂,即512字节(地址右移9位,相当于用地址除以512)。那如果卡表标识内存区域的起始地址是0x0000的话,数组CARD_TABLE的第0、1、2号元素,分别对应了地址范围为0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡页内存块,如下图所示:

​ 一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的卡页,把它们加入GC Roots中一并扫描。

那虚拟机是何时让卡表元素变脏呢?又是如何维护卡表元素的呢?

写屏障

​ 卡表元素何时变脏的答案是很明确的——有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻,把维护卡表的动作放到每一个赋值操作之中。

在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切 面,在引用对象赋值时会产生一个环形(Around)通知,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值 后的则叫作写后屏障(Post-Write Barrier)。下面是简化的代码逻辑:

void oop_field_store(oop* field, oop new_value) {
 // 引用字段赋值操作 
 *field = new_value; 
 // 写后屏障,在这里完成卡表状态更新 
 post_write_barrier(field, new_value); 
 }

​ 应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。

总结
记忆集与卡表
HotSpot虚拟机是用记忆集来记录某块内存区域是否包含跨代引用的对象。记忆集是抽象概念,而卡表是记忆集的实现,卡表是用字节数组实现的,卡表数组的每个元素都是代表某块具体内存区域,这个内存区域叫卡,卡页的大小是512字节,代表一块特定大小的内存块,若在这块内存块中有一个或多个的跨代指针,则将对应的卡表元素标为1,代表“变脏”,否则为0。当虚拟机扫描卡表元素为1时,便将对应的卡页内存区域加入到GC ROOT中一并扫描。

写屏障
使用写屏障来实现卡表元素变脏。写屏障分为写前屏障和写后屏障,大多数垃圾收集器都是使用写后屏障(G1使用写前屏障)。写后屏障具体表现在对引用对象赋值时,如果是跨代引用,则通过写后屏障将对应的卡表元素变脏。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/428550.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Python】进阶学习:pandas--rename()用法详解

【Python】进阶学习:pandas-- rename()用法详解 🌈 个人主页:高斯小哥 🔥 高质量专栏:Matplotlib之旅:零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程👈 希望得到您的…

day34贪心算法 part03

1005. K 次取反后最大化的数组和 简单 给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组: 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。 重复这个过程恰好 k 次。可以多次选择同一个下标 i 。 以这种方式修改数组后,返回数…

软考57-上午题-【数据库】-数据库的控制功能

一、事务管理 1-1、事务的定义 事务是一个操作序列,这些操作,要么都做,要么都不做。 事务和程序是两个不同的概念,一般一个程序可以包含多个事务。 1-2、事务定义的语句 1、事务开始:BEGIN TRANSACTION 2、事务提…

LabVIEW齿轮传动健康状态静电在线监测

LabVIEW齿轮传动健康状态静电在线监测 随着工业自动化的不断发展,齿轮传动作为最常见的机械传动方式之一,在各种机械设备中发挥着至关重要的作用。然而,齿轮在长期运行过程中易受到磨损、变形等因素影响,进而影响整个机械系统的稳…

蓝桥杯集训·每日一题2024 (差分)

前言&#xff1a; 差分笔记以前就做了&#xff0c;在这我就不再写一遍了&#xff0c;直接上例题。 例题&#xff1a; #include<bits/stdc.h> using namespace std; int a[10009],b[100009]; int main(){int n,ans10,ans20;cin>>n;for(int i1;i<n;i){cin>>…

数字经济的新机遇:揭秘Web3的商业价值

引言&#xff1a; 随着技术的飞速发展和互联网的日益普及&#xff0c;数字经济已经成为了当今社会的重要组成部分。而在数字经济的蓬勃发展中&#xff0c;Web3技术被认为是一个颠覆性的力量&#xff0c;它不仅重新定义了数字世界的基础架构&#xff0c;还为商业创新带来了巨大…

嵌入式学习第二十四天!(进程间通信:消息队列、共享内存、信号灯)

进程间的通信&#xff1a; 消息队列、共享内存、信号灯&#xff1a; 1. IPC对象&#xff1a;内存文件 1. ipcs&#xff1a; 查看系统中的消息队列&#xff0c;共享内存、信号灯的信息 2. ipcrm&#xff1a; 删除消息队列、共享内存、信号灯 ipcrm -Q/-M/-S key ipcrm -q/-m/-s…

linux安装部署

jdk&tomcat安装 1.上传jdk、tomcat安装包 2.解压两个工具包 #解压tar -zxvf apache-tomcat-8.5.20.tar.gz#解压jdktar -zxvf jdk-8u151-linux-x64.tar.gz 3.配置并且测试jdk安装 #配置环境变量vim /etc/profile​#java environmentexport JAVA_HOME/soft/jdk1.8.0_151exp…

Whisper实现语音识别转文本

#教程 主要参考开源免费离线语音识别神器whisper如何安装&#xff0c; OpenAI开源模型Whisper——音频转文字 Whisper是一个开源的自动语音识别系统&#xff0c;它在网络上收集了680,000小时的多语种和多任务监督数据进行训练&#xff0c;使得它可以将多种语言的音频转文字。…

【学位论文】上海交通大学 研究生学位论文 本地保存

上海交大研究生学位论文网&#xff1a;http://thesis.lib.sjtu.edu.cn/ &#xff08;只能校内访问或SJTU VPN访问&#xff09; 如果希望下载论文&#xff0c;需要参考&#xff1a;https://github.com/olixu/SJTU_Thesis_Crawler 安装过程 安装过程的几个坑&#xff1a; &a…

【Java开发】Java实现调用微信机器人,发送企业微信通知

请直接看原文: 【Java开发】Java实现调用微信机器人&#xff0c;发送企业微信通知_java 企业微信推送机器人消息-CSDN博客 ------------------------------------------------------------------------------------------------------------------------------- 企业微信机器…

无需安装!7款一键在线UI设计利器

制作完原型后&#xff0c;需要优化界面。此时是UI设计师的任务。UI设计软件对设计师来说非常重要。UI设计工具的使用是否直接影响到最终结果的质量&#xff0c;所以有人会问:UI界面设计使用什么软件&#xff1f;这里有一些UI设计师和对UI设计感兴趣的朋友列出了五款好用免费的U…

Unity 动态加载音频和音效

想要加载音效和音频需要两个组件&#xff1a; 听&#xff1a; 播&#xff1a; 一收一发 在层级中&#xff0c;右键创建 音频源 &#xff0c;放入物体的子物体中。 播放 方式一 拖动需要播放的音频文件到&#xff0c;音频源组件中。 using System.Collections; using Syst…

java BIO

目录 Java BIO基本介绍 Java BIO工作机制 传统的BIO编程实例回顾 1、BIO模式下发送和接收消息 2、BIO模式下多发和多收消息 3、BIO模式下接收多个客户端 伪异步I/O编程 基于BIO形式下的文件上传 Java BIO模式下的端口转发思想 Java BIO基本介绍 Java BIO就是传统的jav…

Pytorch学习 day03(Tensorboard)

Tensorboard Tensorboard能够可视化loss的变化过程&#xff0c;便于我们查看模型的训练状态&#xff0c;也能查看模型当前的输入和输出结果 在Pycharm中&#xff0c;可以通过按住ctrl&#xff0c;并左键点击某个库来进入源文件查看该库的使用方法 SummaryWriter是用来向log_di…

C语言:指针(二)

目录 1.数组名的理解2.使用指针访问数组3.一维数组传参的本质4.二级指针5.指针数组6.字符指针变量7.数组指针变量8.二维数组传参的本质9.函数指针变量10.函数指针数组11.回调函数12.qsort函数13.使用回调函数模拟实现qsort函数 1.数组名的理解 int main() {int arr[] { 1,2,3…

上帝视角看GPU(5):图形流水线里的不可编程单元

【GPU】图形流水线基础【GPU】逻辑上的模块划分【GPU】部署到硬件【GPU】完整的软件栈 前几期我们过了一遍GPU的软硬栈。这次我们将深入GPU图形流水线的一些细节&#xff0c;看看那些不可编程的模块是怎么工作的。 对于GPU的图形流水线来说&#xff0c;最核心最重要的一个组件就…

通过人工智能增强的对话建立有意义的联系

人工智能如何重塑我们的交流&#xff1f;2024年最新对话AI趋势 在技术和人类互动比以往任何时候都更加复杂地交织在一起的时代&#xff0c;人工智能增强的对话已成为建立有意义的联系的关键要素。 这种转变不仅关乎效率&#xff0c;还关乎效率。 这是为了丰富沟通的结构。 在这…

MATLAB--pie函数绘制复杂分类饼图(2)--附案例代码

MATLAB–pie函数绘制复杂分类数据的饼状图 目录 MATLAB--pie函数绘制复杂分类数据的饼状图摘要1. 问题描述2. 具体步骤&#xff1a;3. 绘制结果4. 小结 摘要 在数据可视化中&#xff0c;饼状图是一种常用的展示分类数据的方式。之前&#xff0c;文章介绍了使用MATLAB绘制饼状图…

Vue中的计算属性和方法有什么区别?

Vue.js是一款流行的JavaScript前端框架&#xff0c;提供了丰富的功能和便捷的开发方式。在Vue中&#xff0c;计算属性和方法是常用的两种方式来处理数据和逻辑。但它们之间存在一些区别&#xff0c;本文将详细介绍Vue中计算属性和方法的区别&#xff0c;并通过示例代码加深理解…