signed

QiShunwang

“诚信为本、客户至上”

【JVM】JVM组成和垃圾回收机制,看完就悟!

2021/6/3 17:33:34   来源:

目录

JVM布局(HotSpot)(如何组成):JDK1.8

    1.堆(线程共享)

    2.JVM栈(Java虚拟机栈)(线程私有)

    3.本地方法栈(线程私有)

    4.程序计数器(线程私有)

    5.元空间(JDK1.8)(线程共享)

JVM类加载机制(Class Loading):

双亲委派模型:

垃圾回收:

1.判别死亡对象(垃圾) 

2.垃圾回收的算法:

3.垃圾收集器:

Serial收集器(新生代收集器,串行GC)

ParNew收集器(新生代收集器,并行GC)

Parallel Scavenge收集器(新生代收集器,并行GC)

Serial Old收集器(老年代收集器,串行GC)

Parallel Old收集器(老年代收集器,并行GC)

CMS收集器(老年代收集器,并发GC)

G1收集器(唯一一款全区域的垃圾回收器)


JVM布局(HotSpot)(如何组成):JDK1.8

线程私有:

虚拟机栈(先进后出)

本地方法栈

程序计数器

线程共享:

元空间

    1.堆(线程共享)

new Object()所有对象都存在此区域,此区域也是JVM中最大的区域 JVM垃圾回收就是针对此区域

    堆的划分:

        新生代:第一次创建的对象都会分配到此区域

            区域划分:

            1.Eden:80%

            2.S0 10%

            3.S1 10%

            新生代内存利用率到达90%

        老年代:经历了一定的垃圾回收只后,依然存活下来的对象会移动到老年代,大对象在创建的时候也会直接进入老年代(默认的执行次数为15,经历15次GC就会从新生代进入老年代)

 

JVM参数调优:

三种参数类型:

-X:非标准的参数设置,只能针对特殊Hotspot生效

-XX:标准的参数设置,针对所有Hotspot生效

-D:设置应用程序的参数

-Xmx——>堆最大容量(memory max)

-Xms——>堆最小容量的设置(memory start)

小技巧:通常可以把Xmx和Xms的大小设置相同,可以防止堆扩容所造成的抖动

 

面试题:为什么大对象会直接进入老年代?

因为大对象的初始化比较耗时,如果频繁的创建和消耗会带来一定的性能开销,因此最好的实现方式是直接进入老年代

    2.JVM栈(Java虚拟机栈)(线程私有)

a、局部变量表,8大基础数据类型,对象的复用

b、操作栈:每个方法都对应一个操作栈

c、动态连接:指向常量池的方法引用

d、方法返回地址:PC寄存器的地址

    3.本地方法栈(线程私有)

        它与JVM栈比较类似,只不过JVM是给Java 和 JVM使用费,而本地方法栈是为本地方法

    4.程序计数器(线程私有)

        用来记录线程执行的行号

    5.元空间(JDK1.8)(线程共享)

   前身为 JDK 1.7 方法区(永久代)

    运行时常量信息,字符串常量池,类的元信息

    JDK1.8元空间:本地内存,并且将字符串常量引用到常量池

运⾏时常量池:

运⾏时常量池是⽅法区的⼀部分,存放字⾯量与符号引⽤。

字⾯量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。

符号引⽤ : 类和结构的完全限定名、字段的名称和描述符、⽅法的名称和描述符。


JVM类加载机制(Class Loading):

1. 加载(Loading)(将静态文件转换为运行内存)

    根据类路径全名加载二进制流

    将静态的存储结构转换为运行时的数据结构

    在内存中生成一个此类的方法入口

2. 校验

    文件格式的校验

    字节码校验

3. 准备

    将类中的静态变量在内存中进行分配(只是针对变量类型的初始化)

4. 解析

    初始化final修饰的常量

    符号引用:类、方法的完全限定名(全路径名称) 

    直接引用:将符号引用加载到

5. 初始化

    此步骤开始将执行权从JVM转移到自己写的程序,开始执行构造函数

6. 使用

7. 卸载

2——4为链接

双亲委派模型:

如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆ 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试⾃⼰去完成加载。

 

优点:

保证了唯一性(父类只会加载一次)

保证了安全性(会往上张,而上层的类是系统提供的类,避免加载自定义类,从而一定程度上保证了安全性)

破坏双亲委派模型的三种场景:

    1.在JDK1.2时提出双亲委派模型,为了兼容老代码,因此在JDK1.2时已经出现了破坏双亲委派模型的场景了

    2.因为双亲委派模型自身缺点导致的,比如在父类中要调用子类的方法是无法实现的

    3.人们对于热更新的追求导致了双亲委派模型的又一次破坏

 


垃圾回收:

1.判别死亡对象(垃圾) 

    a)引用计数器算法

        给每个对象创建一个计数器,当有程序引用此类的时候计数器加1,不使用的时候减一,当计数器为0 时,表示此对象没有人使用了,就将其归为死亡对象,等待垃圾回收器进行回收。

        缺点:会有循环引用的问题

                    Hotspot 默认的垃圾回收器使用的不是计数器算法 

   b)可达性分析算法(Hotspot默认使用的算法)

可达到GC roots的类是存活的

2.垃圾回收的算法:

    标记清除算法先标记再清除,缺点是存在内存碎⽚

        可达性分析算法进行标记(存活的对象和死亡的对象)

        缺点:会带来内存碎片

 

    复制算法(新生代回收算法):将内存分为两块,⼀半使⽤,另⼀半在 GC 是将存活的对象放进去,然后清除

        优点:性能比较高(所以放在垃圾回收频率较高的新生代)

        缺点:内存的利用率低

        使用在新生代

        将内存分为三部分Eden 80,S0 10, S1 10;每次使用内存Eden+S0或Eden+S1,内存利用率可以达到90%

 

    标记-整理算法(老生代回收算法):将存活的对象都向⼀端移动,然后直接清理掉端边界以外的内存

        不会产生内存碎片,没有复制算法性能高,一般放在老生代

 

3.垃圾收集器:

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们之间可以搭配使 用。所处的区域,表示它是属于新生代收集器还是老年代收集器。在讲具体的收集器之前我们先来明确

三个概念:

  • 并行(Parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态
  • 并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序
  • 继续运行,而垃圾收集程序在另外一个CPU上。
  • 吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

吞 吐 量 = 运 行 用 户 代 码 时 间 / (运 行 用 户 代 码 时 间 + 垃 圾 收 集 时 间)

例如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%

 

Serial收集器(新生代收集器,串行GC)

 
Serial 收集器是最基本、发展历史最悠久的收集器,曾经(在 JDK 1.3.1 之前)是虚拟机新生代收集的唯一选择。
 
特性:

这个收集器是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会使用一个CPU或一 条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World).

应用场景 :

Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。

优势 :

简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 实际上到现在为止 : 它依然是虚 拟机运行在Client模式下的默认新生代收集器

 

ParNew收集器(新生代收集器,并行GC)

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。

特性 :

Serial收集器的多线程版本

应用场景 :

ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。

作为Server的首选收集器之中有一个与性能无关的很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。

JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。

对比分析 :

Serial收集器对比:

ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的.

Parallel Scavenge收集器(新生代收集器,并行GC)

特性:

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

Parallel Scavenge收集器使用两个参数控制吞吐量:

XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间

XX:GCRatio 直接设置吞吐量的大小

 

直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了。

应用场景:

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

对比分析:
Parallel Scavenge 收集器 VS CMS 等收集器:

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量Throughput)。由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为吞吐量优先收集器。

Parallel Scavenge 收集器 VS ParNew 收集器:

Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。

GC 自适应的调节策略

Parallel Scavenge收集器有一个参数- XX:+UseAdaptiveSizePolicy 。当这个参数打开之后,就不需要手工指定新生代的大小、EdenSurvivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。

注:工作流程图同 ParNew
 

Serial Old收集器(老年代收集器,串行GC)

特性:

Serial OldSerial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

应用场景:

Client 模式

Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。

Server 模式

如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器(老年代收集器,并行GC)

特性:

Parallel OldParallel Scavenge收集器的老年代版本,使用多线程标记-整理算法。

应用场景:
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge Parallel Old 收集器。 这个收集器是在JDK 1.6 中才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge 收集器,老年代除了 Serial Old 收集器外别无选择(Parallel Scavenge 收集器无法与 CMS 收集器配合工作)。由于老年代 Serial Old 收集器在服务端应用性能上的“ 拖累 ,使用了 Parallel Scavenge 收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU 的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew CMS 的组合 给力 。直到 Parallel Old 收集器出现后,“ 吞吐量优先 收集器终于有了比较名副其实的应用组合。

 

CMS收集器(老年代收集器,并发GC

特性:

CMSConcurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。CMS收集器是基于标记清除算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:

  1. 初始标记(CMS initial mark)初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  2. 并发标记(CMS concurrent mark)并发标记阶段就是进行GC Roots Tracing的过程。
  3. 重新标记(CMS remark)重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”
  4. 并发清除(CMS concurrent sweep)并发清除阶段会清除对象。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从 总体上来说, CMS 收集器的内存回收过程是与用户线程一起并发执行的。
优点:
CMS 是一款优秀的收集器,它的主要优点在名字上已经体现出来了: 并发收集、低停顿。
缺点:

CMS收集器对CPU资源非常敏感

其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会

降低。

CMS默认启动的回收线程数是(CPU数量+3/ 4,也就是当CPU4个以上时,并发回收时垃圾收集线程不少于25%CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。

CMS收集器无法处理浮动垃圾

CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次FullGC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为浮动垃圾。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不 能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次 “Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

CMS收集器会产生大量空间碎片

CMS是一款基于标记清除算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC

G1收集器(唯一一款全区域的垃圾回收器)

G1Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的region块,然后并行的对其进行垃圾回收。

G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。

G1垃圾回收器回收region的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标记-整理"算法,从局部(两个region之间)基于"复制"算法) 的策略来对region进行垃圾回收的。无论如

何,G1收集器采用的算法都意味着一个region有可能属于EdenSurvivor或者Tenured内存区域。

图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。

G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象。

年轻代垃圾收集

G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域。

 
老年代垃收集
 

对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同:

初始标记(Initial Mark)阶段 - CMS垃圾收集器的Initial Mark阶段一样,G1也需要暂停应用程序的执行,会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。但是G1的垃圾收集器的Initial Mark阶段是跟minor gc一同发生的。也就是说,在G1中,你不用像在CMS那样,单独暂停应用程序的执行来运行Initial Mark阶段,而是在G1触发minorgc的时候一并将年老代上的Initial Mark给做了。

并发标记(Concurrent Mark)阶段 - 在这个阶段G1做的事情跟CMS一样。但G1同时还多做了一件事情,就是如果在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的cleanup阶段。这也是Garbage First名字的由来。同时,在该阶段,G1会计算每个 region的对象存活率,方便后面的clean up阶段使用 。

最终标记(CMS中的Remark阶段) - 在这个阶段G1做的事情跟CMS一样, 但是采用的算法不同,G1采用一种叫做SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标记可达对象。

筛选回收(Clean up/Copy)阶段 - G1中,没有CMS中对应的Sweep阶段。相反 它有一个Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个阶段也是和minor gc一同发生的,如下图所示:G1Garbage-First)是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的CMS收集器。 如果你的应用追求低停顿,G1可以作为选择;如果你的应用追求吞吐量,G1并不带来特别明显的好处。