前言
在越来越卷的环境中,大多数人或多或少都会接触到 JVM 相关的问题,希望这篇文章可以对大家有些帮助。
提示:本文章大部分内容基于 JDK 1.8 整理
JDK体系结构
在了解JVM之前,需要了解一些前置知识
JVM垃圾回收机机制(GC)
GC的两种判定方法
引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为 0 就会回收,但是 JVM没有用这种方式,因为无法判定相互循环引用(A 引用 B,B 引用 A) 的情况。
可达性性算法(引用链法): 追踪对象之间的引用关系,来确定哪些对象是可达的。
概念:将“GC Roots”对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
- GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等。
java 中垃圾收集的方法有哪些?
标记-清除
从根对象开始对像开始遍历所有可到达对象,标记为非垃圾,
清除没有被标记的对象
标记阶段:从根对象开始遍历所有可达对象,并将其标记为“非垃圾”。
清除阶段:扫描整个堆,将未被标记的对象进行回收(或者放入空闲链表等待下一次分配)。
缺点: 空间碎片,空间碎片太多会导致空间的不连续性
复制算法
将堆内存分为两个区域,一部分用于分配对象,另一部分用于垃圾回收。对象在两个区域之间来回复制,同时也进行内存压缩,从而实现垃圾回收。
缺点:空间利用率折半
标记-整理
与标记-清除算法类似,但在清除阶段后会将所有存活的对象向一端移动,然后清理边界外的内存空间
分代收集
将内存分为多个代,通常是新生代和老年代。新创建的对象首先被分配到新生代,当新生代满时触发垃圾回收,使用复制算法进行回收。而老年代中的对象则采用标记-清除或标记-整理等算法进行回收
JVM内存结构
线程共享部分
方法区: 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码(class文件)。
- 更具体的说,静态变量+常量+类信息(版本、方法、字段等)+ 运行时常量池存在方法区中。
堆区
1.1 概念
-
主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。
-
垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)。
-
堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。
1.2 堆的内存分区
-
Java堆分为年轻代(Young Generation)(1/3)和老年代(Old Generation)(2/3);年轻代又分为伊甸园(Eden)(8/10)和幸存区(Survivor区)(2/10);幸存区又分为s0(Survivor 0)(1/10)和s1(Survivor 1)(1/10)。
-
MinorGC、MajorGC、FullGC
- MinorGC 在年轻代空间不足的时候发生,
- MajorGC 指的是老年代的 GC,出现 MajorGC 一般经常伴有 MinorGC。
- FullGC
- 当老年代无法再分配内存的时候
- 元空间不足的时候
- 显示调用 System.gc 的时候。另外,像 CMS 一类的垃圾收集器,在 MinorGC 出现 promotion failure 的时候也会发生 FullGC。
-
-
年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。一个对象存活一次Minor GC,分代年龄+1,始终在 s0 和 s1 区中移动
-
-
-
-
-
-
当分代年龄达到 15 之后,在Minor GC中存活下来时,复制该非垃圾对象到老年代
-
注意:动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到阈值才能进入老年代。如果在 Survivor 区中相同年龄的所有对象的空间总和大于 Survivor 区空间的一半,则年龄大于或等于该年龄的对象直接进入老年代。
-
-
老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。
-
注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会OOM(OutOfMemoryError异常)内存溢出。
-
线程私有部分
栈(虚拟机栈、线程栈)
数据结构为栈,拥有FILO的特性。
1.1 栈的介绍
- 栈是线程私有的,他的生命周期与线程相同。
- 每个线程运行的时候都会分配一个栈的空间,即每个线程拥有独立的栈空间。
1.2 栈帧的介绍
- 栈帧是栈的元素。
- 每个方法在执行时都会创建一个栈帧。
- 栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。
1.3 栈帧中代码运行方式
1.4 栈帧信息
局部变量表:存储方法内的局部变量
操作数栈:栈的数据结构,存储需要运算操作数的临时内存空间
动态连接:把符号引用转变为直接引用
- 符号:java中 变量名、方法名、符号等都叫做符号
- 直接引用:可以被找到的内存地址
方法出口:存储方法执行完之后的返回地址
本地方法栈
-
本地方法栈用于执行本地方法或者Native方法,也就是使用本地语言(如C、C++等)编写的方法。
-
当Java程序通过JNI(Java Native Interface)调用本地方法时,本地方法栈就会被使用到
-
本地方法:通过native修饰的方法。
private native voi start0();
- 此时使用的内存由本地方法栈中内存进行分配
程序计数器
1.1 定义
- 每个线程都有自己的程序计数器
- 保留线程当前执行的方法
- 记录目前线程执行字节码指令的下一行指令地址
1.2 为什么要程序计数器
- 当线程在运行时碰到优先级更高的线程抢占CPU时,当前线程会挂起,线程重新运行时通过程序计数器继续执行未完成的任务,避免重新执行
- 注意:碰到优先级更高的线程时,会等待当前线程执行完正在执行的指令,但不会等待线程执行下一个指令
JVM内存模块关系
JVM优化
JVM常用调优工具
- jvisualvm:JDK自带工具,JDK 9开始作为独立项目
- Jmap:
- Jstack:
- Jinfo:
- Jstat:
- 阿里巴巴Arthas:
- GC日志详解:
阿里巴巴Arthas
1.下载
1 # github下载arthas
2 wget https://alibaba.github.io/arthas/arthas‐boot.jar
3 # 或者 Gitee 下载
4 wget https://arthas.gitee.io/arthas‐boot.jar
2.使用
-
启动arthas的jar包
-
选择JVM进程
-
-
使用dashboard进入监控大盘
-
为什么要JVM调优
- 优化堆内存
- 提升系统性能
- 减少GC:GC会导致系统卡顿
- Minor GC 和 Full GC 都会做 STW(stop the world)
- Full GC 做 STW 的时间会特别久
- STW:停掉用户线程
- 为什么GC过程中要STW?
- 假如不进行STW,在一条线程执行结束的时候,引用所有的资源都变为垃圾对象,此时假如上百条线程结束,将会导致GC再次发生,进行多次GC。
JVM调优原则
- 不能让朝生夕死的对象由于动态年龄判断进入老年代
面试题
能否对JVM调优,让其几乎不可能发生Full GC
- 根据业务实际情况预估项目每秒产生的对象大小,调整内存分配,避免大量垃圾对象存入老年代
JVM垃圾收集器
1.垃圾收集器总览
- 新生代可配置的收集器:Serial、ParNew、Parallel Scavenge
- 老年代配置的收集器:CMS、Serial Old、Parallel Old
- 新生代和老年代区域的收集器之间进行连线,说明他们之间可以搭配使用。
1.1 怎么解决STW时间问题
- 在很大内存空间的JVM中,通过一个区分 成多个回收区域,分端回收垃圾对象,实际回收时间变长,用户使用应用感受停顿时间变短。
2.新生代收集器
2.1 Serial 垃圾收集器
Serial收集器是最基本的、发展历史最悠久的收集器。
类型:单线程收集器
算法:采用复制算法进行年轻代的垃圾回收
特点
-
串行收集器是指使用单线程进行垃圾回收的收集器。每次回收时,串行收集器只有一个工作线程。
-
对于并行能力较弱的单CPU计算机来说,串行收集器的专注性和独占性往往有更好的性能表现。
-
它存在 Stop The World 问题,及垃圾回收时,要停止程序的运行。
-
使用 -XX:+UseSerialGC 参数可以设置新生代使用这个串行收集器
场景
- 适用于桌面应用和小型服务,或者资源受限的环境。
2.2 ParNew 垃圾收集器
ParNew其实就是Serial的多线程版本,除了使用多线程之外,其余参数和Serial一模一样。
类型:多线程收集器
算法:采用复制算法算法进行年轻代的垃圾回收
场景:适合多核服务器
特点
-
ParNew默认开启的线程数与CPU数量相同,在CPU核数很多的机器上,可以通过参数-XX:ParallelGCThreads来设置线程数。
-
它是目前新生代首选的垃圾收集器,因为除了ParNew之外,它是唯一一个能与老年代CMS配合工作的。
-
它同样存在 Stop The World 问题
-
使用 -XX:+UseParNewGC 参数可以设置新生代使用这个并行收集器
-
除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
2.3 Parallel Scavenge 收集器
类型:多线程收集器
算法:使用复制算法算法
场景:
- 适合多核服务器,强调吞吐量的应用,如大型后端系统。
- 适用于在后台运算而不需要太多交互的任务。
特点
- Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。
- 是 JDK1.8 默认收集器
- 吞吐量 = 代码运行时间 / (代码运行时间+垃圾收集时间)
- 高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务。
回收阀值可用指定的参数进行配置:
-
-XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间,可用把虚拟机在GC停顿的时间控制在MaxGCPauseMillis范围内,如果希望减少GC停顿时间可以将MaxGCPauseMillis设置的很小,但是会导致GC频繁,从而增加了GC的总时间,降低了吞吐量。所以需要根据实际情况设置该值。
-
-Xx:GCTimeRatio:设置吞吐量大小,它是一个0到100之间的整数,默认情况下他的取值是99,那么系统将花费不超过1/(1+n)的时间用于垃圾回收,也就是1/(1+99)=1%的时间。
-
另外还可以指定 -XX:+UseAdaptiveSizePolicy 打开自适应模式,在这种模式下,新生代的大小、eden、from/to的比例,以及晋升老年代的对象年龄参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
-
使用 -XX:+UseParallelGC 参数可以设置新生代使用这个并行收集器
3.老年代收集器
3.1 SerialOld 垃圾收集器
SerialOld是Serial收集器的老年代收集器版本,它同样是一个单线程收集器。
类型:单线程收集器
用途:一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。
算法:标记-整理算法进行老年代的垃圾收集
场景:适用于桌面应用和小型服务,或者资源受限的环境。
3.2 ParallelOld 收集器
老年代ParallelOldGC收集器也是一种多线程的收集器,和新生代的ParallelGC收集器一样,也是一种关注吞吐量的收集器
类型:多线程收集器
算法:标记 - 整理算法
场景:适合多核服务器,强调吞吐量的应用,如大型后端系统。
回收阀值可用指定的参数进行配置:
-
-XX:+UseParallelOldGc进行设置老年代使用该收集器
-
-XX:+ParallelGCThreads也可以设置垃圾收集时的线程数量。
3.3 CMS 收集器
类型:并发收集器
算法:标记 - 清除算法
特点:关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验)
过程
- **初始标记:**只是标记一下 GC Roots 能直接关联的对象,速度很快,STW。
- **并发标记:**进行 ReferenceChains跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
- **重新标记:**为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,STW。
- **并发清除:**清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
- CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值的时候开始回收
回收阀值可用指定的参数进行配置:
-
-XX:CMSInitiatingoccupancyFraction来指定,默认为68,也就是说当老年代的空间使用率达到68%的时候,会执行CMS回收。
-
如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代串行收集器;SerialOldGC进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作。
-
这个过程GC的停顿时间可能较长,所以-XX:CMSInitiatingoccupancyFraction的设置要根据实际的情况。
-
标记清除法有个缺点就是存在内存碎片的问题。
-
-
-XX:+UseCMSCompactAtFullCollecion可以使 CMS 回收完成之后进行一次碎片整理。
主要优点:并发收集、低停顿。
三个明显的缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
4.G1 收集器
是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征。
算法:标记 - 整理算法
类型:并发收集器。
场景:适用于需要大堆内存和同时需要控制停顿时间的应用。
特点
-
相比与 CMS 收集器,G1 收集器两个最突出的改进是:
-
基于标记-整理算法,不产生内存碎片。
-
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
-
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
过程
- 初始标记:Stop The World,仅使用一条初始标记线程对GC Roots关联的对象进行标记;
- 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢;
- 最终标记:Stop The World,使用多条标记线程并发执行;
- 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行;
4.1 参数使用
-XX:+UseG1GC # 使用G1收集器
-XX:ParallelGCThreads # 指定GC工作的线程数量
-XX:G1HeapRegionSize # 指定分区大小(1MB-32MB,且必需是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis # 目标暂停时间(默认200ms)
-XX:G1NewSizePercent # 新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)
-XX:G1MaxNewSizePercent # 新生代内存最大空间
-XX:TargetSurvivorRatio # Survivor区的填充容量(默认50%)
-XX:MaxTenuringThreshold # 最大年龄阀值(默认15)
-XX:InitiatingHeapOccupancyPercent # 老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC)
5. Z Garbage Collector (ZGC)
类型:低延迟垃圾收集器。
算法
- 着色笔技术:加快标记过程
- 读屏障:解决GC和应用之间并发导致的STW问题
- 支持 TB 级堆内存(最大 4T, JDK13 最大16TB)
- 最大 GC 停顿 10ms
- 对吞吐量影响最大,不超过 15%
特点
- JDK 11 引入的一项特性
- 在不关注容量的情况获取最小停顿时间5TB/10ms
- 低停顿时间:ZGC 设计的主要目标之一是实现低停顿时间。它通过使用并发技术,在对堆进行垃圾回收时,尽可能减少应用程序的停顿时间,从而提高应用程序的响应性。
- 可处理大内存堆:ZGC 被设计为能够有效处理非常大(几乎是整个物理内存大小)的堆内存。这使得 ZGC 特别适合需要大内存堆的应用程序。
- 分代收集:ZGC 使用分代收集的方式,将堆内存划分为不同的代(generations),并针对不同代应用不同的垃圾收集策略。
- 并发处理:ZGC 在进行垃圾回收时采用了并发处理的方式,尽量减少对应用程序的影响。这意味着大部分的垃圾收集工作可以与应用程序线程并发执行。
- 适用范围:ZGC 适用于需要低延迟和大堆内存的生产环境。它可以在不牺牲性能的前提下,显著减少长时间停顿对应用程序造成的影响。
场景:适用于有低延迟需求的应用程序和处理大内存堆且对停顿时间敏感的场景。
6.Shenandoah Collector
由 Red Hat 公司开发,旨在减少垃圾收集引起的停顿时间,与 ZGC 类似。
类型:低延迟垃圾收集器。
场景:解决长时间停顿对应用程序性能的影响而设计的,能够在保持较低停顿时间的同时有效管理大内存堆,提高应用程序的响应性能。
特点
- 并发标记和并发压缩:Shenandoah 使用并发算法来进行垃圾回收,允许垃圾收集线程与应用程序线程并发执行。这使得在大部分收集阶段内,应用程序可以继续执行而不会受到明显的停顿影响。
- 整理存储空间:Shenandoah 支持并发的整理算法,能够有效地压缩和整理堆内存,从而减少内存碎片化,提高内存利用率。
- 短暂停顿时间:Shenandoah 的停顿时间通常很短,无论是标记阶段还是压缩阶段,都能在很短的时间内完成,从而减少了对应用程序性能的影响。
- 适用范围:Shenandoah 适用于需要更低停顿时间的 Java 应用程序,尤其是那些对延迟敏感且需要处理大内存堆的应用程序。
7.Epsilon: A No-Op Garbage Collector
类型:实验性垃圾收集器
特点
- JDK 11 引入
- 无任何实际的垃圾收集行为:Epsilon 垃圾收集器不会执行任何垃圾收集操作,它会在应用程序启动时分配一块内存空间,并在应用程序结束时释放该内存空间。
- 适用于不需要垃圾回收的场景:Epsilon 主要用于那些不会产生内存碎片或者不需要进行垃圾回收的场景,例如特定类型的性能测试。
- 性能开销极低:由于没有垃圾收集操作,Epsilon 垃圾收集器在运行时几乎没有性能开销,可以提供更纯粹的性能测试环境。
- 并不是一个真正意义上的垃圾收集器,而是一种被称为“无操作”的垃圾收集器。
- Epsilon 垃圾收集器的设计初衷是为了提供一种极其轻量级的垃圾收集器实现,在某些特定情况下可以用于性能测试、性能分析或者特殊场景下的调试。
场景:更多地是作为实验性质的工具存在。在大多数情况下,我们会选择其他更为常规和成熟的垃圾收集器来满足应用程序的内存管理需求。
-
-XX:+UnlockExperimentalVMOptions #启用实验性的虚拟机选项 -XX:+UseEpsilonGC #使用 Epsilon GC(垃圾收集器)
JVM面试题
JVM的主要组成部分
类加载子系统
当Java虚拟机源代码,编译成字节码之后,虚拟机就可以把字节码读到内存里面,进行解析运行。一共有七步
运行时内存区
JVM的内存结构,不同的数据放在不同的地方,JVM 分为堆区和栈区,还有方法区
执行引擎
读取字节码执行
类加载的过程
加载、验证、准备、解析、初始化。使用、卸载
连接过程: 验证 、准备、解析
加载 验证 准备 初始化 卸载的过程是确定的,解析不一定,有可能在初始化后加载
加载 : 加载阶段是指将类的字节码文件加载到内存中,并创建相应的Class对象。在加载阶段,JVM会根据类的全限定名查找字节码文件,并读取字节码内容到内存中。加载阶段还包括验证字节码文件的合法性,例如检查文件格式、元数据等。
验证 : 在验证阶段,JVM会对类的字节码进行各种校验,以确保字节码的正确性和安全性。验证阶段主要包括四个子阶段:文件格式验证、元数据验证、字节码验证和符号引用验证。通过这些校验,JVM可以确保加载到内存中的类是可靠的
准备 : 先给对象赋予初始值,初始化阶段才会给对象赋值
解析 : 解析阶段是将常量池中的符号引用转换为直接引用的过程。符号引用是一种符号形式的描述,可以是类、字段、方法的符号名。解析阶段会将这些符号引用转换成对应的直接引用,即可以直接指向内存中的目标
初始化: 执行类构造器方法的过程
使用 : 执行用户的程序代码
卸载 : 代码执行完成之后销毁
什么是类加载器
实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。
有哪些类加载器
主要有四种类加载器
启动类加载器(引导类加载器、Bootstrap ClassLoader)
- 由c/c++语言实现的,嵌套在jvm内部
- 用来加载java核心库
- 并不继承java.lang.ClassLoader,没有父加载器
- 为扩展类加载器和系统类加载器的父加载器
- 只能加载java、javax、sun开头的类
扩展类加载器(Extension ClassLoader)
- java语言编写,sun.misc.Launche包下。
- 派生于ClassLoader类,父类加载器为Bootstrap ClassLoader
- 从java.ext.dirs系统属性指定的目录中加载类库或者加载jre/lib/ext子目录下的类库(用户可以在该目录下编写JAR,也会由此加载器所加载)
系统类加载器(System ClassLoader\AppClassLoader)
- 派生于ClassLoader,父类加载器为Extension ClassLoader
- 负责加载classpath或者系统属性java.class.path指定路径下的类库
- java语言编写,sun.misc.Launche包下。
- 负责加载程序中默认的类,可以通过getSystemClassLoader()方法获取该类的加载器。
用户自定义类加载器
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏(可以对字节码文件加密)
- 继承ClassLoader类方式实现自定义类加载器
类加载器双亲委派机制
需要使用到这个类的时候才会对它的class文件加载到内存生成class对象,加载的过程中使用的双亲委派模式,即把请求交给父类处理。
1.如果一个类加载器收到了类加载的请求,它不会自己加载,而是先把这个请求给自己的父类加载器去执行
2.如果这个父类加载器还有父类加载器,则会再将请求给自己的父类加载器,依次递归到顶层的启动类加载器
3.依次进行判断是否能完成委派(加载此类),若能完成委派则该类就由此加载器加载,若无法完成委派,则将委托给子类加载器 进行判断是否能完成委派,依次递归到底层加载器,若期间被加载则完成加载阶段不会再递归(注)。
双亲委派机制,最开始都交由启动类加载器去加载类的字节码对象
注意: 类只能被一个加载器所加载
双亲委派机制的优势(好处)
- 避免类的重复加载
- 保护程序的安全,防止核心API被篡改
怎么打破类加载器的双亲委派机制
自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法,
我们可以在loadClass()方法中修改加载逻辑,绕过父加载器的加载过程,而直接由自定义类加载器来完成类的加载
写在文章结尾
文章内容是作者整理相关资料所写,若文章有不正确内容欢迎在评论区指出。