八股文--->JVM虚拟机

目录

一:说说JAVA的内存区域

二:类加载机制

三:何为双亲委派机制,为什么要设计它?

四:Tomcat打破双亲委派机制

 五:STW

 Stop一the一World

六:怎么判断对象是否可以被回收?

七:常见的引用类型 

八:Minor GC和Full GC 有什么不同呢?

九:关于Eden区和Survivor,老年代

九(1)美团面试 新生代中的survival区满了怎么办?老年代满了怎么办?

十:阿里面试题:能否对JVM调优,让其几乎不发生Full Gc

十(1)JVM堆内存的大小调整参数

十一:逃逸分析

 十二: 垃圾收集算法

(1)标记复制算法

(2)标记-清除算法​​​​​​​

(3)标记-整理算法

十三:常见的垃圾收集器 

十四:CMS的缺点

十五:三色标记

十六:G1

G1收集器一次GC的运作过程大致分为以下几个步骤


一:说说JAVA的内存区域

 

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;

  • Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;

  • 本地方法栈(Native Method Stack):我们知道,java底层用了很多c的代码去实现,而其调用c端的方法上都会有native,代表本地方法服务,而本地方法栈就是为其服务的。

  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;

  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

二:类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验解析初始化,最终形成可以被虚拟机直接使用的java类型

(1)加载:根据查找路径找到相应的class文件,然后导入;

(2)校验:检查加载的class文件的正确性

(3)准备:给类中的静态变量分配空间

(4)解析:虚拟机将类中的符号引用直接替换成直接引用;

(5)初始化:对静态变量和静态代码块进行初始化工作。

三:何为双亲委派机制,为什么要设计它?

类加载器收到了加载类的请求,不会自己先去加载,而是把它交给自己的父类去加载,层层迭代

好处:

沙箱安全机制:例如自己写了java.lang.String.class的类不会被加载,防止了核心API被司仪篡改;

避免类的加载重复:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性

四:Tomcat打破双亲委派机制

tomcat的几个主要类加载器:

  1. commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;

  2. catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;

  3. sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;

  4. WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的

  5. WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;

从图中的委派关系中可以看出:

CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。

WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。

tomcat这种类加载机制违背了java推荐的双亲委派模型了吗?答案是:违背了。

很显然,tomcat不是这样实现,tomcat为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。

 五:STW

 Stop一the一World

简称STW,指的是Gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。

六:怎么判断对象是否可以被回收?

垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。

一般有两种方法来判断:

(1)引用计数器:每当有一个地方引用他,计数器就加1,当引用失效,计数器就减一;当计数器的值为0时,这个对象就是可回收的;它有一个缺点不能解决循环引用的问题

(2)根可达算法:这也是「「JVM 默认使用」」的寻找垃圾算法,它的原理就是定义了一系列的根,我们把它称为 「「"GC Roots"」」 ,从 「「"GC Roots"」」 开始往下进行搜索,走过的路径我们把它称为 「「"引用链"」」 ,当一个对象到 「「"GC Roots"」」 之间没有任何引用链相连时,那么这个对象就可以被当做垃圾回收了

七:常见的引用类型 

  • 强引用:发生 gc 的时候不会被回收。

  • 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。

  • 弱引用:有用但不是必须的对象,在下一次GC时会被回收。

  • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

八:Minor GC和Full GC 有什么不同呢?

Minor GC/Young GC :指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC/Full GC :     一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。  

九:关于Eden区和Survivor,老年代

 

 (1)Eden区和Survivor区默认8:1:1

        大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活 的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所 以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可,

(2)大对象直接进入老年代

        大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC ,再执行下上面的第一个程序会发现大对象直接进了老年代

(3)长期存活对象将进入老年代

        既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
        如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor空间中,并将对象年龄设为1。
        对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

(4)对象动态年龄判断

        当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于 这批对象年龄最大值的对象,就可以直接进入老年代了
        例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年 龄判断机制一般是在minor gc之后触发的。

(5)老年代空间分配担保机制

        年轻代每次 minor gc 之前JVM都会计算下老年代 剩余可用空间
        如果这个可用空间小于年轻代里现有的所有对象大小之和( 包括垃圾对象 )就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了
        如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的 平均大小 。
        如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,
        如果回收完还是没有足够空间存放新的对象就会发生"OOM"
        当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full,gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM“

ps:何为OOM

序申请内存过大,虚拟机无法满足我们,然后自杀了。这个现象通常出现在大图片的APP开发,或者需要用到很多图片的时候。通俗来讲就是我们的APP需要申请一块内存来存放图片的时候,系统认为我们的程序需要的内存过大,及时系统有充分的内存,比如1G,但是系统也不会分配给我们的APP,故而抛出OOM异常,程序没有捕捉异常,故而弹窗崩溃了
 

九(1)美团面试 新生代中的survival区满了怎么办?老年代满了怎么办?

答:这个没有深入了解,我的思路是survival区满了那么将年龄比较大的对象移到老年代。那如果老年代也满了。。。那。。。那。。。也不能随机清理吧,万一程序崩了呢。

答案:新生代调优建议:对于上述的新生代问题, 如果服务器内存足够用, 建议是直接增大新生代空间(如 -Xmn)。如果内存不够用, 则增加 Survivor 空间, 减少 Eden 空间, 但是注意减少 Eden 空间会增加 Minor GC 频率, 要考虑到应用对延迟和吞吐量的指标最终是否符合。要增大多少 Survivor 空间? 需要观察多次 Minor GC 过程, 看 Minor GC 后存活下来的对象大小, 最终确定 Survivor 的合适大小。 整个调优过程可能需要几次调整, 才能找到比较合适的值。调整几次后, 如果内存还是不够用, 就要需要考虑增大服务器内存, 或者把负载分担到更多的 JVM 实例上。Survivor 空间计算公式: survivor 空间大小 = -Xmn[value] / (-XX:SurvivorRatio= + 2)  。

老年代满了该咋办我也没找到答案,求大佬指出思路。

十:阿里面试题:能否对JVM调优,让其几乎不发生Full Gc

        对象动态年 龄判断机制一般是在minor gc之后触发的。
        线程每秒产生60m的对象,每过14s,新生代的eden就会被占满,此时出发minor gc,60m>Survivor的一半,此时60m的对象就会被放入老年区,老年区会被慢慢的沾满,导致full gc;
        如果提高新生代的存储大小,按照8:1:1,Survivor的存储大小也会被提高到200m,此时60m<Survivor的一半,60m的对象会被放入Survivor而不会进入老年代,线程继续产生60m对象进入到eden区的时候,产生full gc,会回收eden和Survivor
        因此答案为:提高新生代的存储大小
 

十(1)JVM堆内存的大小调整参数

指定初始和最大堆大小

您可以使用标志-Xms(初始堆大小)和-Xmx(最大堆大小)指定初始和最大堆大小。如果您知道应用程序需要多少堆才能正常工作,您可以将-Xms和设置-Xmx为相同的值。如果没有,JVM 将使用初始堆大小开始,然后增长 Java 堆,直到在堆使用和性能之间找到平衡。

十一:逃逸分析

对象栈上分配
        我们通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。

        为了减少临时对象在堆内分配的数量,JVM通过 逃逸分析 确定该对象不会被外部访问。如果不会逃逸可以将该对象在 栈上分配 内存,这样该对象所占用的 内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。


        对象逃逸分析 :就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
 

 public User test1() { 
     User user = new User(); 
     user.setId(1); 
     user.setName("zhuge"); 
     //TODO 保存到数据库 
     return user; 
 } 
 
 public void test2() { 
     User user = new User(); 
     user.setId(1); 
     user.setName("zhuge"); 
     //TODO 保存到数据库 
 }

很显然test1方法中的user对象被返回了,这个对象的作用域范围不确定,test2方法中的user对象我们可以确定当方法结 束这个对象就可以认为是无效对象了,对于这样的对象我们其实可以将其分配在栈内存里,让其在方法结束时跟随栈内 存一起被回收掉。

        JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过 标量替换 优先分配在栈上(栈上分配 ),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
        标量替换: 通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时, JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就 不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认 开启。
        标量与聚合量: 标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及 reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一 步分解的聚合量。

 十二: 垃圾收集算法

(1)标记复制算法

为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的 内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对 内存区间的一半进行回收。

(2)标记-清除算法

算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标 记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。它是最基础的收集算法,比较简单,但是会带来 两个明显的问题: 1. 效率问题 (如果需要标记的对象太多,效率不高) 2. 空间问题(标记清除后会产生大量不连续的碎片)

(3)标记-整理算法

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

十三:常见的垃圾收集器 

1.1 Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工 作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

新生代采用复制算法,老年代采用标记-整理算法。

 Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5 以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

1.2 Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))

Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算 法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(- XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。 Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以 选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

新生代采用复制算法,老年代采用标记-整理算法。 

 Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。
 

1.3 ParNew收集器(-XX:+UseParNewGC)

ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用

新生代采用复制算法,老年代采用标记-整理算法

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器)配合工作。


 

 1.4 CMS收集器(-XX:+UseConcMarkSweepGC(old))

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体 验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程 (基本上)同时工作。 从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面 几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。

并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但 是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的 对象状态发生改变。

重新标记: 重新标记是为了解决第二步并发标记所导致的标错情况,这里简单举个例子:并发标记时a没有被任何对象引用,此时垃圾回收器将该对象标位垃圾,在之后的标记过程中,a又被其他对象引用了,这时候如果不进行重新标记就会发生「「误清除」」。这部分内容也是在「「STW」」的情况下去标记的。

这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三 色标记里的增量更新算法(见下面详解)做重新标记。

并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。

并发重置:重置本次GC过程中的标记数据。

十四:CMS的缺点

CMS的「「三个缺点」」:

  • 「1.影响用户线程的执行效率」

    • CMS默认启动的回收线程数是(处理器核心数 + 3)/ 4 ,由于是和用户线程一起并发清理,那么势必会影响到用户线程的执行速度,并且这个影响「「随着核心线程数的递减而增加」」。所以 JVM 提供了一种 "「「增量式并发收集器」」"的 CMS 变种,主要是用来减少垃圾回收线程独占资源的时间,所以会感觉到回收时间变长,这样的话「「单位时间内处理垃圾的效率就会降低」」,也是一种缓和的方案。

  • 「2.会产生"浮动垃圾"」

    • 之前说到 CMS 真正清理垃圾是和用户线程一起进行的,在「「清理」」这部分垃圾的时候「「用户线程会产生新的垃圾」」,这部分垃圾就叫做浮动垃圾,并且只能等着下一次的垃圾回收再清除。

  • 「3.会产生碎片化的空间」

    • CMS 是使用了标记删除的算法去清理垃圾的,而这种算法的缺点就是会产生「「碎片化」」,后续可能会「「导致大对象无法分配」」从而触发「「和 Serial Old 一起配合使用」」来处理碎片化的问题,当然这也处于 「「STW」」的情况下,所以当 java 应用非常庞大时,如果采用了 CMS 垃圾回收器,产生了碎片化,那么在 STW 来处理碎片化的时间会非常之久。

十五:三色标记

在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。

这里我们引入“三色标记”来给大家解释下,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以 下三种颜色:

白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
灰色:正在搜索的对象
黑色:搜索完成的对象(不会当成垃圾对象,不会被GC)

 多标-浮动垃圾

在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过 (被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动 垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。 另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

漏标-读写屏障

漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决。

有两种解决方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB) 。

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

原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾) 以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。
 

十六:G1

G1可以理解为是CMS的升级,G1 垃圾收集器可以给你设定一个你希望Stop The Word 停顿时间,G1垃圾收集器会根据这个时间尽量满足你

G1将Java堆划分为多个大小相等的独立区域( Region ),JVM最多可以有2048个Region。

G1保留了年轻代和老年代的概念,堆的划分不再是「物理」形式,而是以「逻辑」的形式进行划分;比如说:新对象一般会分配到Eden区、经过默认15次的Minor GC新生代的对象如果还存活,会移交到老年代等等…

年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。

一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。

G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样, 唯一不同的是对大对象的处理 ,G1有专门分配大对象的Region叫Humongous区 ,而不是让大对象直接进入老年代的Region中。

在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。

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

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

G1收集器一次GC的运作过程大致分为以下几个步骤:

初始标记 (initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
并发标记 (Concurrent Marking):同CMS的并发标记
最终标记 (Remark,STW):同CMS的重新标记
筛选回收 (Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序 , 根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划 

比如说老年代此时有1000个 Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set ,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代

回收算法主要用的是复制算法 , 将一个region中的存活对象复制到另一个region中,这种不会像CMS那样 回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片 。

G1垃圾收集分类
YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做YoungGC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC


MixedGC
        不是FullGC,老年代的堆占有率达到参数( -XX:InitiatingHeapOccupancyPercent )设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区

        正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现 没有足够 的空region 能够承载拷贝对象就会触发一次Full GC


Full GC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)

G1垃圾收集器优化建议
假设参数 -XX:MaxGCPauseMills (GC停顿时间)设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。

如果设置的停顿时间太小,年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。 所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.
 

什么场景适合使用G1

1. 50%以上的堆被存活对象占用
2. 对象分配和晋升的速度变化非常大
3. 垃圾回收时间特别长,超过1秒
4. 8GB以上的堆内存(建议值)
5. 停顿时间是500ms以内

因为G1里面的算法时间复杂度很高,如果内存不大,G1的优势不一定比CMS高

每秒几十万并发的系统如何优化JVM
Kafka类似的支撑高并发消息系统大家肯定不陌生,对于kafka来说,每秒处理几万甚至几十万消息时很正常的,一般来说部署kafka需要用大内存机器(比如64G),也就是说可以给年轻代分配个三四十G的内存用来支撑高并发处理,这里就涉及到一个问题了,我们以前常说的对于eden区的young gc是很快的,这种情况下它的执行还会很快吗?很显然,不可能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按kafka这个并发量放满三四十G的eden区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为young gc卡顿几秒钟没法处理新消息,显然是不行的。那么对于这种情况如何优化了,我们可以使用G1收集器,设置 -XX:MaxGCPauseMills 为50ms,假设50ms能够回收三到四个G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。
G1天生就适合这种大内存机器的JVM运行,可以比较完美的解决大内存垃圾回收时间过长的问题。
 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值