JVM(二)—GC垃圾回收

简介:

学习Java或者从事Java开发的程序员应该都知道,在Java语言背后有着一套成熟的体系,这个体系支撑着Java项目的生存周期,并且在Java项目生存期间,GC机制为整个项目的运行提供了不可忽略的作用,可以说GC是Java语言的核心

浅谈JVM中运行时的内存区域分布

首先,程序计数器和Java虚拟机栈、本地方法栈3个区域都是线程私有的部分,也就是说,这三个部分上面分配的运行时内存都会伴随着线程执行结束而自动被释放,尤其是在逃逸分析之后的栈上分配更是为了减少GC消耗的性能采用的一种方案

栈上分配这个概念之前在总结中也有提到,这种技术的实现,也就可以将一些方法内创建的实例对象需要的内存大小直接开辟在虚拟机栈上,并且随着方法的执行和结束,伴随着栈帧的入栈和出栈,这一块申请在栈上的内存空间也将直接被回收,不需要GC的参与

堆和方法区

说到方法区,需要清楚的一个概念是,方法区是JVM规范中的运行时数据区的一个部分,其余的组成可以参考之前的博客,至于永久带以及元空间这两种说法,是不同的JVM或不同版本的JVM对方法区的一种具体实现,永久带和元空间都是HotSpot JVM的实现方式,1.7和之前的HotSpot版本的JVM中方法区的实现为永久带,HotSpot在1.8和之后移除了永久带 ,并且引入一个新的实现元空间(metaspace)

我们所听说到的一些GC的实现方式以及一些GC的必要算法其实更多的是面向堆上的失去引用的实例,其实正是因为堆上的对象只有在程序运行期间才会知道创建了什么对象,以及程序运行时可能会出现一些难以分析的场景,也正是因为这种运行时的复杂性,我们才为在堆上执行GC时提供了这么多的GC策略

对象是否还活着

我们判断一个对象该被回收的特点是:不可能再被任何途径使用的对象

引用计数

引用计数是早期出现的一种判断对象是否还存活的方式,判定效率很高,并且在一些场景中也是用到了这种方式,通过为每一个对象设置一个引用计数器,如果存在一条引用则计数器自增,若失去一条引用的计数器自减

问题

  • 如果在GC过程中存在两个对象相互引用并且这两个对象没有被其余对象所引用,也就是说这两个对象实际上是应该被GC的对象,但是因为两者的互相引用没有使计数器的值减少为0,也正是因为这个原因,没有办法通知GC收集器去收集他们

引用计数这个方式在书上也有实例,但是说实话我并没从书上的例子中看出JVM没有使用引用计数这种方式去判断一个对象是否是存活的,但是实际情况是,JVM的GC机制确实没有使用这种方式去实现如何判断一个对象是否该被GC

可达性分析算法

可达性分析算法在GC的过程中是判断对象是否存活的一种主要算法,这种算法为我们提供了一个新的概念GC ROOTS

GC ROOTS体系

GC ROOTS

通过这个名字我们可以直接对它进行一个翻译,GC的根节点集,从图中的意思我们可以看出,GC ROOTS作为GC的根节点向下不断延伸,形成一颗类似于树的结构,这些路径我们不妨称为

可作为GC ROOTS的元素

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

上面的四种类型在之前介绍JVM内存区域的时候已经为大家解读过,其实仔细分析能够作为GC ROOTS的元素,能够发现,其实在编写代码的时候大家就能发现

  • 栈帧中的本地变量表中引用的对象其实就是声明在方法中的引用对象
  • 方法区中的类的静态属性其实就是我们常说的static修饰的"属于类"的引用对象
  • 常量区中引用的对象就比如在类成员变量中声明为final的饮用对象
  • 本地方法栈的引用对象就有些类似Java方法中的引用对象了

什么是可达性分析算法

JVM从GC ROOTS结点向下搜索,经历过的路径称为 "引用链",当一个对象到GC Roots没有任何引用链相连(如图所示obj 8 -10即为不可达的),这就说明该对象是不可用的,接下来再通过JVM的一些机制将对它进行回收或拯救

增强引用

JDK1.2之前,Java中的引用很传统 如果reference类型的数据中存储的数值代表另一块内存的起始地址,就称这块内存代表一个引用

JDK在1.2之后,丰富了引用的类型,对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用,扩充引用类型的目的其实很明确,在GC时,按照引用类型的不同,在回收时采用不同的逻辑。 这几种类型的引用主要在jvm内存缓存、资源释放、对象可达性事件处理等场景会用到

  • 强引用

    
    这种类型的引用在代码中最广泛存在,那就是使用new关键字实例化的引用,只要这种强引用只要还存在,GC就不会发生在被强引用所引用的对象身上
    
  • 软引用

    
    这种类型的引用用来描述一些有用但是并非必须的对象,在JVM即将发生OOM的时候,将会把被软引用所引用的对象列进回收范围内进行二次回收
    
  • 弱引用

    弱引用所引用的对象,只能生存到下一次GC前,当下一次GC开始的时候,不管内存空间是否足够,都会对这种类型的引用引用的对象实行GC
    
  • 虚引用

    为一个对象设置徐引用关联的唯一目的是让这个对象在被回收器回收的时候收到一个系统通知
    

GC的标记过程

一个对象并不是在可达性分析中若判定为不在引用链上就会被标记为即将被GC的对象,一个对象被GC的过程至少要经过2次标记,下面将为大家介绍如何进行两次标记,以及对象如何进行自我拯救脱离将被GC的命运

  • 可达性分析

    上文介绍了可达性算法的具体过程,那么在一个对象呗可达性分析出不在GC链上的时候,将会被第一次标记,并且进行一次筛选,**筛选的条件是是否重有必要执行finalize方法**,这一次筛选也是作为第二次标记的条件
    
  • finalize方法

    当一个对象没有重写finalize方法,或者finalize方法被执行过一次,那么这一次筛选将会视这两种情况为没有必要执行finalize方法
    
    如果这次筛选其中一些对象被判定为了有必要执行finalize方法,那么这些对象将会被放置在一个F-Queue队列中,并且稍后会有一个由JVM创建的,低优先级的Finalizer线程去执行它,当然,**这里说的"执行"并不一定意味着进入F-Queue队列中等待执行finalize方法的对象一定会执行finalize方法**
    
    如果一个对象在执行finalize方法的时候进入一个死循环,或者说finalize方法执行时间过长,就可能导致队列中其余的等待执行finalize方法的对象不能够继续执行,如果发生这种情况,可能会导致F-Queue队列中的对象发生永久等待,更有可能导致GC崩溃
    
    如果对象尝试通过finalize方法拯救自己,**只需要将自己赋值给某个类变量或者对象的成员变量即可(回到引用链上)**,**这样的话这个对象将会在第二次标记的过程中被移除即将被回收的集合,如果这个对象没有完成自我拯救的话,那就真的被GC了**
    
    

对方法区的回收

永久代的垃圾回收主要存在两个部分:无用的常量和类

对一个字符串"abc"来说,如果程序中没有一个String对象是"abc",也就是说没有任何String对象引用常量池中的"abc"常量,也没有其余地方使用了这个字面量,如果这个时候发生GC,那么这个"abc"常量将会被进行清理

如何判断一个类无用

  • 该类的所有实例都已经被回收
  • 加载该类的ClassLoader已经被回收
  • 该类的Class对象没有在任何地方被引用,无法在其余地方通过反射访问该类的方法

当然,这里说的满足上述三条时,一个类将会被回收,并不是和对象一样不使用了就必然会被回收,可以通过一些JVM参数进行指定

《深入理解JVM》这本书里提到,在大量使用反射,动态代理、CGLib等框架动态生成自定义ClassLoader的场景都需要具备卸载类的功能,以保证永久代不会溢出,想想之前一个SpringBoot项目的部署直接使一个远程服务器宕机的事故,终于能有所体会...

垃圾回收算法

接下来谈到的垃圾回收算法,网上一搜一大堆,电子书中也是一翻就有,所以这里不再介入截图之类的过多说辞,仅仅放上我认为的重点

标记-清除算法

在介绍这个算法之前,希望大家还能够记起之前提到过的指针碰撞和空闲页表这两种分配内存空间的方式,并且思考一下区别

标记清除算法较为简单,首先标记出所有需要回收的对象,接着回收所有被标记的对象

可以看到标记清除算法只是简单地将该回收的对象进行标记,并且下一步并没有对对象的内存区域布局进行改变,这样可以想象的来,整个堆上如果使用这种算法实现GC的话,整个堆上的空间将会是断断续续,需要使用空闲列表来维护使用的和未使用的空间

并且,标记清除算法可能造成较大的内存碎片,若分配较大的内存空间时,可能会因为空间不足提前触发GC

复制算法

为了解决标记清除算法产生的诸多问题,复制算法诞生了

它将可用内存等容量得划分为两个区域,每次只使用其中的一块,当这一块内存用完了之后,会将依旧存活的对象复制到另一块上面,然后再把已经使用过的内存空间一次性清理掉

通过这种算法,可以看出GC的区域只是整个可用空间的一半,并且通过复制算法实现的GC机制,不会出现内存碎片,空闲出来的永远是一个空的半区,只需要移动指针,按照指针碰撞的方式进行分配即可

这只是最原始的复制算法,不过这种算法存在一种最明显的缺点,那就是每次分配空间只能使用可用空间的一半,这种设计方式的代价是不是高了点?

复制算法的改良版

在JVM的世界里,存在这样一种比例 新生代的对象:老年代对象 = 8:1,所以,复制算法不需要使用1:1的内存格局去实现,在后来的JVM中,复制算法有3个比较重要的区域Eden和2个Survivor区,Eden和Survivor的比例为8:1,每次使用的是Eden和其中一块Survivor区域,这样的话新生代可使用的内存空间就占到了9/10,只有1/10的空间会被浪费掉

使用这种方式进行回收的时候,将Eden和其中一块作为新生代空间的Survivor区域中依旧存活的对象复制到另一块Survivor中,并且完全清除Eden和Survivor中的内存区域,其实不难发现,与最开始的复制算法根本没有差别,只是引入了1块Eden和2块Survivor区域来降低1:1的最初的复制算法带来的代价问题

当然,我们没有办法保证每次存活的对象只有小于或等于10%,这个时候,如果Survivor区域的内存空间不足,将会向其余内存(老年代)进行分配担保,内存的分配担保是说,如果另一块Survivor空间不足以存放接下来存活的对象,将会通过这种担保方式直接进入老年代

在我看来,老年代是不存在复制算法的,因为老年代没有别的内存空间给它做担保,也就是说,老年代的无用对象回收需要借助于其余的算法,并且这个观点在书中也被证实

标记-整理算法

复制算法,在对象存活情况较高的情况下它的效率也就下降了,并且需要额外的空间进行担保,所以老年代一般不采用这种算法去实现

标记整理算法是说,标记过程仍然保持与标记清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存,显而易见,这种算法实现的GC在之后的内存分配的方式上,还是选择了指针碰撞,并且不存在大量的内存碎片

HotSpot算法实现

并且通过GC Roots遍历根节点的这个步骤的过程中,如果这些引用链非常庞大,如果需要一个个检查这里面的引用,必然又是一项耗时操作

现在我们知道了,标记这一个步骤主要依赖的是可达性算法的分析,那么我们可以想象得到,运行的程序中的每一个时刻中的引用链说不定都是在变化的,那么我们就不能时时刻刻都可以进行可达性分析

可达性分析对时间的敏感同时体现在GC停顿上,结合我上面说到的,如果程序正在运行,那么整个GC链中的引用可能是时刻都在变化的,这对于我们分析可达性而言,无疑是一种问题,这个时候,可达性分析就需要一种称作为 "一致性" 的特点,这种特点也导致了GC进行时,必须停止所有Java的执行线程(Stop The World)这使得整个分析期间,程序就像被冻结在某个时间点上,不可以出现对象间的引用持续变化,该点不满足的情况下,分析的结果就不能被保证

接下来我想说的是我个人的理解,这里看书的时候很迷糊,同时也希望如果自己的观点有什么问题,可以指出

书上提到,目前主流的JVM采用的都是准确式GC,所以当系统停顿下来之后,并不需要一个不漏地去检查完所有执行上下文和全局引用的位置,虚拟机应该是有办法直接得知哪些地方存放着对象的引用,并且也介绍了,这种实现是因为一个叫OopMap的数据结构计算出来的

在类加载的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用

浅谈OopMap

书上举了一个String.hashCode()方法编译后的本地代码

0x026eb7a9:call     0x026e83e0      ;   OopMap{ebx=Oop [16]=Oop off=[142]}
…………
…………
0x026eb7be:hlt

这段生成的本地代码指明了EBX寄存器和栈中偏移量为16的内存区域各有一个普通对象指针的引用,有效范围从call之灵开始到+142,即hlt指令为止

因为我们需要从GC停顿中去进行可达性分析,又因为可达性分析不能真正地通过各个遍历可作为GC Roots的结点去完成,这样效率过低,又是因为JVM采用准确式GC,并且配合OopMap这种数据结构,能够知道在GC停顿的时候哪个位置会存在引用

首先这是String.hashCode()编译得到的本地代码,我的理解是这样的,可达性分析判断的能作为GC Roots的类型上面也有提及,这个编译得到的本地代码,其实就是在GC停顿时,有执行hashCode方法的常量,或者字符串引用,就保存在EBX寄存器和栈中偏移量为16的内存区域中,通过GC停顿这个操作,可以再这一刻冻结的时间内,找到这个引用并且作为根节点进行可达性分析

安全点

虽然有了OopMap的帮助,但是还是有一个问题的存在,那就是时刻都有可能导致引用关系变化,或者说OopMap内容变化指令非常多,不可能为每一条指令都生成OopMap

HotSpot只是在特定的位置记录了这些信息,而这些信息就称为安全点,程序执行时,并不是在所有地方都可以停下来开始GC,只有在安全点才可以暂停,这种安全点的选择,既不能选取地太多让程序过长时间陷入等待,也不能设置得太少以至于GC等待的时间过长

"长时间"运行的程序,才是作为安全点的选取标准的,长时间执行的明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,具有这些功能的指令才会产生安全点

如何让所有线程都能在GC暂停的时候,运行到安全点

  • 抢先式中断

    
    抢先式中断不需要每条线程去配合,在GC发生的时候,JVM会把所有线程全部中断,如果发现有线程不在安全点的话,将恢复该线程,让其运行至安全点
    
  • 主动式中断

    采用这种方式的时候,不需要直接对线程操作,只需要设置一个标志,每个线程执行的时候,主动去轮询这个标志,如果发现中断标志为true的时候,就将自己中断挂起,**轮询标志的地方是与安全点重合的,再加上创建对象需要分配内存的地方**
    
    ### 安全区域
    为什么已经有了安全点,还需要引入安全区域这个概念,安全点的机制能够使得在不长的时间内就可以遇到可进入GC的安全点,但是如果程序不执行的时候呢
    
    **程序不执行就是指没有分配CPU时间,典型的例子就是线程处于blocked状态,这个时候线程无法响应JVM中的中断请求,JVM同样也不会等待线程重新被分配CPU时间,这个时候就需要安全区域的加入了**
    
    在线程执行到安全区域的时候,会标识自己进入了安全区,这样的话如果GC到来,就不用管已经标记自己进入安全区域的线程了,在线程要离开安全区域的时候,JVM会判断是否已经完成了根节点的枚举,或者是整个GC的过程,如果完成了,该线程继续运行,如果没有完成的话,那就必须进行等待
    
    

    以上是JVM GC 的一些较为重点的内容,其中不少有我的理解,如有误请指出

相关文章
|
24天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
49 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
22天前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
6天前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
24天前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
38 3
|
25天前
|
算法 Java
JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
本文详细介绍了JVM中的GC算法,包括年轻代的复制算法和老年代的标记-整理算法。复制算法适用于年轻代,因其高效且能避免内存碎片;标记-整理算法则用于老年代,虽然效率较低,但能有效解决内存碎片问题。文章还解释了这两种算法的具体过程及其优缺点,并简要提及了其他GC算法。
 JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
|
26天前
|
算法 Java
谈谈HotSpot JVM 中的不同垃圾回收器
【10月更文挑战第5天】理解 HotSpot JVM 中的不同垃圾回收器(如 CMS、G1 和 ZGC)的区别,需要深入了解它们的设计原理、工作方式和应用场景。以下是对这三个垃圾回收器的简要概述以及一个示例 Java 程序,虽然示例程序本身不能直接展示垃圾回收器的内部机制,但可以帮助观察不同垃圾回收器的行为。
15 1
|
2月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
104 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
2月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
1月前
|
存储 Java PHP
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
50 0
|
2月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制(GC)
本文将探讨Java的自动内存管理核心——垃圾回收机制。通过详细解析标记-清除算法、复制算法和标记-整理算法等常用垃圾回收算法,以及CMS、G1等常见垃圾回收器,帮助读者更好地理解Java应用的性能优化和内存管理。同时,探讨分代收集、分区收集等策略在实际项目中的应用。结语部分总结了垃圾回收机制在Java开发中的重要性,并展望了未来可能的发展。
49 0