百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

深入理解Java虚拟机(JVM) --- 垃圾收集算法(中)

bigegpt 2024-08-05 11:52 10 浏览

2 回收无效对象的过程

当经可达性算法筛选出失效的对象之后,并不是立即清除,而是再给对象一次重生的机会

  • 判断是否覆盖finalize()
  • 未覆盖该或已调用过该方法,直接释放对象内存
  • 已覆盖该方法且还未被执行,则将finalize()扔到F-Queue队列中
  • 执行F-Queue中的finalize() 虚拟机会以较低的优先级执行这些finalize(),不会确保所有的finalize()都会执行结束 如果finalize()中出现耗时操作,虚拟机就直接停止执行,将该对象清除
  • 对象重生或死亡
  • 如果在执行finalize()方法时,将this赋给了某一个引用,则该对象重生
  • 如果没有,那么就会被垃圾收集器清除
注意:强烈不建议使用finalize()进行任何操作! 如果需要释放资源,请用try-finally或者其他方式都能做得更好. 因为finalize()不确定性大,开销大,无法保证各个对象的调用顺序.

以下代码示例看到:一个对象的finalize被执行,但依然可以存活

/**
 * 演示两点:
 * 1.对象可以在被GC时自救
 * 2.这种自救机会只有一次,因为一个对象的finalize()最多只能被系统自动调用一次,因此第二次自救失败
 * @author sss
 * @since 17-9-17 下午12:02
 *
 */
public class FinalizeEscapeGC {
 private static FinalizeEscapeGC SAVE_HOOK = null;
 private void isAlive() {
 System.out.println("yes,I am still alive :)");
 }
 @Override
 protected void finalize() throws Throwable {
 super.finalize();
 System.out.println("finalize methodd executed!");
 FinalizeEscapeGC.SAVE_HOOK = this;
 }
 public static void main(String[] args) throws InterruptedException {
 SAVE_HOOK = new FinalizeEscapeGC();
 // 对象第一次成功自救
 SAVE_HOOK = null;
 System.gc();
 // 因为finalize方法优先级很低,所以暂停0.5s以等待它
 Thread.sleep(500);
 if (SAVE_HOOK != null) {
 SAVE_HOOK.isAlive();
 } else {
 System.out.println("no,I am dead :(");
 }
 // 自救失败
 SAVE_HOOK = null;
 System.gc();
 Thread.sleep(500);
 if (SAVE_HOOK != null) {
 SAVE_HOOK.isAlive();
 } else {
 System.out.println("no,I am dead :(");
 }
 }
}

运行结果

finalize methodd executed!
yes,I am still alive :)
no,I am dead :(

3 方法区的内存回收

使用复制算法实现堆的内存回收,堆被分为新生代和老年代

  • 新生代中的对象"朝生夕死",每次垃圾回收都会清除掉大量对象
  • 老年代中的对象生命较长,每次垃圾回收只有少量的对象被清除

由于方法区中存放生命周期较长的类信息、常量、静态变量. 因此方法区就像堆的老年代,每次GC只有少量垃圾被清除.

方法区中主要清除两种垃圾

  • 废弃常量
  • 无用类

3.1 回收废弃常量

回收废弃常量和回收对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除.

3.2 回收无用类

判定无用类的条件则较为苛刻

  • 该类所有实例都已被回收 即Java堆不存在该类的任何实例
  • 加载该类的ClassLoader已被回收
  • 该类的java.lang.Class对象没有被任何对象或变量引用,无法通过反射访问该类的方法 只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class.这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除.

4 垃圾收集算法

4.1 清除(Sweep)

最基础的收集算法,后续算法也都是基于此并改进其不足而得.

该算法会从每个GC Roots出发,依次标记有引用关系的对象,最后将没有被标记的对象清除

把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中 当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。

不足

清除这种回收方式的原理及其简单,但是有两个缺点

内存碎片

由于Java虚拟机的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。

分配效率较低

如果是一块连续的内存空间,那么我们可以通过指针加法(pointer bumping)来做分配 而对于空闲列表,Java虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。

第二种是压缩(compact),即把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。 这种算法会带来大量的空间碎片,导致需要分配一个较大连续空间时容易触发FullGC,降低了空间利用率.

为了解决这个问题,又提出了“标记-整理算法”,该算法类似计算机的磁盘整理,首先会从GC Roots出发标记存活的对象,然后将存活对象整理到内存空间的一端,形成连续的已使用空间,最后把已使用空间之外的部分全部清理掉,这样就不会产生空间碎片的问题

4.2 复制算法(Copy)

把内存区域分为两等分,分别用两个指针from和to来维护,并且只是用from指针指向的内存区域来分配内存。 当发生垃圾回收时,便把存活的对象复制到to指针指向的内存区域中,并且交换from指针和to指针的内容。复制这种回收方式同样能够解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下。

将内存分成大小相等两份,只将数据存储在其中一块上

  • 当需要回收时,首先标记废弃数据
  • 然后将有用数据复制到另一块内存
  • 最后将第一块内存空间全部清除

4.2.1 分析

  • 这种算法避免了空间碎片,但内存缩小了一半.
  • 每次都需将有用数据全部复制到另一片内存,效率不高

4.2.2 解决空间利用率问题

堆内存空间分为较大的Eden和两块较小的Survivor,每次只使用Eden和Survivor区的一块。这种情形下的“ Mark-Copy"减少了内存空间的浪费。“Mark-Copy”现作为主流的YGC算法进行新生代的垃圾回收。 在新生代中,由于大量对象都是"朝生夕死",也就是一次垃圾收集后只有少量对象存活 因此我们可以将内存划分成三块

  • Eden、Survior1、Survior2
  • 内存大小分别是8:1:1

分配内存时,只使用Eden和一块Survior1.

  • 当发现Eden+Survior1的内存即将满时,JVM会发起一次Minor GC,清除掉废弃的对象,
  • 并将所有存活下来的对象复制到另一块Survior2中.
  • 接下来就使用Survior2+Eden进行内存分配

通过这种方式,只需要浪费10%的内存空间即可实现带有压缩功能的垃圾收集方法,避免了内存碎片的问题.

4.2.3 分配担保

准备为一个对象分配内存时,发现此时Eden+Survior中空闲的区域无法装下该对象 就会触发MinorGC(新生代 GC 算法),对该区域的废弃对象进行回收.

但如果MinorGC过后只有少量对象被回收,仍然无法装下新对象

  • 那么此时需要将Eden+Survior中的所有对象都转移到老年代中,然后再将新对象存入Eden区.这个过程就是"分配担保".

在发生 minor gc 前,虚拟机会检测老年代最大可用连续空间是否大于新生代所有对象总空间 若成立,minor gc 可确保安全 若不成立,JVM会查看 HandlePromotionFailure是否允许担保失败

  • 若允许 那么会继续检测老年代最大可用的连续空间是否 > 历次晋升到老年代对象的平均大小
  • 若大于 则将尝试进行一次 minor gc,尽管这次 minor gc 是有风险的
  • 若小于或 HandlePromotionFailure 设置不允许冒险 改为进行一次 full gc (老年代GC)

4.3 压缩算法(Compact)

在回收前,标记过程仍与"清除"一样 但后续不是直接清理可回收对象,而是

  • 将所有存活对象移到一端
  • 直接清掉端边界之外内存
  • 分析
  • 这是一种老年代垃圾收集算法. 老年代中对象一般寿命较长,每次垃圾回收会有大量对象存活 因此如果选用"复制"算法,每次需要較多的复制操作,效率低

而且,在新生代中使用"复制"算法 当 Eden+Survior 都装不下某个对象时,可使用老年代内存进行"分配担保"

而如果在老年代使用该算法,那么在老年代中如果出现 Eden+Survior 装不下某个对象时,没有其他区域给他作分配担保

因此,老年代中一般使用"压缩"算法

4.4 分代收集算法(Generational Collection)

当前商业虚拟机都采用此算法. 根据对象存活周期的不同将Java堆划分为老年代和新生代,根据各个年代的特点使用最佳的收集算法.

  • 老年代中对象存活率高,无额外空间对其分配担保,必须使用"标记-清除"或"标记-压缩"算法
  • 新生代中存放"朝生夕死"的对象,用复制算法,只需要付出少量存活对象的复制成本,就可完成收集

5 Java中引用的种类

Java中根据生命周期的长短,将引用分为4类

  • 强引用 我们平时所使用的引用就是强引用 类似A a = new A(); 即通过关键字new创建的对象所关联的引用就是强引用 只要强引用还存在,该对象永远不会被回收
  • 软引用 一些还有用但并非必需的对象 只有当堆即将发生OOM异常时,JVM才会回收软引用所指向的对象. 软引用通过SoftReference类实现 软引用的生命周期比强引用短一些
  • 弱引用 也是描述非必需对象,比软引用更弱 所关联的对象只能存活到下一次GC发生前. 只要垃圾收集器工作,无论内存是否足够,弱引用所关联的对象都会被回收. 弱引用通过WeakReference类实现.
  • 虚引用 也叫幽灵(幻影)引用,最弱的引用关系. 它和没有引用没有区别,无法通过虚引用取得对象实例. 设置虚引用唯一的作用就是在该对象被回收之前收到一条系统通知. 虚引用通过PhantomReference类来实现.

总结

Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。它从一系列GC Roots出发,边标记边探索所有被引用的对象。

为了防止在标记过程中堆栈的状态发生改变,Java虚拟机采取安全点机制来实现Stop-the-world操作,暂停其他非垃圾回收线程。

回收死亡对象的内存共有三种方式,分别为:会造成内存碎片的清除、性能开销较大的压缩、以及堆使用效率较低的复制。

今天的实践环节,你可以体验一下无安全点检测的计数循环带来的长暂停。你可以分别测单独跑foo方法或者bar方法的时间,然后与合起来跑的时间比较一下。

// time java SafepointTestp
// 还可以使用如下几个选项
// -XX:+PrintGC
// -XX:+PrintGCApplicationStoppedTime 
// -XX:+PrintSafepointStatistics
// -XX:+UseCountedLoopSafepoints
public class SafepointTest {
 static double sum = 0;
 public static void foo() {
 for (int i = 0; i < 0x77777777; i++) {
 sum += Math.sqrt(i);
 }
 }
 public static void bar() {
 for (int i = 0; i < 50_000_000; i++) {
 new Object().hashCode();
 }
 }
 public static void main(String[] args) {
 new Thread(SafepointTest::foo).start();
 new Thread(SafepointTest::bar).start();
 }
}

参考

  • java的gc为什么要分代?
  • 深入理解Java虚拟机(第2版)
  • 深入拆解Java虚拟机

相关推荐

Linux 命令 ncftp(文件传输)——想玩转linux就请一直看下去

我是IT悟道,点击右上方“关注”,每天分享IT、科技、数码方面的干货。Linuxncftp命令Linux命令大全Linuxncftp命令用于传输文件。FTP让用户得以下载存放于服务器主机的文件,...

玩转 Linux 之:磁盘分区、挂载知多少?

今天来聊聊linux下磁盘分区、挂载的问题,篇幅所限,不会聊的太底层,纯当科普!!1、Linux分区简介1.1主分区vs扩展分区硬盘分区表中最多能存储四个分区,但我们实际使用时一般只分为两...

一文带你了解 Linux 文件权限,从基础到高级

在Linux中,每个文件和目录都关联了一组权限,定义了不同用户对其的访问能力。权限分为三类:读取(read,r)、写入(write,w)和执行(execute,x),分别用字母r、w、x...

Linux 使用 socat 让云服务器作为跳板机的方法

概念三台机器:客户端A(192.168.1.11)服务器B(192.168.1.88)跳板机C(192.168.1.32)实现A与B的双向数据传输,本该这样:A<...

Linux启动流程之ROM-CODE(linux启动详解)

1.从哪里开始?下图是AM335X核心板和功能框图:AM335X核心板的存储信息如下:AM335X核心板运行linux系统,在这里提出一个问题:上电后指令从哪里开始执行?DDRorEMMC?2....

「Linux」——select和epoll详解(linux epoll详解)

select和epoll详解select和epoll的区别(面试常考)select一、什么是select1.select函数原型2.参数解释3.参数timeout取值4.返回值5.监控原理二、sele...

Linux中使用输入输出和错误重定向, 赶紧收藏!

Linux中的每个进程都提供三个打开的文件(通常称为文件描述符),分别是标准的输入、输出和错误文件。StandardInput是键盘,抽象为文件,使编写脚本和程序更容易。StandardOut...

「正点原子Linux连载」第七十一章Linux 4G通信实验

1)实验平台:正点原子Linux开发板2)摘自《正点原子I.MX6U嵌入式Linux驱动开发指南》关注官方微信号公众号,获取更多资料:正点原子第七十一章Linux4G通信实验前面我们学习了如何在Li...

LSM Oops 内存错误根因分析与解决

作者简介:吴文涵,图形算法出身,同时热爱linux内核开发的工程师,喜欢推导并乐于分享。版权声明:本文最先发表于“泰晓科技”微信公众号,欢迎转载,转载时请在文章的开头保留本声明。Oops是...

连你家电器的算力都不放过,新发现Linux恶意软件用IoT设备挖矿

萧箫发自凹非寺量子位|公众号QbitAI继电脑和手机后,挖矿病毒也盯上了IoT设备。无论是智能冰箱、彩电还是洗衣机,但凡有点算力的(物联网和端侧)设备都可能被这种病毒感染,用于挖掘加密货币等...

Linux-AT命令干货分享,还不赶紧收藏!

苹果iOS 26锁屏大升级:更个性更沉浸 有五大亮点

【CNMO科技消息】CNMO注意到,苹果近日在iOS26开发者预览版中推出了多项锁屏界面创新功能,可以大幅提升用户个性化设置与操作便捷性。这些更新不仅优化了视觉体验,还通过技术手段增强了交互效率,为...

福彩 3D 第 2025178 期:心水407 !大小奇偶双平衡 + 跨度适配,速收藏

福彩3D第2025178期:497后和值回落!五维分析+形态调整策略福彩3D2025177期开奖号码497,组六形态,大小比2:1,奇偶比1:2,和值20,跨度5。面对大...

一加 Ace5 至尊版手机首发适配和平精英手游

7月8日消息,据用户反馈,一加Ace5至尊版手机开启新版本系统推送,升级包大小约6.83MB、版本号为15.0.2.215(CN01),适配了腾讯《和平精英》手游144Hz高刷。IT...

外媒称苹果今年秋季将推出超15款新品 远不止iPhone 17

【CNMO科技消息】2025年已过半程,有外媒指出苹果计划在今年秋季推出超过15款新产品,涵盖iPhone革新、M5芯片设备迭代、可穿戴设备升级及智能家居布局等。苹果1.iPhone17系列:产品...