关于gc:面试重灾区JVM内存结构和垃圾回收机制

92次阅读

共计 7660 个字符,预计需要花费 20 分钟才能阅读完成。

JVM 介绍

1. JVM 的体系架构(内存模型)

绿色的为线程公有,橘色的为线程共有

2. 类加载器

负责将 .class 文件加载到内存中,并且将该文件中的数据结构转换为办法区中的数据结构,生成一个 Class 对象

2.1 类加载器分类

  • 自启动类加载器。Bootstrap ClassLoader类加载器。负责加载 jdk 自带的包。

    • %JAVA_HOME%/lib/rt.jar%即 JDK 源码
    • 应用 C++ 编写
    • 在程序中间接获取被该加载器加载的类的类加载器会呈现null
  • 扩大类加载器.Extension ClassLoader。负责加载 jdk 扩大的包

    • 便于将来扩大
    • %JAVA_HOME/lib/ext/*.jar%
  • 利用类加载器或零碎类加载器。AppClassLoader 或 SystemClassLOader

    • 用于加载自定义类的加载器
    • CLASSPATH门路下
  • 自定义类加载器

    • 通过实现 ClassLoader 抽象类实现

2.2 双亲委派机制

当利用类加载器获取到一个类加载的申请的时候,不会立刻解决这个类加载申请,而是将这个申请委派给他的父加载器加载,如果这个父加载器不可能解决这个类加载申请,便将之传递给子加载器。一级一级传递领导能够加载该类的类加载器。

该机制又称 沙盒平安机制 。避免开发者对JDK 加载做毁坏

2.3 突破双亲委派机制

  • 自定义类加载器,重写 loadClass 办法
  • 应用线程上下文类加载器

2.4 Java 虚拟机的入口文件

sun.misc.Launcher

3. Execution Engine

执行引擎负责执行解释命令,交给操作系统进行具体的执行

4. 本地接口

4.1 native 办法

native办法指 Java 层面不能解决的操作,只能通过本地接口调用本地的函数库( C 函数库)

4.2 Native Interface

一套调用函数库的接口

5. 本地办法栈

在加载 native 办法的时候,会将执行的 C 函数库的办法,放在这个栈区域执行

6. 程序计数器

每个线程都有程序计数器,次要作用是存储代码指令,就相似于一个执行打算。

外部保护了多个指针,这些指针指向了办法区中的办法字节码。执行引擎从程序计数器中获取下一次要执行的指令。

因为空间很小,他是以后线程执行代码的一个行号指示器 /

不会引发 OOM

7. 办法区

供各线程共享的运行时内存区域,寄存了各个类的构造信息(一个 Class 对象),包含:字段,办法,构造方法,运行时常量池。

尽管 JVM 标准将办法区形容为堆的一个逻辑局部,但它却还有一个别名叫做 Non-Heap(非堆),目标就是要和堆离开

次要有:永恒代或者元空间。存在 GC

元空间中因为间接应用物理内存的影响,所以默认的最大元空间大小为 1/4 物理内存大小

8. Java 栈

次要负责执行各种办法,是线程公有的,随线程的沦亡而沦亡,不存在垃圾回收的问题。八大数据类型和实例援用都是在函数的栈内存中分配内存的。

默认大小为 512~1024K,通过-Xss1024k 参数批改

8.1 栈和队列数据结构

FILO:先进后出

队列FIFO:先进先出

8.2 存储的数据

  • 本地变量Local Variable。包含办法的形参和返回值
  • 栈操作Operand Stack。包含各种压栈和出栈操作
  • 栈帧数据Frame Data。就相当于一个个办法。在栈空间中,办法被称为栈帧

8.3 执行流程

栈中执行的单位是栈帧,栈帧就是一个个办法。

  • 首先将 main 办法压栈,成为一个栈帧
  • 而后调用其余办法,即再次压栈
  • 栈帧中存储了这个办法的局部变量表,操作数栈、动静链接、办法进口等
  • 栈的大小和 JVM 的实现无关,通常在256K~756K

9. 办法区,栈,堆的关系

10. Heap 堆

10.1 堆内存构造

默认初始大小为物理内存的1/64,默认最大大小为1/4。在理论生产中个别会将这两个值设置为雷同,防止垃圾回收器执行完垃圾回收当前还须要进行空间的扩容计算,浪费资源。

堆外内存:内存对象调配在 Java 虚拟机的堆以外的内存,这些内存间接受操作系统治理(而不是虚拟机),这样做的后果就是可能在肯定水平上缩小垃圾回收对应用程序造成的影响。应用未公开的 Unsafe 和 NIO 包下 ByteBuffer 来创立堆外内存。

默认的堆外内存大小为, 通过 -XX:MaxDirectMemorySize= 执行堆外内存的大小

10.1.1 JDK1.7

在逻辑上划分为三个区域:

  • 新生区Young Generation Space

    • 伊甸区Eden Space
    • 幸存区Survivor 0 Space
    • 幸存区Survivor 1 Space
  • 养老区Tenure Generation Space
  • 永恒区Permanent Space(办法区)

在物理层面划分为两个区域:

  • 新生区
  • 老年区
10.1.1.1 堆内存 GC 过程

次要流程有三步:

  • Eden 满了当前登程一次轻 GC(Minor GC),没有死亡的对象,年龄 +1,寄存到from 区域
  • Eden 再次满了当前再次触发一次 GC,没有死亡的对象搁置于to 区域,而后将 from 区域中没有死亡的对象全副置于 to 区域,年龄 +1。之后每一次 GC 都会登程一次fromto的替换,哪个区域是空的那个区域就是to
  • survivor 区域满了当前,再次触发 GC,当存在对象的年龄等于 15 的时候,就会将该对象移入老年区

    • MaxTenuringThreshold通过这个参数设置当年龄为多少的时候移入
  • 老年区满了当前触发一次Full GC,如果老年区无奈再寄存对象间接报OOM

留神:每一次 GC 都会给存活的对象的年龄 +1

10.1.2 JDK1.8

1.7 相比,仅仅是将永恒代更替为了 元空间 。元空间的寄存内置是 物理内存 ,而不是JVM 中。

这样解决,能够使元空间的大小不再受虚拟机内存大小的影响,而是由零碎以后可用的空间来管制。

新生区和老年区的大小比例为 1:2, 通过-XX:NewRatio=n 设置新生代和老年代的比例,n 代表老年区所占的比例。

Eden Space 和 Survivor Space 之间的比例默认为 8:1,通过-XX:SurvivorRatio 设置伊甸区和幸存者区的比例

逻辑层面分层:

  • 新生区Young Generation Space

    • 伊甸区Eden Space
    • 幸存区Survivor 0 Space
    • 幸存区Survivor 1 Space
  • 老年区Tenure Generation Space
  • 元空间(办法区)

物理层面分层:

  • 新生区 他占据堆的 1 /3
  • 老年区 他占据堆的 2 /3

10.2 堆参数调优

10.2.1 罕用堆参数
参数 作用
-Xms 设置初始堆大小,默认为物理内存的 1 /64
-Xmx 设置最大堆大小,默认为物理内存的 1 /4
-XX:+PrintGCDetails 输入具体的 GC 日志

模仿 OOM

// 设置最大堆内存为 10m 
//-Xms10m -Xmx10m -XX:+PrintGCDetails

上面咱们具体分析一下 GC 的过程做了什么,GC 日志怎么看

名称:GC 以前占用 ->GC 之后占用(总共占用)

//GC 调配失败
GC (Allocation Failure)
    [PSYoungGen: 1585K->504K(2560K)] 1585K->664K(9728K), 0.0009663 secs] //[新生代,以前占用 -> 线程占用(总共闲暇)] 堆应用大小 -> 堆当初大小(总大小)
    [Times: user=0.00 sys=0.00, real=0.00 secs] 
    
    
[Full GC (Allocation Failure)
 [PSYoungGen: 0K->0K(2560K)] 
 [ParOldGen: 590K->573K(7168K)] 590K->573K(9728K),
 [Metaspace: 3115K->3115K(1056768K)], 0.0049775 secs] 
 [Times: user=0.00 sys=0.00, real=0.01 secs] 

11. 垃圾回收算法

11.1 垃圾回收类型

  • 一般 GC(minor GC)产生在新生区的,很频繁
  • 全局 GCmajor GC产生在老年代的垃圾收集动作,呈现一次 major GC 常常会随同至多一次的Minor GC

11.2 垃圾回收算法分类

11.2.1 援用计数法

次要思维:每存在一个对象援用就给这个对象加一,当这个对象的援用为零的时候,便触发垃圾回收。个别不应用

毛病:

  • 每次新创建对象就须要增加一个计数器,比拟节约
  • 循环援用较难解决
11.2.2 复制算法

次要思维:将对象间接拷贝一份,搁置到其余区域

长处:不会产生内存碎片

毛病:占用空间比拟大

应用场景:新生区的复制就是通过复制算法来执行的。当 Minor Gc 当前,就会幸存的对象复制一份搁置到 to

11.2.3 标记革除算法

次要思维:从援用根节点遍历所有的援用,标记出所有须要清理的对象,而后进行革除。两步实现

毛病:在进行垃圾回收的时候会打断整个代码的运行。会产生内存碎片

11.2.4 标记整顿算法

次要思维:和标记革除算法一样,最初增加了一个步骤整顿,将整顿内存碎片。三步实现

毛病:效率低,须要挪动对象。

11.3 各大垃圾回收算法比拟

11.3.1 内存效率

复制算法 > 标记革除法 > 标记整顿法

11.3.2 内存参差度

复制算法 = 标记整顿法 > 标记革除法

11.3.3 内存利用率

标记整顿法 = 标记革除法 > 复制算法

11.3.4 最优算法

通过场景应用不同的算法,来达到最优的目标

年老代:因为其对象存活时间段,对象死亡率高,所以个别应用复制算法

老年代:区域大,存活率高,个别采纳标记革除和标记整顿的混合算法。

老年代个别是由标记革除或者是标记革除与标记整顿的混合实现。以 hotspot 中的 CMS 回收器为例,CMS 是基于 Mark-Sweep 实现的,对于对像的回收效率很高,而对于碎片问题,CMS 采纳基于 Mark-Compact 算法的 Serial Old 回收器做为弥补措施:当内存回收不佳(碎片导致的 Concurrent Mode Failure 时),将采纳 Serial Old 执行 Full GC 以达到对老年代内存的整顿。

11.3.5 GCRoots

下面咱们提到标记革除算法的时候,提到了一个名词,根节点援用。那么什么叫做根节点援用呢?

根节点援用也成GCRoots,他是指垃圾回收算法进行对象遍历的根节点。即从这个对象开始往下遍历,标记须要进行回收的对象。

垃圾回收标记的过程就是:以 GCRoots 对象开始向下搜寻,如果一个对象到 GCRoots 没有任何的援用链相连时,阐明此对象不可用。

就是从 GCRoots 进行遍历,能够被遍历到的就不是垃圾,没有被遍历到的就是垃圾,断定死亡

11.3.5.1 可达性对象和不可达性对象

可达性对象是指,在对象链路援用的顶层是一个 GCRoot 援用

不可达对象是指,在对象链路援用的顶层不是一个 GCRoot 援用

艰深解释:可达性对象就是对象有一个归属,这个归属有一个术语名称叫做GCRoot,不可达性对象就是这些对象没有归属。

11.3.5.2 什么援用能够作为 GCRoots
  • 栈内的局部变量援用
  • 元空间中的动态属性援用
  • 元空间中的常量援用
  • 本地办法栈中 native 润饰的办法

说白了,就是所有裸露给开发者的援用

12. 垃圾回收器

垃圾回收器是基于 GC 算法实现的。

次要有四种垃圾回收器,不过具体有七种应用形式

12.1 四种垃圾回收器

12.1.1 串行垃圾回收器(Serial)

单线程进行垃圾回收,此时其余的线程全副被暂停

通过-XX:+UseSerialGC

12.1.2 并行垃圾回收器(Parallel)

多线程进行垃圾回收,此时其余的线程全副被暂停

12.1.3 并发垃圾回收器(CMS)

GC 线程和用户线程同时运行

12.1.4 G1 垃圾回收器

分区垃圾回收。物理上不辨别新生区和养老区,将堆内存划分为 1024 个小的 region,每一个占据的空间在2~32M,每一个region 都可能是 Eden SpaceSurvivor01 SpaceSurvivor02 SpaceOld区。

整体应用了标记整顿算法,部分应用了复制算法。通过复制算法将 GC 后的对象从一个 region 向另一个 region 迁徙,至于造成了内存碎片问题,通过整体的标记整顿算法,防止了内存碎片的诞生

在进行垃圾回收的时候间接对一个 region 进行回收,保留下来的对象通过复制算法复制到 TO 区或者 Old 区。

逻辑上堆有四个区,每一个区的大小不定,按需分配。分为 Eden SpaceSurvivor01 SpaceOldHumongous。其中 Humongous 用来寄存大对象,个别是间断存储,当因为间断 region 有余的时候,会触发 Full GC 清理四周的 Region 以寄存大对象

G1 堆内存示意

G1 垃圾回收

呈现大对象,三个 region 不能寄存,进行 FullGC

执行流程

  • 初始标记。GC 多线程,标记GCRoots
  • 并发标记。用户线程和 GC 线程同时进行 。GC 线程遍历GCRoots 的所有的对象,进行标记
  • 从新标记。修改被并发标记标记的对象,因为用户程序再次调用,而须要勾销标记的对象。GC 线程
  • 筛选回收。清理被标记的对象。GC 线程
  • 用户线程持续运行

12.1.4.1 案例
  • 初始标记。是通过一个大对象引发的 G1

  • 并发标记

  • 从新标记、筛选清理和大对象引发的Full GC

12.1.4.2 G1 罕用参数
-XX:+UseG1GC  开启 GC
-XX:G1HeapRegionSize=n : 设置 G1 区域的大小。值是 2 的幂,范畴是 1M 到 32M。指标是依据最小的 Java 堆大小划分出约 2048 个区域
-XX:MaxGCPauseMillis=n : 最大进展工夫,这是个软指标,JVM 将尽可能(但不保障)进展工夫小于这个工夫
    
-XX:InitiatingHeapOccupancyPercent=n  堆占用了多少的时候就触发 GC,默认是 45
-XX:ConcGCThreads=n  并发 GC 应用的线程数
-XX:G1ReservePercent=n 设置作为闲暇空间的预留内存百分比,以升高指标空间溢出的危险,默认值是 10%

12.2 罕用参数

DefNew      Default New Generation // 串行垃圾回收器,新生代叫法
Tenured     Old  // 串行垃圾回收器,老年代叫法
ParNew         Parallel New Generation // 新生代并行垃圾回收器,新生代叫法
PSYongGen     Parallel Scavenge // 新生代和老年代垃圾回收器,叫法
ParOldGen     Parallel Old Generation // 新生代和老年代垃圾回收器,叫法

12.3 新生代垃圾回收器

上图显示的是新生区和老年区能够应用垃圾回收器的所有品种,咱们一个一个来阐明

12.3.1 串行 GC(Serial/Serial Coping)

新生代 应用 Serial Coping 垃圾回收器应用 复制算法

老年区 默认应用 Serial Old 垃圾回收器,应用 标记革除算法和标记整顿算法

通过 -XX:+UseSerialGC 设置

12.3.2 并行 GC(ParNew)

新生区 应用 ParNew 垃圾回收器,应用复制算法

老年区 应用 Serial Old 垃圾回收器(不举荐这样应用),应用标记革除算法和标记整顿算法

通过 -XX:+UseParNewGC 启动

12.3.3 并行回收 GC(Parallel/Parallel Scavenge)

新生代 应用并行垃圾回收

老年代 应用并行垃圾回收。Java1.8 中默认应用的垃圾回收器

一个问题:Parallel 和 Parallel Scavenge 收集器的区别?

Parallel Scavenge收集器相似于 ParNew 也是一个新生代的垃圾收集器,应用了复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。

parallel Scavenge是一种自适应的收集器,虚构机会依据以后零碎运行状况收集性能监控信息,动静调整这些参数以提供最合适的提顿工夫或者最大吞吐量

他关注的点是:

可管制的吞吐量。吞吐量 = 运行用户代码工夫 /(运行用户代码工夫 + 垃圾收集工夫),

同时,当新生代抉择为 Parallel Scavenge 的时候,会默认激活老年区应用并行垃圾回收

通过 -XX:UseParallelGC 或者 -XX:UseParallelOldGC 两者会相互激活

-XX:ParallelGCThreads=n示意启动多少个 GC 线程

cpu>8 时 N= 5 或者 8

cpu<8 时 N= 理论个数

12.4 老年代垃圾回收器

12.4.1 串行垃圾回收器(Serial Old/Serial MSC)

Serial OldSerial 垃圾收集器老年代版本,是一个单线程的收集器,应用 标记整顿算法 ,运行在Client 中的年轻代垃圾回收算法

与新生代的 Serial GC 相关联

12.4.2 并行回收(Parallel Old/Parallel MSC)

Parallel Old/采纳 标记整顿算法 实现

与新生代的 Parallel Scavenge GC 相关联

12.4.3 并发标记革除 GC

CMS收集器 (Concurrent Mark Sweep 并发标记革除):一种以获取最短回收进展工夫为指标的收集器

适宜利用在互联网站或者 B/S 零碎的服务器上,器重服务器的响应速度

CMS非常适合堆内存大、CPU核数多的服务端利用,也是 G1 呈现之前大型利用的首选收集器

标记的时候,GC 线程运行;革除的时候和用户线程一起运行

通过 -XX:+UseConcMarkSweepGC 指令开启

配合 新生区的 pallellal New GC 回收器应用

当 CMS 因为 CPU 压力太大无奈应用的时候会应用 SerialGC 作为备用收集器

12.4.3.1 CMS 执行过程
  • 初始标记 (CMS initial mark)。遍历寻找到所有的GCRootsGC 线程执行,用户线程暂停
  • 并发标记 (CMS concurrent mark) 和用户线程一起遍历GCRoots,标记须要革除的对象
  • 从新标记(CMS remark)。修改标记期间,对因用户程序持续运行而不须要进行回收的对象进行修改
  • 并发革除 (CMS concurrent sweep) 和用户线程一起革除所有标记的对象

12.4.3.2 优缺点

长处:

  • 并发收集低进展

毛病:

  • 并发执行,对 CPU 资源压力大
  • 采纳标记革除算法会导致大量的内存碎片

12.5 垃圾回收器小结

参数(-XX:+……) 新生代垃圾回收器 新生代算法 老年代垃圾回收器 老年代算法
UseSerialGC SerialGC 复制算法 Serial Old GC 标整
UseParNewGC Parallel New GC 复制算法 Serial Old GC 标整
UseParllelGC Parallel Scavenge GC 复制算法 Parallel GC 标整
UseConcMarkSweepGC Parallel New GC 复制算法 CMS 和 Serial Old GC 标清
UseG1GC 整体标整 部分复制

垃圾回收算法通用逻辑

12.6 CMS 和 G1 的区别

  • G1 不会引发内存碎片
  • G1 对内存的精准管制,能够精准的去收集垃圾。依据设置的 GC 解决工夫去收集垃圾最多的区域

13. JMM

java 内存模型。是一种标准。

线程在操作变量的时候,首先从物理内存中复制一份到本人的工作内存中(栈内存),更新当前再写入物理内存中

特点:

  • 原子性
  • 可见性
  • 有序性

更多原创文章和学习教程请关注笔者同名公众号 @MakerStack 获取

正文完
 0