JVM系列(十三):垃圾回收相关算法

1、标记阶段

 

1.1、垃圾标记阶段

  • 堆中存放着对象实例,垃圾回收之前,首先要区分内存中哪些存活、哪些死亡,标记为死亡的才垃圾回收,释放内存。
  • 如何标记死亡对象?当一个对象不再被任何存活的对象引用时,宣布死亡
  • 有两种方式:引用计数算法可达性分析算法

1.2、引用计数算法

1.2.1、介绍

  • 为每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况
  • 被引用,则计数器+1;引用失效,则-1
  • 优点:实现简单、垃圾对象便于识别、判定效率高、回收没有延迟性(随时可以回收)
  • 缺点:
  1. 需要增加存储计数器属性属性,增加内存开销(不多);
  2. 需要更新计数器属性,耗时间,增加时间开销(不多);
  3. 存在一个严重问题,无法处理循环引用情况,这是致命的,所以Java没有采用这个算法;

1.2.2、循环引用问题

  1. 情况1:p指针指向对象1,然后对象1指向同类型对象2,对象2又指向同类型对象1(构成循环引用),此时尚可回收;
  2. 情况2:在情况1的基础上,将指针p指向空或者其他对象,那么对象1-3就没有被引用了,但是计数器属性值不为零无法回收,造成内存泄漏。

  • 引用计数器算法,Java没用,python用了,因为引用计数器算法速度快
  • Python如何解决循环引用问题?
  1. 手动解除,在合适机会解除引用关系
  2. 使用弱引用weakref,weakref是python提供的标准库,用以解决循环引用

1.3、可达性分析算法(亦称根搜索算法、追踪性垃圾收集

  • GC Root根集合:是一组必须活跃的引用

1.3.1、基本思路

  • 以根对象集合为起始点,按照从上到下的方式搜索被根对象集合所连接的对象是否可以到达(搜索到)
  • 搜索目标对象后,内存中存活的对象都会被根对象直接或间接连接,搜索时走过的路径称为引用链
  • 如果没有被根对象直接或间接连接,那么不可达,以为这该对象已经死亡,可以回收

1.3.2、GC Root中包含元素

  • 虚拟机(本地变量表)中引用的对象(方法参数、局部变量表等)
  • 本地方法栈JNI引用的对象
  • 方法区(其实1.8以后是堆了)中静态属性引用的对象
  • 方法区(其实1.8以后是堆了)中常量引用的对象(如字符串常量池的引用)
  • 同步锁synchronized持有的对象
  • Java虚拟机内部引用的对象,比如数据类型对应的Class对象(Double),常驻的异常对象(NullPointerException、OutOfMemoryError),系统加载类等
  • 反应java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存
  • 除了以上固定的元素之外,还有一些临时性的对象,根据用户所选的垃圾收集器以及当前回收内存区域不同来决定的,比如:分代收集和局部回收,在java堆某个区域中的对象完全有可能被位于堆中其他区域的对象引用,这时候需要将这些关联区域中的对象一起加到GC Root集合中,保证可达性分析的正确性
  • 记忆Tip: GC Root中采用栈方式存放变量和指针,如果一个指针保存了堆内存里面的对象,但是自己又不存放在堆内存中,那么它就是一个Root

1.3.3、注意

  • 如果使用可达性分析算法,那么分析过程中,不能进行操作,要在一个保持一致性的快照中进行,不满足这个条件,那么分析结果的准确性无法保证。
  • 所以在GC的时候,需要STW(即使在号称几乎不停顿的CMS收集器中,在枚举根节点的时候也必须停顿

2、对象的finalization机制

2.1、引入finalization

  • Java提供Finalization对象终止机制来允许开发人员在对象被销毁前自定义操作
  • 当垃圾回收器发现一个对象没有被GC Root直/间接引用时,会先调用这个对象的finalization()方法(只会调用一次)
  • Finalization()方法允许子类重写,用于在对象被回收时进行一些操作,比如资源回收(关闭文件、关闭Socket、关闭数据库连接)
  • Finalize ()交给GC回收器调用,自己不要主动调用finazation()
  • Finalize ()方法可能使得对象复活
  • Finalize ()什么时候执行时不确定的,因为GC的时候才会调用,极端情况下,一直不GC,那么一直不调用(由低优先级的finalize线程调用)
  • Finalize ()方法的执行可能影响GC效率(重写前方法啥也不干,重写由用户自定义,在自定义时做了一些操作,可以使得对象重新被引用)

2.2、Java对象三种状态

  • 可触及的:GC Root直/间接引用的对象
  • 可复活的:GC Root没有直/间接引用的对象,但是经过finalize方法可能复活
  • 不可触及的:GC Root没有直/间接引用的对象,finalize()调用以后还不能复活的对象,就只能GG了(不可触及的对象活不了了,因为finalize方法只调用一次)
  • 由于存在了finalize方法,只有不可触及状态的对象才会被回收

2.3、对象obj回收过程(经历两次标记过程)

  • Obj到 GC Root没有引用链,第一次标记
  • 判断是否调用finalize()方法:
  1. 如果对象obj没有重写finalize()方法,或者finalize()方法已经被调用一次,那么obj直接不可触及(没有重写的话,原方法就是啥也不执行)
  2. 如果重写了finalize方法,finalize方法未执行过,那么将对象obj插入一个F-Queue队列,该队列由虚拟机创建
  3. finalize方法是obj逃离死亡的最后机会,稍后GC会对F-Queue队列中的对象进行二次标记。如果obj在finalize方法中与引用链上任何对象发生了联系,那么在第二次标记时,obj被移出“即将回收”集合,逃脱死亡。之后,如果对象再次出现没有被GC Root引用的情况,那么finalize不会执行了,对象直接转为不可触及状态,因为finalize只执行一次

3、垃圾清除阶段

  • 标记好存活、死亡对象后,需要执行垃圾回收,释放死亡对象的内存
  • 常用算法:标记-清除算法、复制算法、标记-压缩算法

3.1、标记-清除算法(Mark-Sweep)

3.1.1、标记-清除算法执行过程

  • 标记:Collector从引用根节点遍历所有被对象,并在对象的Header中记录该对象为可达对象(标记的是引用对象)
  • 清除:Collector对堆内存从头到尾进行线性遍历,发现某对象Header没有标记为可达对象,将其回收。这里的清除并不是真的置空,而是把需要清除的对象的地址保存在空闲列表地址中,下次有新对象时,判断垃圾的地址够不够,够就放。

3.1.2、优缺点

  • 优点:基础、常见,简单
  • 缺点:两次遍历,效率一般;GC时需要STW,用户体验差;清理的空间是零碎的,产生内存碎片,需要维护一个空闲列表

3.2、复制算法(Copying)

3.2.1、核心思想​​​​​​

  • 将内存分为两块,每次使用其中一块,垃圾回收时将该内存块中存活的对象复制到未使用的另一内存块,然后清除正在使用的内存块中的所有对象,再交换两个内存块的角色。

3.2.2、优缺点

  • 效率高
  • 复制后空间连续,无碎片
  • 需要两倍空间
  • 比如栈中对对象的引用,在复制的时候,需要更改引用地址,维护引用关系,这时候内存和时间开销都不小

3.2.3、应用场景

  • 如果垃圾特别多,存活的对象不多的时候,要复制的对象不多,那么这个时候用复制算法就合适;
  • 如果存活的对象多,那么要复制的对象就多,这个时候复制算法就不合适
  • 大部分对象都是朝生夕死,所以在新生代中使用复制算法和好,要复制的对象不多,从Eden区到S区,然后S区中在复制交换,量都不是很大,因为大部分对象朝生夕死。
  • 老年代中就不合适使用了,因为大部分老年代的都不死

3.3、标记-压缩(整理)算法

3.3.1、出现原因

  • 标记-清除算法可以效率低、产生内存碎片
  • 复制算法占用空间多、老年代不适合

3.2.2、标记-压缩(整理)算法执行过程

  • 第一阶段,标记所有被引用的对象
  • 第二阶段,将所有存活对象压缩到内存的一端,按顺序排放,之后清理边界以外的空间

3.3.4、标记-压缩与标记-清除算法

  • 标记-压缩算法,相当于在标记-清除算法完成后再进行内存碎片整理,等效于标记-清除-压缩算法(Mark-Sweep-Compact)
  • 标记-清除算法属于非移动式回收,标记-压缩属于移动式回收,移动对象需要更改引用,具有风险
  • 标记-清除需要维护一个空闲列表,但是标记-压缩后,没有了碎片,可以只保存指向空闲块的一个指针,减少了开销

3.3.5、指针碰撞

  • 内存有序排放,使用和未使用的互不交叉,彼此之间都有一个指针标记起始点。当新对象分配内存时,需要修改指针的偏移量,将新对象分配在第一个空闲内存,这种分配方式称为指针碰撞

3.3.6、标记-压缩算法优缺点

  • 解决了标记-清除算法中内存碎片的问题,不需要空闲列表了,只需要一个指针
  • 消除了复制算法中内存减半的代码
  • 对象移动时,可能需要调整引用关系,调整引用地址
  • 移动时需要STW


4、分代收集算法

  • 不同生命周期的对象采用不同的收集方式,提高效率
  • 年轻代:区域较小,对象生命周期短,存活率低,回收频繁。所以可以使用复制算法
  • 老年代:区域较大,对象生命周期长,存活率高,回收不那么频繁。所以使用标记-清除或者标记-压缩算法
  1. Mark阶段的工作量与存活对象成正比(标记存活对象);
  2. Sweep阶段的工作量与管理的区域大小成正相关(其实更多是死亡对象?不是的,因为清除的时候要扫描整个堆);
  3. Compact阶段的工作量与存活对象数量成正比;

5、增量收集算法

5.1、出现原因

  • 现有算法中,垃圾回收都会出现STW,所有应用线程被挂起。如果GC时间过长,那么影响用户体验或者系统的稳定性,增量收集算法应运而生。

5.2、基本思想

  • 垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域内存,然后切换到应用程序线程,依次反复,知道GC完成。

  • 增量收集算法的基础还是标记-清除和复制算法,通过对线程冲突妥善处理,允许垃圾收集线程用分阶段的方式完成标记、清理、复制工作

5.3、优缺点

  • 优点:GC时间断性执行用户线程,减少用户等待时间
  • 缺点:线程切换消耗时间空间,使得GC总成本上升,且由于切换线程导致系统吞吐量下降

6、分区算法

  • 堆越大,一次GC时间越长,所以为了控制GC时间,可以将一块大的内存区域分割为多个小块,每次根据允许的时间,回收若干个小区块。
  • 分代算法是按照对象的生命周期将区分为年轻代和老年代,分区算法是将整个堆分为连续的不同小区间,每个区独立使用,独立回收,每次回收可以控制一次回收多少个小区间。每个小区间可以是年轻代,也可以是老年代

使用Python来安装geopandas包时,由于geopandas依赖于几个其他的Python库(如GDAL, Fiona, Pyproj, Shapely等),因此安装过程可能需要一些额外的步骤。以下是一个基本的安装指南,适用于大多数用户: 使用pip安装 确保Python和pip已安装: 首先,确保你的计算机上已安装了Python和pip。pip是Python的包管理工具,用于安装和管理Python包。 安装依赖库: 由于geopandas依赖于GDAL, Fiona, Pyproj, Shapely等库,你可能需要先安装这些库。通常,你可以通过pip直接安装这些库,但有时候可能需要从其他源下载预编译的二进制包(wheel文件),特别是GDAL和Fiona,因为它们可能包含一些系统级的依赖。 bash pip install GDAL Fiona Pyproj Shapely 注意:在某些系统上,直接使用pip安装GDAL和Fiona可能会遇到问题,因为它们需要编译一些C/C++代码。如果遇到问题,你可以考虑使用conda(一个Python包、依赖和环境管理器)来安装这些库,或者从Unofficial Windows Binaries for Python Extension Packages这样的网站下载预编译的wheel文件。 安装geopandas: 在安装了所有依赖库之后,你可以使用pip来安装geopandas。 bash pip install geopandas 使用conda安装 如果你正在使用conda作为你的Python包管理器,那么安装geopandas和它的依赖可能会更简单一些。 创建一个新的conda环境(可选,但推荐): bash conda create -n geoenv python=3.x anaconda conda activate geoenv 其中3.x是你希望使用Python版本。 安装geopandas: 使用conda-forge频道来安装geopandas,因为它提供了许多地理空间相关的包。 bash conda install -c conda-forge geopandas 这条命令会自动安装geopandas及其所有依赖。 注意事项 如果你在安装过程中遇到任何问题,比如编译错误或依赖问题,请检查你的Python版本和pip/conda的版本是否是最新的,或者尝试在不同的环境中安装。 某些库(如GDAL)可能需要额外的系统级依赖,如地理空间库(如PROJ和GEOS)。这些依赖可能需要单独安装,具体取决于你的操作系统。 如果你在Windows上遇到问题,并且pip安装失败,尝试从Unofficial Windows Binaries for Python Extension Packages网站下载相应的wheel文件,并使用pip进行安装。 脚本示例 虽然你的问题主要是关于如何安装geopandas,但如果你想要一个Python脚本来重命名文件夹下的文件,在原始名字前面加上字符串"geopandas",以下是一个简单的示例: python import os # 指定文件夹路径 folder_path = 'path/to/your/folder' # 遍历文件夹中的文件 for filename in os.listdir(folder_path): # 构造原始文件路径 old_file_path = os.path.join(folder_path, filename) # 构造新文件名 new_filename = 'geopandas_' + filename # 构造新文件路径 new_file_path = os.path.join(folder_path, new_filename) # 重命名文件 os.rename(old_file_path, new_file_path) print(f'Renamed "{filename}" to "{new_filename}"') 请确保将'path/to/your/folder'替换为你想要重命名文件的实际文件夹路径。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值