浅析JAVA虚拟机JVM

1 篇文章 0 订阅
1 篇文章 0 订阅

文章目录

概要

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行,Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。可在JVM虚拟机上运行的不仅仅只有java,包括Java, Kotlin, Groovy, Scala, Jruby, Jython, Fantom, Clojure, Rhino, Ceylon等,所有可以把JVM理解成为在操作系统上运行的vmware虚拟机

一、JVM虚拟机内存划分

根据虚拟机规范,JVM的内存分为 堆、虚拟机栈、本地方法栈、程序计数器、方法区5部分

线程独占:栈,本地方法栈,程序计数器
每个线程都会有它独立的空间,随线程生命周期而创建和销毁
线程共享:堆,方法区
所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁

1. 堆

       1.堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError
       2. 堆的特点:
           1. 线程共享,整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java 虚拟机栈、本地方法栈都是一个线程对应一个
           2. 在虚拟机启动时创建
          3. 垃圾回收的主要场所
          4. 可分为:新生代(Eden区 From Survior To Survivor)、老年代、元空间/永久代
在这里插入图片描述

2. 虚拟机栈

      每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误
       2. 虚拟机栈的特点
           1. 局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变
           2. Java 虚拟机栈也是线程私有,随着线程创建而创建,随着线程的结束而销毁
           3. Java 虚拟机栈会出现两种异常:StackOverFlowError (若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常)和 OutOfMemoryError(若允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OutOfMemoryError 异常)
每调用一个方法都会生成一个新的栈帧,调用方法的过程就是一个压栈和出栈的过程,遵循先进后出的原则,如下图示例:

public class Stack {
    public static void main(String[] args) {
        System.out.println("准备入栈A");
        A();
    }

    public static  void A(){
        System.out.println("准备入栈B");
        B();
    }
    public static  void B(){
        System.out.println("执行B");
    }
}

在这里插入图片描述

3.本地方法栈

      本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。它与 Java 虚拟机栈实现的功能类似,只不过本地方法栈是描述本地方法运行过程的内存模型。本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。

4.程序计数器

      程序计数器,也叫PC 寄存器。JVM支持多个线程同时运行,每个线程都有自己的程序计数器。倘若当前执行的是 JVM 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native 方法,则PC寄存器中为空

5.方法区

      1.Java 虚拟机规范中定义方法区是堆的一个逻辑部分。
      2. 方法区也是所有线程共享。主要用于存储已经被虚拟机加载的类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫 非堆 。需要注意的是,方法区只是规范上面的一个逻辑概念,并不是真实的物理存储的命名
在这里插入图片描述

6.直接内存(堆外内存)(不属于JVM划分内存)

      1. 直接内存是除 Java 虚拟机之外的内存,但也可能被 Java 使用,直接内存的大小不受 Java 虚拟机控制,但既然是内存,当内存不足时就会抛出 OutOfMemoryError 异常
      2. 直接内存与堆内存比较:
           1. 直接内存申请空间耗费更高的性能
           2. 直接内存读取 IO 的性能要优于普通的堆内存

二、JVM判断对象是否存活的算法

如何判断哪些对象或者内存为垃圾,有两种比较经典的判断策略

  • 引用计数算法
  • 可达性分析算法

1.引用计数算法

      比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为 0 的对象。此算法最致命的是无法处理循环引用的问题。
在这里插入图片描述

2.根可达性清理算法

      根搜索算法的中心思想,就是从某一些指定的根对象(GC Roots)出发,一步步遍历找到和这个根对象具有引用关系的对象,然后再从这些对象开始继续寻找,从而形成一个个的引用链(其实就和图论的思想一致),然后不在这些引用链上面的对象便被标识为引用不可达对象,也就是我们说的“垃圾”,这些对象便需要回收掉。这种算法很好地解决了上面 引用计数算法 的循环引用的问题了。

  • 引用类型
  • 对象引用类型分为强引用、软引用、弱引用和虚引用。
  • 强引用:就是我们一般声明对象是时虚拟机生成的引用,强引用环境下,垃圾回收时需要严
    格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收
  • 软引用:软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机
    会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟
    机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟
    机在发生 OutOfMemory 时,肯定是没有软引用存在的。
  • 弱引用:弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾
    回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。

      强引用不用说,我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。
他们一般被作为缓存使用,而且一般是在内存大小比较受限的情况下做为缓存。因为如果内
存足够大的话,可以直接使用强引用作为缓存即可,同时可控性更高。因而,他们常见的是
被使用在桌面应用系统的缓存。

  • 查找根部节点
    查找根部节点
  • 根据跟查找他们引用的对象
    在这里插入图片描述
  • 剩下的没有标记的对象即视为垃圾
    在这里插入图片描述

三、常用的垃圾回收算法

GC针对的内存区域是堆和方法区

1.标记 - 清除算法(Mark-Sweep,一般适用于老年代)

      此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。

  • 清理前
    在这里插入图片描述
  • 清理后
    在这里插入图片描述

2.复制(Copying,一般适用于新生代)

      此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。

  • 清理前
    在这里插入图片描述
  • 清理后
    在这里插入图片描述

3.标记-整理算法(Mark-Compact,一般适用于老年代)

      此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除标记对象,并未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

  • 清理前
    在这里插入图片描述
  • 整理存活对象
    在这里插入图片描述
  • 清理对象
    在这里插入图片描述

四、三色标记

1.三色标记

      CMS和G1在标记的时候都是并发线程去标记,采用的就是三色标记法,关于三色标记法,首先要知道 在JVM中如何找到碎片采用的是根可达算法 root searching方法。找到以后进行mark sweep 方法进行标记。然后再root searching 一遍进行回收。所以mark sweep的特点是 地址不连续,再标记的过程中也会有新的对象被放到老年区,这时就会出现碎片,因为要扫两次所以效率略低。
三色标记法就是根据mark sweep来实现的,他是遍历过程按照是否扫描访问检查等等说法是否执行过没把对象分为了三种:

  • 白色:尚未访问过。
  • 本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
  • 本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。
    如下图所示:
  1. 开始的时候,会任务所有的对象都是白色的;
    在这里插入图片描述
  2. 用根可达算法将根下的引用对象都标记为灰色
    在这里插入图片描述
  3. 移动到 灰色对象中,将本对象 标记为黑色;
    在这里插入图片描述
  4. 重复第3第4步,直至扫完所有灰色对象
    在这里插入图片描述

2.为什么会漏标

      在并发标记阶段的时候,因为用户线程与 GC 线程同时运行,有可能会产生多标或者漏标。如下图所示:
1.开始扫描
在这里插入图片描述

2.如果此刻E到C的引用消失,那么继续扫描,C会被标记为黑色,并不会被回收,C这种垃圾被称为浮动垃圾
在这里插入图片描述
3、C本来是应该继续扫描,但是E此时断开与C的链接并开始与G进行依赖,但是此刻E已经被标记为黑色,已经结束扫描了,所以G会被标记为垃圾,这种错误在JVM中会导致系统报错
在这里插入图片描述
4、等初始的 GC Roots 遍历完(并发标记),该集合的对象遍历即可(重新标记)。
在这里插入图片描述

由上图可知,若要误标?当下面两个条件同时满足,会产生误标:

  • 赋值器插入了一条或者多条黑色对象到白色对象的引用
  • 赋值器删除了全部从灰色对象到白色对象的直接引用或者间接引用

所以只要破坏其中之一就好,CMS采用增量更新(Incremental Update)的是与G1采用SATB(Snapshot At The Beginning),下文会具体介绍:

2.1写屏障(Store Barrier,采用该种方式的回收器为:CMS、G1)

      给某个对象的成员变量赋值时,其底层代码大概长这样:

/**
* @param field 某对象的成员变量,如 D.fieldG
* @param new_value 新值,如 null
*/
void oop_field_store(oop* field, oop new_value) { 
    *field = new_value; // 赋值操作
} 

      所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念):

void oop_field_store(oop* field, oop new_value) {  
    pre_write_barrier(field); // 写屏障-写前操作
    *field = new_value; 
    post_write_barrier(field, value);  // 写屏障-写后操作
}

2.2 读屏障(Load Barrier,采用该种方式的回收器为:ZGC)

oop oop_field_load(oop* field) {
    pre_load_barrier(field); // 读屏障-读取前操作
    return *field;
}

读屏障是直接针对当读取成员变量时,一律记录下来:

void pre_load_barrier(oop* field, oop old_value) {  
  if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
      oop old_value = *field;
      remark_set.add(old_value); // 记录读取到的对象
  }
}

      这种做法是保守的,但也是安全的。因为条件二中【黑色对象 重新引用了 该白色对象】,重新引用的前提是:得获取到该白色对象,此时已经读屏障就发挥作用了。

五、 根(GC Roots)

1、什么是根(GC Roots)

根部定义(GC Roots):

  • 垃圾回收时,JVM首先要找到所有的GC Roots,这个过程称作 「枚举根节点」 ,这个过程是需要暂停用户线程的,即触发STW。然后再从GC Roots这些根节点向下搜寻,可达的对象就保留,不可达的对象就回收。
  • GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象(如方法区中类静态属性引用的对象 )。只有找到这种对象,后面的搜寻过程才有意义,不能被回收的对象所依赖的其他对象肯定也不能回收嘛。当JVM触发GC时,首先会让所有的用户线程到达安全点SafePoint时阻塞,也就是STW,然后枚举根节点,即找到所有的GC Roots,然后就可以从这些GC Roots向下搜寻,可达的对象就保留,不可达的对象就回收。即使是号称几乎不停顿的CMS、G1等收集器,在枚举根节点时,也是要暂停用户线程的。GC Roots是一种特殊的对象,是Java程序在运行过程中所必须的对象,而且是根对象。

可以作为GC Roots的对象:
在这里插入图片描述
1、方法区静态属性引用的对象
全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。

2、方法区常量池引用的对象
也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。

3、方法栈中栈帧本地变量表引用的对象
属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。

4、JNI本地方法栈中引用的对象
和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。

5、被同步锁持有的对象
被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁不就失效了嘛。

六.垃圾收集器

1. 什么是Stop-The-World?
Stop-The-World:简称 STW,是在垃圾回收算法执行过程中,将jvm内存冻结,停顿的一种状态,在STW状态下,所有的线程都是停止运行的 - >垃圾回收线程除外,当STW发生时,出了GC所需要的线程,其他的线程都将停止工作,中断了的线程知道GC线程结束才会继续任务,STW是不可避免的,垃圾回收算法的执行一定会出现STW,而我们最好的解决办法就是减少停顿的时间,GC各种算法的优化重点就是为了减少STW,这也是JVM调优的重点。

1.Serial 和Serial Old 收集器(可以应对JVM内存在百兆内)

      Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。Serial收集器没有线程交互的开销,可以获得很高的单线程收集效率。JDK1.3之前回收新生代唯一的选择。以下列代码为例:

public class Stack {
    public static void main(String[] args) {
        System.out.println("准备入栈A");
        A();
    }

    public static  void A(){
        System.out.println("准备入栈B");
        B();
    }
    public static  void B(){
        System.out.println("执行B");
    }
}

正常的运行逻辑为main -> A -> B
在这里插入图片描述
期间发生GC的运行逻辑为main -> A ->单线程回收垃圾 -> B
在这里插入图片描述

  • 新生代采用复制算法,老年代采用标记-整理算法;
  • 简单而高效(与其他收集器的单线程相比)
    多线程下:
    在这里插入图片描述

2.Parallel Scavenge 和 Parallel Old收集器(收集器可以应对JVM内存在几个G内)

      Parallel收集器其实就是Serial收集器的多线程版本。它具有多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。属于并行收集,不能交替指向Mark、Sweep、Compact和Copy。

Parallel old: Parallel Old是Parallel Scavenge收集器的老年代版本, 支持多线程并发收集, 基于标记-整理算法实现 。 这个收集器 是直到JDK 6时才开始提供的, 在此之前, 新生代的Parallel Scavenge收集器一直处于相当尴尬的状态, 原因是如果 新生代选择了Parallel Scavenge收集器, 老年代除了Serial Old (PS MarkSweep) 收集器以外别无选择, 其他表现良 好的老年代收集器, 如CMS无法与它配合工作 。 由于老年代Serial Old收集器在服务端应用性能上的“拖累”, 使用 Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果 。 同样, 由于单线程的老年代收集中无法充分 利用服务器多处理器的并行处理能力, 在老年代内存空间很大而且硬件规格比较高级的运行环境中, 这种组合的总 吞吐量甚至不一定比ParNew加CMS的组合来得优秀。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合, 在注重吞吐量或者处理器 资源较为稀缺的场合, 都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
Parallel Scavenge : Parallel Scavenge收集器也是一款新生代收集器, 它同样是基于标记-复制算法实现的收集器, 也是能够并行收 集的多线程收集器.Parallel Scavenge的诸多特性从表面上看和ParNew非常相似, 那它有什么特别之处呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同, CMS等收集器的关注点是尽可能地缩短垃圾收 集时用户线程的停顿时间, 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量 (Throughput) 。 所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值
此处,粘贴至原文链接:https://blog.csdn.net/qyj19920704/article/details/123931202

仍以下列代码为例:

public class Stack {
    public static void main(String[] args) {
        System.out.println("准备入栈A");
        A();
    }

    public static  void A(){
        System.out.println("准备入栈B");
        B();
    }
    public static  void B(){
        System.out.println("执行B");
    }
}

正常的运行逻辑为main -> A -> B
在这里插入图片描述
期间发生GC的运行逻辑为main -> A ->多线程回收垃圾 -> B
在这里插入图片描述

  • 新生代采用标记-复制,老年代采用标记-整理
  • 比Serial 高效,且吞吐量较大
    多线程下:
    在这里插入图片描述

3.CMS 和 ParNew 收集器(收集器可以应对JVM内存在20个G内,一般不建议使用,建议直接使用G1)

      CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
在这里插入图片描述

      CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。不过,这种参数有利有弊,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,然后在该阶段又傻傻的触发一次。所以利弊需要把握。

3.1 初始标记(STW阶段)

      初始标记过程中,仅仅标记GC Roots能够直接关联的对象和一些年轻代引用老年代的对象,这个过程很快并且会触发 STW。如果不触发STW,那么可能就会造成对应引用的不断变更或者不断生成新的垃圾对象。-XX:+CMSParallelInitialMarkEnabled参数可以开启初始标记以多线程的方式进行 ,从而提高标记效率,缩短STW的时间。

3.2 并发标记

      并发标记过程中允许用户线程正常执行,采用三色标记算法从初始标记的对象开始遍历整个老年代,进行存活对象标记,这个过程相对过长,但对于用户来说是几乎无感知的。
      在并发标记的过程中,用户线程也在同时执行,所以老年代中也有可能在随时发生变化。比如有新的对象进入了老年代,年轻代引用了老年代中的对象或者老年代引用了年轻代中的对象这些跨代引用现象。为了解决这种问题,Hotspot虚拟机采用的卡表(Card Table)的方式记录跨代引用的对象。

3.3 重新标记 (STW阶段)

      由于在并发标记过程中是与用户线程并发执行的,会存在一些对象引用关系的变更或者是一些新对象进入到了老年代,所以会产生一些漏标或者错标的对象。这个时候就需要再次停止用户线程,进行重新标记。在重新标记阶段,会遍历整个年轻代和老年代进行存活对象标记。遍历年轻代的原因是因为可能存在跨代引用的对象。这个过程是比较耗时的,所以我们可以通过开启-XX:+CMSScavengeBeforeRemark参数,这个参数的作用是在执行重新标记前,先进行一次MinorGC,将垃圾对象进行清除,这样的话只需要扫描幸存区就可以了,而且遍历扫描的时间也会减少一些。

3.4 并发清理

      收集那些在标记阶段没有标记的对象,消亡对象所占的空间会被添加到释放列表里用于重新分配,对死亡对象的聚集工作就发生在这个点。注意:存活的对象不会被移动。

3.5 并发重置

      做一些数据结构的清理工作,为下一次收集做准备。

3.6 解决三色标记漏标的问题(增量更新,Incremental Update)

  • 增量更新:
          增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。

      当对象的成员变量的引用发生变化时,我们可以利用写屏障,将新的成员变量引用对象记录下来:

void post_write_barrier(oop* field, oop new_value) {  
  if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
      remark_set.add(new_value); // 记录新引用的对象
  }
}

      这种做法的思路是:不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)。
      增量更新破坏了条件二:【黑色对象 重新引用了 该白色对象】,从而保证了不会漏标。

4.G1收集器(收集器可以应对JVM内存在上百G)

      G1(Garbage First)是一个横跨新生代和老年代的垃圾收集器。实际上,它已经打乱了新生代和老年代的堆结构,直接将堆分成极其多个区域。每个区域都可以充当 Eden 区、Survivor 区或者老年代中的一个。它采用的是标记 - 压缩算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。G1 能够针对每个细分的区域来进行垃圾回收。在选择进行垃圾回收的区域时,它会优先回收死亡对象较多的区域。这也是 G1 名字的由来。
在这里插入图片描述

      其中每个小块的内存大小(region )是一致的,数值是在 1M 到 32M 字节之间的一个 2 的幂值数,每个Region的大小可以根据参数-XX:G1HeapRegionSize 设定,JVM 会尽量划分 2048 个左右、同等大小的 region。当然这个数字既可以手动调整,G1 也会根据堆大小自动进行调整。且G1在逻辑上区分老年代和新生代,但是在物理内存分区上并不区分新生代和老年代`
      region 大小和大对象很难保证一致,这会导致空间的浪费。上面示意图中有的区域是 Humongous 颜色,但没有用名称标记,这是为了表示,特别大的对象是可能占用超过一个 region 的。并且,region 太小不合适,会令你在分配大对象时更难找到连续空间,这是一个长久存在的情况。这本质也可以看作是 JVM 的 bug,尽管解决办法也非常简单,直接设置较大的 region 大小

在这里插入图片描述

4.1初始标记(STW阶段)

      这里其实只是对GC Roots进行扫描,标记一下与GC Roots直接关联的对象(将他们压入扫描栈marking stack中等待继续扫描),并且修改Region的TAMS指针。需要注意的是G1并没有使用mark word中的mark bit,而是使用外部的bitmap来记录mark信息。这一步会造成mutator(用户线程)的STW,但它的耗时是十分短暂的。

4.2并发标记

      以GC Roots直接关联的对象对整个堆进行可达性分析,不断从扫描栈取出引用递归扫描整个堆的对象图,将扫描到的对象进行标记,并将字段压入扫描栈,直到扫描栈被清空。这个过程也将扫描一部分SATB write barrier记录的引用和更新一部分Region的Rset。这个步骤的实现是整个G1在回收流程中较为复杂的一步,我们下文会详细的讲解。整个marking的过程耗时较长但能与mutator并行执行。

4.3最终标记(STW阶段)

      在并发标记之后又会进入一个短暂的暂停期,用于处理并发阶段遗留的少量SATB记录。

4.4清除(STW阶段)

      G1在清除时与传统的mark-compact(标记-复制算法)是不同的,他并不是必须依赖global concurrent marking的结果,而是采用CSet作为回收集合,对堆进行清理。在纯G1模式下,CSet会根据统计模型选定收益最高、开销不超过用户指定的期望停顿时间以内的若干个Region,我们下文将详细讲解此步骤。
      注:这里是指Young GC和Mixed GC,而Full GC并不包含在内。在纯G1的模式下并没有Full GC这个概念,Full GC 只有在Concurrent Mode Failure时通过Serial GC执行,在这一点上G1与CMS是一样的。

4.5 解决三色标记漏标的问题(SATB,Snapshot At The Beginning)

  • 原始快照 (STAB):
          原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删 除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描 一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

      当对象E的成员变量的引用发生变化时(objE.fieldG = null;),我们可以利用写屏障,将E原来成员变量的引用对象G记录下来:

void pre_write_barrier(oop* field) {
    oop old_value = *field; // 获取旧值
    remark_set.add(old_value); // 记录 原来的引用对象
}

【当原来成员变量的引用发生变化之前,记录下原来的引用对象】
      这种做法的思路是:尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB),当某个时刻 的GC Roots确定后,当时的对象图就已经确定了。

七 垃圾回收的时机

1. Young GC(Minor GC)

  • 大多数情况下,对象在年轻代中的Eden区进行分配,若Eden区没有足够空间,就会触发YGC(Minor GC)。

2.Full GC(Major GC)

  • 老年代的内存使用率达到了一定阈值(默认是92% 通过-XX:MaxTenuringThreshoId设置)
  • 方法区可用内存不足
  • Metaspace(元空间)扩容到了-XX:MetaspaceSize 参数的指定值。(元空间在空间不足时会进行扩容)
  • 显式调用System.gc() 或者Runtime.gc()

3. 存活对象进入老年代的条件

  • 大对象会直接进入老年代。比如:一次加载过多数据到内存(比如SQL查询未分页),导致大对象进入老年代。
  • 执行Minor GC时,JVM会先检查Survivor空间是否够用
  • young gc后,To Survivor区不足以存放存活对象
    如果够用则直接进行Minor GC
    如果不够用,则检查老年代最大连续可用空间是否大于新生代的总和或者历次晋升到老年代的对象的平均大小
          如果大于,则直接执行Young GC(这个时候执行是没有风险的)。
          如果小于,则直接执行Full GC
  • 动态年龄判定规则。To Survivor区中年龄从小到大的对象占据空间的累加之和,占到了 To Survivor区一半以上的空间,那么大于等于此年龄的对象会直接进入老年代,而不需要达到默认的晋升年龄。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值