JVM垃圾回收与内存分配

简介: JVM垃圾回收与内存分配

本文主要针对Java虚拟机再进行内存垃圾回收遇到的三个问题进行研究,分别是:哪些内存需要回收?Java虚拟机会在什么时候进行垃圾回收?如何回收?

一、哪些内存需要回收?

程序计数器、虚拟机栈、本地方法栈 3 个区域为线程隔离数据区,会随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

二、Java虚拟机如何垃圾回收?

1.Java堆内存的回收

Java堆中放的都是对象实例,我们要对Java堆中的内存回收主要是针对的是已经结束生命周期的对象,要想知道哪些对象是否“存活”,常见的判断对象是否存活的算法有引用计数算法和可达性分析算法。

引用计数算法

算法实现:

  • 给对象添加一个引用计数器,每当有一个地方引用他时,计数器的值加1;
  • 当引用失效时,计数器减1;
  • 任何时刻计数器为0的对象就是不可能在被使用的。

优点:

  • 实现简单,判定效率高

缺点:

  • 难以解决对象之间相互循环引用的问题

可达性分析算法

算法实现:

通过一系列的“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

网络异常,图片无法展示
|

在Java语言中可以做GC Roots的对象包括下面几种:

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

Java语言的引用

jdk1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用等4种引用,引用强度以次递减。

  • 强引用:类似于Object obj = new Object();由用户自己创建的对象,只要强引用在垃圾收集器就不回收被引用的对象。
  • 软引用:主要指的是内存中还有用但并非必需的对象,Java中使用SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,虚拟机就会抛出内存溢出异常。
  • 弱引用:主要指的是内存中非必需的对象,它的引用强度较软引用更低一点。Java中使用WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
  • 虚引用:Java中使用PhantomReference 类实现虚引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

\

2.方法区的回收

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。方法区(HotSpot虚拟机中的永生代)的垃圾收集主要包括两部分内容:废弃变量无用的类

判断废弃常量:一般是判断没有该常量的引用。

判断无用的类:要以下三个条件都满足

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

3.垃圾收集算法

1)标记 -清除算法(Mark-Sweep)

“标记-清除”算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。它的主要缺点有两个:

  • 效率问题:标记和清除过程的效率都不高;
  • 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World。

网络异常,图片无法展示
|

2)复制算法(Copying)

为解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。它的主要缺点有两个:

  • 效率问题:在对象存活率较高时,复制操作次数多,效率降低;
  • 空间问题:內存缩小了一半;需要额外空间做分配担保(老年代)

网络异常,图片无法展示
|

From Survivor, To Survivor使用的就是复制算法。老年代不使用这种算法,

3)标记-整理(Mark-Compact)

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

网络异常,图片无法展示
|

4) 分代收集算法(Generational Collection)

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

“分代收集”算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

4.垃圾收集器

前面的收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。

在JDK1.7Update14之后的Hotspot虚拟机正式提供了商用的G1收集器,在此版本中的虚拟机包含的所有收集器如下图:

网络异常,图片无法展示
|

上图展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们可以相互搭配使用。虚拟机所处在的区域,则表示他是属于新生代收集器还是老年收集器。

网络异常,图片无法展示
|

网络异常,图片无法展示
|

Serial 收集器

Serial 收集器是一个单线程收集器。它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束(STW),STW会极大的影响用户体验,对许多应用来说是不能接受的。但serial收集器仍然是虚拟机运行在client模式下默认的新生代的收集器,主要是因为serial收集器在单核cpu的环境下,没有线程交互的开销,专心做垃圾收集工作,可以得到单线程最高的收集效率。

网络异常,图片无法展示
|

ParNew 收集器

ParNew 收集器可以认为是 Serial 收集器的多线程版本。除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、对象分配策略、回收策略等等)和 Serial 收集器完全一样。在实现上,两款收集器共用相当多的代码。

ParNew 收集器是Server模式下的虚拟机中首选的新生代收集器,主要是因为只有他能与CMS收集器配合工作。

网络异常,图片无法展示
|

Parallel Scavenge 收集器

Parallel Scavenge 收集器这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。

CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。

作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。


Serial Old 收集器

Serial Old收集器是Serial收集器的老年代版本,它也是一款使用"标记-整理"算法的单线程的垃圾收集器。这款收集器主要用于客户端应用程序中作为老年代的垃圾收集器,也可以作为client模式下的垃圾收集器,当它用于服务端应用系统中的时候,主要是在JDK1.5版本之前和Parallel Scavenge年轻代收集器配合使用,或者作为CMS收集器的后备收集器。

网络异常,图片无法展示
|

Parallel Old 收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用"标记-整理"算法。这个收集器是在JDK1.6版本中出现的,所以在JDK1.6之前,新生代的Parallel Scavenge只能和Serial Old这款单线程的老年代收集器配合使用。Parallel Old垃圾收集器和Parallel Scavenge收集器一样,也是一款关注吞吐量的垃圾收集器,和Parallel Scavenge收集器一起配合,可以实现对Java堆内存的吞吐量优先的垃圾收集策略。

Parallel Old垃圾收集器的工作原理和Parallel Scavenge收集器类似。

网络异常,图片无法展示
|

CMS 收集器

CMS收集器是目前老年代收集器中比较优秀的垃圾收集器。CMS是Concurrent Mark Sweep,从名字可以看出,这是一款使用"标记-清除"算法的并发收集器。CMS垃圾收集器是一款以获取最短停顿时间为目标的收集器。由于现代互联网中的应用,比较重视服务的响应速度和系统的停顿时间,所以CMS收集器非常适合在这种场景下使用。CMS收集器的运行过程相对上面提到的几款收集器要复杂一些。  

网络异常,图片无法展示
|

从图中可以看出,CMS收集器的工作过程可以分为4个阶段:

  • 初始标记(CMS initial mark)阶段
  • 并发标记(CMS concurrent mark)阶段
  • 重新标记(CMS remark)阶段
  • 并发清除(CMS concurrent sweep)阶段

  从图中可以看出,在这4个阶段中,初始标记和重新标记这两个阶段都是只有GC线程在运行,用户线程会被停止,所以这两个阶段会发送STW(Stop The World)。初始标记阶段的工作是标记GC Roots可以直接关联到的对象,速度很快。并发标记阶段,会从GC Roots 出发,标记处所有可达的对象,这个过程可能会花费相对比较长的时间,但是由于在这个阶段,GC线程和用户线程是可以一起运行的,所以即使标记过程比较耗时,也不会影响到系统的运行。重新标记阶段,是对并发标记期间因用户程序运行而导致标记变动的那部分记录进行修正,重新标记阶段耗时一般比初始标记稍长,但是远小于并发标记阶段。最终,会进行并发清理阶段,和并发标记阶段类似,并发清理阶段不会停止系统的运行,所以即使相对耗时,也不会对系统运行产生大的影响。

  由于并发标记和并发清理阶段是和应用系统一起执行的,而初始标记和重新标记相对来说耗时很短,所以可以认为CMS收集器在运行过程中,是和应用程序是并发执行的。由于CMS收集器是一款并发收集和低停顿的垃圾收集器,所以CMS收集器也被称为并发低停顿收集器。

  虽然CMS收集器可以是实现低延迟并发收集,但是也存在一些不足。

首先,CMS收集器对CPU资源非常敏感。对于并发实现的收集器而言,虽然可以利用多核优势提高垃圾收集的效率,但是由于收集器在运行过程中会占用一部分的线程,这些线程会占用CPU资源,所以会影响到应用系统的运行,会导致系统总的吞吐量降低。CMS默认开始的回收线程数是(Ncpu + 3) / 4,其中Ncpu是机器的CPU数。所以,当机器的CPU数量为4个以上的时候,垃圾回收线程将占用不少于%25的CPU资源,并且随着CPU数量的增加,垃圾回收线程占用的CPU资源会减少。但是,当CPU资源少于4个的时候,垃圾回收线程占用的CPU资源的比例会增大,会影响到系统的运行,假设有2个CPU的情况下,垃圾回收线程将会占据超过50%的CPU资源。所以,在选用CMS收集器的时候,需要考虑,当前的应用系统,是否对CPU资源敏感。

其次,CMS收集器在处理垃圾收集的过程中,可能会产生浮动垃圾,由于它无法处理浮动垃圾,所以可能会出现Concurrent Mode Failure问题而导致触发一次Full GC。 所谓的浮动垃圾,是由于CMS收集器的并发清理阶段,清理线程是和用户线程一起运行,如果在清理过程中,用户线程产生了垃圾对象,由于过了标记阶段,所以这些垃圾对象就成为了浮动垃圾,CMS无法在当前垃圾收集过程中集中处理这些垃圾对象。由于这个原因,CMS收集器不能像其他收集器那样等到完全填满了老年代以后才进行垃圾收集,需要预留一部分空间来保证当出现浮动垃圾的时候可以有空间存放这些垃圾对象。在JDK 1.5中,默认当老年代使用了68%的时候会激活垃圾收集,这是一个保守的设置,如果在应用中老年代增长不是很快,可以通过参数"-XX:CMSInitiatingOccupancyFraction"控制触发的百分比,以便降低内存回收次数来提供性能。在JDK 1.6中,CMS收集器的激活阀值变成了92%。如果在CMS运行期间没有足够的内存来存放浮动垃圾,那么就会导致"Concurrent Mode Failure"失败,这个时候,虚拟机将启动后备预案,临时启动Serial Old收集器来对老年代重新进行垃圾收集,这样会导致垃圾收集的时间边长,特别是当老年代内存很大的时候。所以对参数"-XX:CMSInitiatingOccupancyFraction"的设置,过高,会导致发生Concurrent Mode Failure,过低,则浪费内存空间。

CMS的最后一个问题,就是它在进行垃圾收集时使用的"标记-清除"算法。上文介绍垃圾回收原理的时候,我们讲到"标记-清除"算法,在进行垃圾清理以后,会出现很多内存碎片,过多的内存碎片会影响大对象的分配,会导致即使老年代内存还有很多空闲,但是由于过多的内存碎片,不得不提前触发垃圾回收。为了解决这个问题,CMS收集器提供了一个"-XX:+UseCMSCompactAtFullCollection"参数,用于CMS收集器在必要的时候对内存碎片进行压缩整理。由于内存碎片整理过程不是并发的,所以会导致停顿时间变长。

"-XX:+UseCMSCompactAtFullCollection"参数默认是开启的。

虚拟机还提供了一个"-XX:CMSFullGCsBeforeCompaction"参数,来控制进行过多少次不压缩的Full GC以后,进行一次带压缩的Full GC,默认值是0,表示每次在进行Full GC前都进行碎片整理。

虽然CMS收集器存在上面提到的这些问题,但是毫无疑问,CMS当前仍然是非常优秀的垃圾收集器。

G1 收集器

1)G1垃圾收集器简介

简介: G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多核处理器及大容量内存的机器

特点: STW停顿时间敏感,提升用户体验,高吞吐量,CPU利用率高。

网络异常,图片无法展示
|

从上图看,G1将Java堆划分为多个大小相等的独立区域(Region),每一个小方格代表一个Region,JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数**-XX:G1HeapRegionSize**手动指定Region大小,但是推荐默认的计算方式

G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化

默认年轻代对堆内存的占比是5%,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%。

PS: 年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1

Humongous区

  G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50% ,比如每个Region是2M,只要一个对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。

作用: Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销

PS: Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。

2)G1垃圾收集器GC步骤

网络异常,图片无法展示
|

  • 初始标记【STW】: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。(同CMS)
  • 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象集合的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。(同CMS)
  • 重新标记/最终标记【STW】: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。(同CMS)
  • 筛选回收【STW】
  1. 定义: 筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划
  2. 举个 : 比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,通过回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region,尽量把GC导致的停顿时间控制在我们指定的范围内。
  3. 这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
  4. 回收算法:不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中。这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。
  5. CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本
  6. 筛选回收如何实现? G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。
  • 并发重置: 重置本次GC过程中的标记数据。

3)G1垃圾收集器的特点

  • 并行与并发: G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短STW停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  • 分代收集: 虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
  • 空间整合: 与CMS的标记-清理算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部上来看是基于标记-复制算法实现的。
  • 可预测的停顿: 这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型【后台维护的优先列表】 ,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数**-XX:MaxGCPauseMillis**指定)内完成垃圾收集。

  毫无疑问, 可以由用户指定期望的停顿时间是G1收集器很强大的一个功能, 设置不同的期望停顿时间, 可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。 默认的停顿目标为两百毫秒,一般不需要调整,即使调整也应尽可能使其合理,不能太短,如果我们把停顿时间调得非常低,譬如设置为二十毫秒, 很可能每次只能回收很小的一部分内存, 收集器收集的速度逐渐跟不上分配器分配的速度, 导致垃圾慢慢堆积。 很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间, 但应用运行时间一长就不行了,最终占满堆引发Full GC反而降低性能, 所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。

4)G1垃圾收集分类

  • YoungGC: YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC。G1垃圾收集器刚开始年轻代只占堆内存百分之5,会随着每次计算回收时间而增加,最多不超过百分之60。
  • MixedGC【混合收集】: 不是FullGC,老年代的堆占有率达到参数**-XX:InitiatingHeapOccupancyPercent**设定的值则触发,回收所有的年轻代和部分老年代(根据筛选回收阶段计算优先级后排序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。
  • Full GC: 停止系统程序,然后采用单线程进行标记、清理、压缩整理以空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)

5)什么场景适合使用G1

  • 50%以上的堆被存活对象占用: 使用G1,就不用特意预留出很大的老年代空间,G1会根据对象存活状态,动态分配每种不同代对象需要占用的空间。
  • 对象分配和晋升的速度变化非常大: 前提还是大内存机器才使用G1,大内存的主机如果对象分配和晋升的速度变化非常快的话,G1的这种内存设计可以很快的划分出对应所需的区域【区域占比动态增长,不像CMS等垃圾收集器要划分固定的空间来区分年轻代和老年代】,但因为G1算法比较复杂,在小内存机器里面性能不如CMS等主流垃圾收集器。
  • 垃圾回收时间特别长,超过1秒: G1有一大好处就是可以设置我们每次想要回收的停顿时间【-XX:MaxGCPauseMillis】,可以有效提升用户体验。
  • 8GB以上的堆内存(建议值): G1适合8G以上内存的机器使用【结构设计,2048个Region,内存太小的话每个Region也很小,很容易就超过Region的一半被识别为超大对象,这样Humongous区东西会很多,反而不能很好的进行GC收集】
  • 停顿时间是500ms以内: 和第三点类似,G1分段收集【每次不一定全部回收完所有的垃圾对象】,并且可以手动设置我们每次GC想要STW的时间。

5.HotSpot的算法实现

1)枚举根节点

从可达性分析中从GC Roots节点找引用为例,可作为GC Roots的节点主要是全局性的引用与执行上下文中,如果要逐个检查引用,必然消耗时间。

另外可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里的“一致性”的意思是指整个分析期间整个系统执行系统看起来就行被冻结在某个时间点,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性就无法得到保证。这点是导致GC进行时必须暂停所有Java执行线程的其中一个重要原因。

由于目前主流的Java虚拟机都是准确式GC,做一档执行系统停顿下来之后,并不需要一个不漏的检查执行上下文和全局的引用位置,虚拟机应当有办法得知哪些地方存放的是对象的引用。在HotSpot的实现中,是使用一组OopMap的数据结构来达到这个目的的。

2)安全点

在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots的枚举,但可能导致引用关系变化的指令非常多,如果为每一条指令都生成OopMap,那将会需要大量的额外空间,这样GC的空间成本会变的很高。

实际上,HotSpot也的确没有为每条指令生成OopMap,只是在特定的位置记录了这些信息,这些位置被称为安全点(SafePoint)。SafePoint的选定既不能太少,以致让GC等待时间太久,也不能设置的太频繁以至于增大运行时负荷。所以安全点的设置是以让程序“是否具有让程序长时间执行的特征”为标准选定的。“长时间执行”最明显的特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。

对于SafePoint,另一个问题是如何在GC发生时让所有线程都跑到安全点在停顿下来。这里有两种方案:抢先式中断和主动式中断。抢先式中断不需要线程代码主动配合,当GC发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程来响应GC。

而主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的另外再加上创建对象需要分配的内存的地方。

3)安全区域

使用安全点似乎已经完美解决了如何进入GC的问题,但实际情况却并不一定,安全点机制保证了程序执行时,在不太长的时间内就会进入到可进入的GC的安全点。但是程序如果不执行呢?所谓的程序不执行就是没有分配cpu时间,典型的例子就是线程处于sleep状态或者blocked状态,这时候线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm显然不太可能等待线程重新分配cpu时间,对于这种情况,我们使用安全区域来解决。

安全区域是指在一段代码片段之中,你用关系不会发生变化。在这个区域的任何地方开始GC都是安全的,我们可以把安全区域看做是扩展了的安全点。

当线程执行到安全区域中的代码时,首先标识自己已经进入了安全区,那样当在这段时间里,JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。当线程要离开安全区域时,他要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则他就必须等待,直到收到可以安全离开安全区域的信号为止。

\

6.GC日志的理解与分析

7.垃圾收集器参数总结

\

\

三、Java虚拟机在什么时候进行垃圾回收?

相关文章
|
3天前
|
存储 算法 Java
【JavaEE初阶】 关于JVM垃圾回收
【JavaEE初阶】 关于JVM垃圾回收
|
7天前
|
存储 算法 Java
JVM性能调优:内存模型及垃圾收集算法
JVM性能调优:内存模型及垃圾收集算法
15 0
|
7天前
|
存储 算法 Java
深入理解Java虚拟机(JVM)的垃圾回收机制
【5月更文挑战第30天】 在Java开发领域,垃圾回收(Garbage Collection, GC)是确保应用程序性能和内存效率的关键因素。本文将深入探讨Java虚拟机(JVM)的垃圾回收机制,解析其工作原理、不同算法的特点以及如何通过调优来提高应用性能。我们将透过JVM的内存结构,探索垃圾回收过程中涉及的关键技术点,并讨论现代Java应用中常见的垃圾回收器实现。
|
8天前
|
Java Linux
JVM堆内存诊断
JVM堆内存诊断
15 0
|
8天前
|
存储 安全 Java
浅谈JVM内存结构
浅谈JVM内存结构
13 0
|
9天前
|
存储 算法 Oracle
深入理解 JVM(重点:双亲委派模型 + 垃圾回收算法)
深入理解 JVM(重点:双亲委派模型 + 垃圾回收算法)
|
10天前
|
存储 Java 开发者
深入理解Java虚拟机:JVM内存模型解析
【5月更文挑战第27天】 在Java程序的运行过程中,JVM(Java Virtual Machine)扮演着至关重要的角色。作为Java语言的核心执行环境,JVM不仅负责代码的执行,还管理着程序运行时的内存分配与回收。本文将深入探讨JVM的内存模型,包括其结构、各部分的作用以及它们之间的相互关系。通过对JVM内存模型的剖析,我们能够更好地理解Java程序的性能特征,并针对性地进行调优,从而提升应用的执行效率和稳定性。
|
10天前
|
Java
<Java SE> 5道递归计算,创建数组,数组遍历,JVM内存分配...
<Java SE> 5道递归计算,创建数组,数组遍历,JVM内存分配
36 2
|
13天前
|
存储 算法 Java
JVM(垃圾回收机制 --- GC)
JVM(垃圾回收机制 --- GC)
33 5
|
13天前
|
存储 Java 对象存储
JVM(内存区域划分)
JVM(内存区域划分)
29 1