java 虚拟机垃圾回收

本文总结自《深入理解Java虚拟机》一书。主要是对java虚拟机GC哪些内存区域以及如何回收等内容作出整理。

一、GC回收哪些区域

前文Java虚拟机内存划分与各区域OOM中描述了Java虚拟机内存的划分,包括:与线程有关的程序计数器、虚拟机栈和方法栈,以及与线程无关的堆、方法区。

其中,程序计数器、虚拟机栈以及方法栈生命周期与线程相关,线程结束时内存也跟着被回收。虚拟机栈中的栈帧随方法执行入栈出栈,其内存分配大小在类结构确定下来时已经确定,方法执行完内存也会回收。

而堆与方法区则是线程共享的,接口的不同实现类需要的内存,方法中的不同分支需要的内存都是动态分配的,需要在程序运行期间才能够知道哪些对象要创建,因此,垃圾收集关注的是这部分内存。

二、如何判断对象已死

1. 堆

引用计数算法

在对象中添加一个引用计数器,当被引用时+1,引用失效则-1,计数器值为0时则对象不再被使用。
优点:实现简单,效率高。
缺点:循环引用问题(objA.instance=objB;objB.instance=objA)很难解决。

可达性分析算法

通过一系列“GC Roots”对象作为起始点,向下搜索,走过的对象路径称为引用链,如果一个对象没有任何引用链相连,则此对象是不可用的。
其中,GC Roots节点选择:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象;
  • 方法区中静态类属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中Native方法引用的对象。

但是,即使在可达性分析中不可达的对象,也并非“非死不可”,要确定一个对象死亡,至少要经历两次标记过程:
可达性分析时发现对象没有与任何GC Roots相连的引用链连接,进行第一次标记,并进行筛选,看其是否有必要执行finalize()方法(标准是:对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,则都是没有必要执行的)。
如果有必要执行finalize()方法,对象加入F-Queue队列,并在稍后由一个虚拟机自动创建的、低优先级的Finalizer线程去执行它(执行是指触发finalize()方法,但是并不会等它执行完,以防止一个对象在finalize()方法中耗时太久或者死循环导致F-Queue队列中其他对象永久处于等待,甚至是导致整个内存回收系统崩溃),finalize()方法是对象自救的最后一次机会,稍后GC将对F-Queue队列中对象进行第二次小规模标记。如果此时,对象已经与其他对象进行关联(把自己赋值给别的对象),则会被移除出“即将回收”的集合,如果此时对象还没有自救成功,则基本上就被回收了。

另外再说一下这个引用。JDK1.2之前,java定义:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,则称这块内存代表着一个引用。
1.2之后,将引用划分为强引用、软引用、弱引用以及虚引用:

  • 强引用:Object obj = new Object()这种就是强引用,只要强引用存在,GC就不会回收掉被引用的对象;
  • 软引用:SofReference类实现的引用,用来描述一些有用但并非必须的对象。在系统要发生内存溢出异常前,将会把软引用对象列进回收范围进行第二次回收。如果第二次回收完还是没有足够内存,才抛出内存溢出异常。
  • 弱引用:用WeakReference实现的引用,用于描述非必需对象。弱引用对象只能生存到下一次GC前,当GC时,无论当前内存是否足够,都会回收掉弱引用关联的对象。
  • 虚引用:用PhantomReference实现的引用,也叫幽灵引用或幻影引用,能在被GC时收到一个系统通知,虚引用关联的对象不会影响其GC。

2. 方法区

Java虚拟机规范中不要求虚拟机在方法区实现GC,而且方法区中GC性价比较低:堆中,GC一般可回收70%-95%的空间,而永久代中效率远低于此。
永久代中GC主要包括两部分:

  • 废弃常量
    与堆中对象类似,当没有String对象引用常量池中的如“abc”这样的字符串时,且没有其他地方引用了这个字面量,如果有必要则“abc”常量会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也是类似。
  • 无用的类
    同时满足以下三个条件才能算是“无用的类”:
    • 该类所有实例都已经被回收(堆中不存在该类实例);
    • 加载该类的ClassLoader已经被回收;
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。在大量使用反射、动态代理 、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

三、垃圾收集算法

注:以下图片 ,黑色代表可回收对象内存,灰色代表存活对象内存,白色代表未使用内存。

1. 标记-清除算法

首先标记所有需要回收的对象,标记后统一回收所有被标记的对象。

回收前:
标记清除-回收前
回收后:
标记清除-回收后
算法缺点:

  • 效率问题,标记和清除两个过程效率都不高;
  • 空间问题,标记清除后产生大量不连续的内存碎片,碎片过多可能会导致程序运行过程中需要分配较大对象时,无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作。

2. 复制算法

将可用内存按等容量划分为大小相同的两块,每次只使用其中一块,当这块内存用完,将存活的对象复制到另一个块内存,然后再把已使用过的内存空间直接清理掉。这样就可以针对整个半区回收内存,内存分配时也不用考虑碎片问题,只要移动指针,按顺序分配即可,实现简单,运行高效。

回收前:
复制-回收前
回收后:
复制-回收后
算法缺点:可用内存缩小为一半,代价太高。

目前商业虚拟机都采用这种GC算法来回收新生代,IBM公司研究表明,新生代对象98%都是“朝生夕死”的,并不需要按1:1的比例划分。而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间。回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间,最后清理Eden和刚刚用过的Survivor空间即可。

HotSpot虚拟机默认Eden与Survivor大小比例是8:1,则其实每次浪费掉的只有10%的内存,避免了复制算法的问题。但是,由于没法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

3. 标记-整理算法

复制收集算法在对象存活率较高时就会进行较多的复制操作,效率变低。根据老年代特点,提出了“标记-整理算法”,标记过程与“标记-清除”算法一样,但后续不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。

清理前:
标记整理-回收前
回收后:
在这里插入图片描述

4. 分代收集算法

当前商业虚拟机GC都采用分代收集算法,其实也并不能说是算法,而是根据对象存活周期不同将内存划分为几块,一般是分为新生代和老年代,针对不同年代的特点采用不同GC算法:

  • 新生代中,每次垃圾收集时都发现有大批对象死去,少量存活,就选择复制算法,只要付出少量存活对象的复制成本就可以完成收集。
  • 老年代中,对象存活率高、没有额外空间进行分配担保,采用“标记-清除”或“标记-整理”算法进行回收。

四、HotSpot中对象存活判定与垃圾收集实现

HotSpot虚拟机是jdk默认的虚拟机,我们在pc上配置好环境变量,使用java -version 命令即可看到java版本信息以及虚拟机信息。
HotSpot虚拟机
在虚拟机实现上面所说的算法时,对算法的执行效率有严格的考量,以保证虚拟机高效运行。

首先,从可达性分析开始,可作为GC Roots节点的变量主要在全局性的常量或类静态属性,以及执行的上下文(栈帧中的局部变量表)中。现在很多应用仅方法区就有数百兆,如果要逐个检查引用,会消耗很多时间。
此外,可达性分析对时间的敏感还体现在GC停顿上。因为分析时要保证对象引用关系不会发生变化,因此在分析时要保证系统停顿下来,以保证分析结果的准确性。因此要停止所有Java线程(Stop The World)。

系统停顿下来,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,目前主流Java虚拟机都使用准确式GC,虚拟机有办法知道哪些地方存放着对象引用。在HotSpot中使用一组称为OopMap的数据结构来达到该目的。在OopMap协助下,HotSpot虚拟机可以快速准确地完成GC Roots枚举,但是OopMap内容变化的指令非常多,如果为每条指令都生成OopMap,则空间成本非常高。
而实际上,HotSpot并没有为每条指令都生成OopMap,而是在特定位置记录了这些信息,这些位置称为安全点(Safepoint),到达安全点再暂停去GC。安全点的选取,要保证既不能太少让GC等太久,也不能太多以致于增大运行负荷。
另一个需要考虑的问题是如何在GC发生时让所有线程都跑到最近的安全点上,有两种方案

  • 抢先式中断:不需要线程主动配合,在GC发生时,首先把所有线程全部中断,如果有线程中断的地方不在安全点上,就恢复线程,让其跑到安全点。(几乎没有虚拟机这么干)
  • 主动式中断:当GC需要中断线程时,不直接对线程操作,仅仅设置一个标志,各个线程去主动轮询这个标志,发现中断标志为真时,就中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

此外,如果程序没有执行,比如线程sleep时,线程无法及时跑到安全点,无法进行GC,因此就有了安全区域(Safe Region)。在安全区域中引用关系不会发生变化,在这个区域中任意地方开始GC都是安全的。当线程执行到Safe Region时,会标志自己进入安全区,如果这段时间里JVM发起GC,就不管标志为Safe Region状态的线程。当线程离开安全区时,会检查是否已经完成了GC,如果完成了,线程就继续执行,如果没完成,则等待到可以安全离开Safe Region的信号为止。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为什么要学JVM1、一切JAVA代码都运行在JVM之上,只有深入理解虚拟机才能写出更强大的代码,解决更深层次的问题。2、JVM是迈向高级工程师、架构师的必备技能,也是高薪、高职位的不二选择。3、同时,JVM又是各大软件公司笔试、面试的重中之重,据统计,头部的30家互利网公司,均将JVM作为笔试面试的内容之一。4、JVM内容庞大、并且复杂难学,通过视频学习是最快速的学习手段。课程介绍本课程包含11个大章节,总计102课时,无论是笔试、面试,还是日常工作,可以让您游刃有余。第1章 基础入门,从JVM是什么开始讲起,理解JDK、JRE、JVM的关系,java的编译流程和执行流程,让您轻松入门。第2章 字节码文件,深入剖析字节码文件的全部组成结构,以及javap和jbe可视化反解析工具的使用。第3章 类的加载、解释、编译,本章节带你深入理解类加载器的分类、范围、双亲委托策略,自己手写类加载器,理解字节码解释器、即时编译器、混合模式、热点代码检测、分层编译等核心知识。第4章 内存模型,本章节涵盖JVM内存模型的全部内容,程序计数器、虚拟机栈、本地方法栈、方法区、永久代、元空间等全部内容。第5章 对象模型,本章节带你深入理解对象的创建过程、内存分配的方法、让你不再稀里糊涂。第6章 GC基础,本章节是垃圾回收的入门章节,带你了解GC回收的标准是什么,什么是可达性分析、安全点、安全区,四种引用类型的使用和区别等等。第7章 GC算法与收集器,本章节是垃圾回收的重点,掌握各种垃圾回收算法,分代收集策略,7种垃圾回收器的原理和使用,垃圾回收器的组合及分代收集等。第8章 GC日志详解,各种垃圾回收器的日志都是不同的,怎么样读懂各种垃圾回收日志就是本章节的内容。第9章 性能监控与故障排除,本章节实战学习jcmd、jmx、jconsul、jvisualvm、JMC、jps、jstatd、jmap、jstack、jinfo、jprofile、jhat总计12种性能监控和故障排查工具的使用。第10章 阿里巴巴Arthas在线诊断工具,这是一个特别小惊喜,教您怎样使用当前最火热的arthas调优工具,在线诊断各种JVM问题。第11章 故障排除,本章会使用实际案例讲解单点故障、高并发和垃圾回收导致的CPU过高的问题,怎样排查和解决它们。课程资料课程附带配套项目源码2个159页高清PDF理论篇课件1份89页高清PDF实战篇课件1份Unsafe源码PDF课件1份class_stats字段说明PDF文件1份jcmd Thread.print解析说明文件1份JProfiler内存工具说明文件1份字节码可视化解析工具1份GC日志可视化工具1份命令行工具cmder 1份学习方法理论篇部分推荐每天学习2课时,可以在公交地铁上用手机进行学习。实战篇部分推荐对照视频,使用配套源码,一边练习一遍学习。课程内容较多,不要一次性学太多,而是要循序渐进,坚持学习。      

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值