JVM知识点(2)

目录

[Java中可作为GC Roots的引用有哪几种?](#Java中可作为GC Roots的引用有哪几种?)

finalize方法

垃圾回收算法

标记-清除

标记-复制

标记-整理

分代收集算法

为什么要用分代收集

标记复制的标记过程和复制会不会停顿

MinorGC,MajorGC,MixedGC,FullGC

FullGC怎么清理

什么时候触发FullGC

空间分配担保是什么

垃圾收集器

Serial收集器

Serial收集器是最基础,历史最悠久的收集器

ParNew收集器

[Parallerl Scavenge收集器](#Parallerl Scavenge收集器)

SerialOld收集器

[Parallel Old收集器](#Parallel Old收集器)

CMS收集器

G1收集器

ZGC收集器

垃圾回收器的作用

CMS

重新标记

什么是三色标记

G1

CMS,G1

如何选择垃圾收集器

JVM调优

jmap

可视化的性能监控工具

第三方工具

JVM常见参数

堆内存

GC收集器

JVM调优

CPU内存过高怎么排查

内存飙高问题怎么排查

频繁的minroGc怎么办

频繁FUllGC

怎么排查

类加载机制

类加载机制

类的生命周期

类装载的过程

双亲委派

为什么使用双亲委派

破坏双亲委派

解释执行和编译执行的区别

Java中可作为GC Roots的引用有哪几种?

所谓的GC Roots,就是一组必须活跃的引用,它们是程序运行时的起点,是一切引用链的源头。在Java中,GC Roots包括以下几种:

  • 虚拟机栈中的引用(方法的参数、局部变量等)
  • 本地方法栈中 JNI 的引用
  • 类静态变量
  • 运行时常量池中的常量(String 或 Class 类型)

finalize方法

如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记,随后进行一次筛选。

筛选的条件是对象是否有必要执行 finalize()方法。

如果对象在 finalize() 中成功拯救自己------只要重新与引用链上的任何一个对象建立关联即可。

譬如把自己 (this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它就"逃过一劫";但是如果没有抓住这个机会,那么对象就真的要被回收了。

垃圾回收算法

垃圾收集算法主要是三种:

分别是标记-清除算法、标记-复制算法和标记-整理算法。

标记-清除

标记-清除算法分为两个阶段:

  • 标记:标记所有需要回收的对象
  • 清除:回收所有被标记的对象
  • 优点是实现简单,缺点是回收过程中会产生内存碎片。
标记-复制

标记-复制算法可以解决标记-清除算法的内存碎片问题,因为它将内存空间划分为两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后清理掉这一块。

缺点是浪费了一半的内存空间。

标记-整理

标记-整理算法是标记-清除复制算法的升级版,它不再划分内存空间,而是将存活的对象向内存的一端移动,然后清理边界以外的内存。

缺点是移动对象的成本比较高。

分代收集算法

分代收集算法是目前主流的垃圾收集算法,它根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代。

新生代用复制算法,因为大部分对象生命周期短。老年代用标记-整理算法,因为对象存活率较高。

为什么要用分代收集

分代收集算法的核心思想是根据对象的生命周期优化垃圾回收。

新生代的对象生命周期短,使用复制算法可以快速回收。老年代的对象生命周期长,使用标记-整理算法可以减少移动对象的成本。

标记复制的标记过程和复制会不会停顿

在标记-复制算法 中,标记阶段和复制阶段都会触发STW。

  • 标记阶段停顿是为了保证对象的引用关系不被修改。
  • 复制阶段停顿是防止对象在复制过程中被修改。

MinorGC,MajorGC,MixedGC,FullGC

Minor GC 也称为 Young GC,是指发生在年轻代的垃圾收集。年轻代包含 Eden 区以及两个 Survivor 区。

Major GC 也称为 Old GC,主要指的是发生在老年代的垃圾收集。是 CMS 的特有行为。

Mixed GC 是 G1 垃圾收集器特有的一种 GC 类型,它在一次 GC 中同时清理年轻代和部分老年代。

Full GC 是最彻底的垃圾收集,涉及整个 Java 堆和方法区。它是最耗时的 GC,通常在 JVM 压力很大时发生。

FullGC怎么清理

Full GC 会从 GC Root 出发,标记所有可达对象。新生代使用复制算法,清空 Eden 区。老年代使用标记-整理算法,回收对象并消除碎片。

什么时候触发FullGC

在进行 Young GC 的时候,如果发现老年代可用的连续内存空间 < 新生代历次 Young GC 后升入老年代的对象总和的平均大小,说明本次 Young GC 后升入老年代的对象大小,可能超过了老年代当前可用的内存空间,就会触发 Full GC。

执行 Young GC 后老年代没有足够的内存空间存放转入的对象,会立即触发一次 Full GC。

空间分配担保是什么

空间分配担保是指在进行 Minor GC 前,JVM 会确保老年代有足够的空间存放从新生代晋升的对象。如果老年代空间不足,可能会触发 Full GC。

垃圾收集器

JVM 的垃圾收集器主要分为两大类:分代收集器和分区收集器,分代收集器的代表是 CMS,分区收集器的代表是 G1 和 ZGC。

CMS 是第一个关注 GC 停顿时间的垃圾收集器,JDK 1.5 时引入,JDK9 被标记弃用,JDK14 被移除。

G1 在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。

ZGC 是 JDK11 推出的一款低延迟垃圾收集器,适用于大内存低延迟服务的内存管理和回收,在 128G 的大堆下,最大停顿时间才 1.68 ms,性能远胜于 G1 和 CMS。

Serial收集器

Serial收集器是最基础,历史最悠久的收集器

如同它的名字(串行),它是一个单线程工作的收集器,使用一个处理器或一条收集线程去完成垃圾收集工作。并且进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束------这就是所谓的"Stop The World"。

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并发版本,使用多条线程进行垃圾收集。

Parallerl Scavenge收集器

Paraller Scabenge收集器是一款新生代收集器,基于标记复制算法实现,也能够并行收集。和 ParNew 有些类似,但 Parallel Scavenge 主要关注的是垃圾收集的吞吐量------所谓吞吐量,就是 CPU 用于运行用户代码的时间和总消耗时间的比值,比值越大,说明垃圾收集的占比越小。

SerialOld收集器

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

Parallel Old收集器

Parallel Old是Parallel Scavenge 收集器的老年代版本,基于标记-整理算法实现,使用多条 GC 线程在 STW 期间同时进行垃圾回收。

CMS收集器

CMS 是一种低延迟的垃圾收集器,采用标记-清除算法,分为初始标记、并发标记、重新标记和并发清除四个阶段,优点是垃圾回收线程和应用线程同时运行,停顿时间短,适合延迟敏感的应用,但容易产生内存碎片,可能触发 Full GC。

G1收集器

G1 在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为默认的垃圾收集器。

G1 是一种面向大内存、高吞吐场景的垃圾收集器,它将堆划分为多个小的 Region,通过标记-整理算法,避免了内存碎片问题。优点是停顿时间可控,适合大堆场景,但调优较复杂。

ZGC收集器

ZGC 是 JDK 11 时引入的一款低延迟的垃圾收集器,最大特点是将垃圾收集的停顿时间控制在 10ms 以内,即使在 TB 级别的堆内存下也能保持较低的停顿时间。

它通过并发标记和重定位来避免大部分 Stop-The-World 停顿,主要依赖指针染色来管理对象状态。

  • 标记对象的可达性:通过在指针上增加标记位,不需要额外的标记位即可判断对象的存活状态。
  • 重定位状态:在对象被移动时,可以通过指针染色来更新对象的引用,而不需要等待全局同步。

适用于需要超低延迟的场景,比如金融交易系统、电商平台。

垃圾回收器的作用

垃圾回收器的核心作用是自动管理 Java 应用程序的运行时内存。它负责识别哪些内存是不再被应用程序使用的,并释放这些内存以便重新使用。

这一过程减少了程序员手动管理内存的负担,降低了内存泄漏和溢出错误的风险。

CMS

CMS 使用标记-清除算法进行垃圾收集,分 4 大步:

  • 初始标记:标记所有从 GC Roots 直接可达的对象,这个阶段需要 STW,但速度很快。
  • 并发标记:从初始标记的对象出发,遍历所有对象,标记所有可达的对象。这个阶段是并发进行的。
  • 重新标记:完成剩余的标记工作,包括处理并发阶段遗留下来的少量变动,这个阶段通常需要短暂的 STW 停顿。
  • 并发清除:清除未被标记的对象,回收它们占用的内存空间。

重新标记

remark阶段,通常结合三色标记法来执行。确保在并发标记期间所有存活对象都被正确标记。目的是修正并发标记阶段中可能遗漏的对象引用变化。

什么是三色标记

三色标记法用于标记对象的存活状态,它将对象分为三类:

  1. 白色(White):尚未访问的对象。垃圾回收结束后,仍然为白色的对象会被认为是不可达的对象,可以回收。
  2. 灰色(Gray):已经访问到但未标记完其引用的对象。灰色对象是需要进一步处理的。
  3. 黑色(Black):已经访问到并且其所有引用对象都已经标记过。黑色对象是完全处理过的,不需要再处理。

三色标记法的工作流程:

①、初始标记(Initial Marking):从 GC Roots 开始,标记所有直接可达的对象为灰色。

②、并发标记(Concurrent Marking):在此阶段,标记所有灰色对象引用的对象为灰色,然后将灰色对象自身标记为黑色。这个过程是并发的,和应用线程同时进行。

此阶段的一个问题是,应用线程可能在并发标记期间修改对象的引用关系,导致一些对象的标记状态不准确。

③、重新标记(Remarking):重新标记阶段的目标是处理并发标记阶段遗漏的引用变化。为了确保所有存活对象都被正确标记,remark 需要在 STW 暂停期间执行。

④、使用写屏障(Write Barrier)来捕捉并发标记阶段应用线程对对象引用的更新。通过遍历这些更新的引用来修正标记状态,确保遗漏的对象不会被错误地回收。

G1

G1 把 Java 堆划分为多个大小相等的独立区域Region,每个区域都可以扮演新生代或老年代的角色。

这种区域化管理使得 G1 可以更灵活地进行垃圾收集,只回收部分区域而不是整个新生代或老年代。

①、并发标记,G1 通过并发标记的方式找出堆中的垃圾对象。并发标记阶段与应用线程同时执行,不会导致应用线程暂停。

②、混合收集,在并发标记完成后,G1 会计算出哪些区域的回收价值最高(也就是包含最多垃圾的区域),然后优先回收这些区域。这种回收方式包括了部分新生代区域和老年代区域。

选择回收成本低而收益高的区域进行回收,可以提高回收效率和减少停顿时间。

③、可预测的停顿,G1 在垃圾回收期间仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以 JVM 启动时指定期望停顿时间,G1 会尽可能地在这个时间内完成垃圾回收。

CMS,G1

特性 CMS G1
设计目标 低停顿时间 可预测的停顿时间
并发性
内存碎片 是,容易产生碎片 否,通过区域划分和压缩减少碎片
收集代数 年轻代和老年代 整个堆,但区分年轻代和老年代
并发阶段 并发标记、并发清理 并发标记、并发清理、并发回收
停顿时间预测 较难预测 可配置停顿时间目标
容易出现的问题 内存碎片、Concurrent Mode Failure 较少出现长时间停顿

CMS 适用于对延迟敏感的应用场景,主要目标是减少停顿时间,但容易产生内存碎片。

G1 则提供了更好的停顿时间预测和内存压缩能力,适用于大内存和多核处理器环境。

如何选择垃圾收集器

如果应用程序只需要一个很小的内存空间(大约 100 MB),或者对停顿时间没有特殊的要求,可以选择 Serial 收集器。

如果优先考虑应用程序的峰值性能,并且没有时间要求,或者可以接受 1 秒或更长的停顿时间,可以选择 Parallel 收集器。

如果响应时间比吞吐量优先级高,或者垃圾收集暂停必须保持在大约 1 秒以内,可以选择 CMS/ G1 收集器。

如果响应时间是高优先级的,或者堆空间比较大,可以选择 ZGC 收集器。

JVM调优

操作系统层面,我用过 top、vmstat、iostat、netstat 等命令,可以监控系统整体的资源使用情况,比如说内存、CPU、IO 使用情况、网络使用情况。

JDK 自带的命令行工具层面,我用过 jps、jstat、jinfo、jmap、jhat、jstack、jcmd 等,可以查看 JVM 运行时信息、内存使用情况、堆栈信息等。

jmap

①、我一般会使用 jmap -heap <pid> 查看堆内存摘要,包括新生代、老年代、元空间等。

②、或者使用 jmap -histo <pid> 查看对象分布。

③、还有生成堆转储文件:jmap -dump:format=b,file=<path> <pid>

可视化的性能监控工具

①、JConsole:JDK 自带的监控工具,可以用来监视 Java 应用程序的运行状态,包括内存使用、线程状态、类加载、GC 等。

②、VisualVM:一个基于 NetBeans 的可视化工具,在很长一段时间内,VisualVM 都是 Oracle 官方主推的故障处理工具。集成了多个 JDK 命令行工具的功能,非常友好。

③、Java Mission Control:JMC 最初是 JRockit VM 中的诊断工具,但在 Oracle JDK7 Update 40 以后,就绑定到了 HotSpot VM 中。不过后来又被 Oracle 开源出来作为了一个单独的产品。

第三方工具

MAT,GChisto

JVM常见参数

堆内存

  • -Xms:初始堆大小
  • -Xmx:最大堆大小
  • -XX:NewSize=n:设置年轻代大小
  • -XX:NewRatio=n:设置年轻代和年老代的比值。如:n 为 3 表示年轻代和年老代比值为 1:3,年轻代占总和的 1/4
  • -XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。如 n=3 表示 Eden 占 3 Survivor 占 2,一个 Survivor 区占整个年轻代的 1/5

GC收集器

  • -XX:+UseSerialGC:设置串行收集器
  • -XX:+UseParallelGC:设置并行收集器
  • -XX:+UseParalledlOldGC:设置并行老年代收集器
  • -XX:+UseConcMarkSweepGC:设置并发收集器

JVM调优

JVM 调优是一个复杂的过程,调优的对象包括堆内存、垃圾收集器和 JVM 运行时参数等。

如果堆内存设置过小,可能会导致频繁的垃圾回收。

在项目运行期间,我会使用 JVisualVM 定期观察和分析 GC 日志,如果发现频繁的 Full GC,我会特意关注一下老年代的使用情况。

接着,通过分析 Heap dump 寻找内存泄漏的源头,看看是否有未关闭的资源,长生命周期的大对象等。

之后进行代码优化,比如说减少大对象的创建、优化数据结构的使用方式、减少不必要的对象持有等。

CPU内存过高怎么排查

首先使用top命令观看CPU占用情况

接着使用jstack命令查看对应进程的线程堆栈信息

然后再使用 top 命令查看进程中线程的占用情况,找到占用 CPU 较高的线程 ID。

top 命令显示的线程 ID 是十进制的,而 jstack 输出的是十六进制的,所以需要将线程 ID 转换为十六进制。

jstack 的输出中搜索这个十六进制的线程 ID,找到对应的堆栈信息。

最后,根据堆栈信息定位到具体的业务方法,查看是否有死循环、频繁的垃圾回收、资源竞争导致的上下文频繁切换等问题。

内存飙高问题怎么排查

内存飚高一般是因为创建了大量的 Java 对象导致的,如果持续飙高则说明垃圾回收跟不上对象创建的速度,或者内存泄漏导致对象无法回收。

第一,先观察垃圾回收的情况,可以通过 jstat -gc PID 1000 查看 GC 次数和时间。

第二步,通过 jmap 命令 dump 出堆内存信息。

第三步,使用可视化工具分析 dump 文件,比如说 VisualVM,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。

频繁的minroGc怎么办

频繁的 Minor GC 通常意味着新生代中的对象频繁地被垃圾回收,可能是因为新生代空间设置的过小,或者是因为程序中存在大量的短生命周期对象(如临时变量)。

可以使用 GC 日志进行分析,查看 GC 的频率和耗时,找到频繁 GC 的原因。

或者使用监控工具查看堆内存的使用情况,特别是新生代(Eden 和 Survivor 区)的使用情况。

如果是因为新生代空间不足,可以通过 -Xmn 增加新生代的大小,减缓新生代的填满速度。

如果对象需要长期存活,但频繁从 Survivor 区晋升到老年代,可以通过 -XX:SurvivorRatio 参数调整 Eden 和 Survivor 的比例。默认比例是 8:1,表示 8 个空间用于 Eden,1 个空间用于 Survivor 区。调整为 6 的话,会减少 Eden 区的大小,增加 Survivor 区的大小,以确保对象在 Survivor 区中存活的时间足够长,避免过早晋升到老年代。

频繁FUllGC

频繁的 Full GC 通常意味着老年代中的对象频繁地被垃圾回收,可能是因为老年代空间设置的过小,或者是因为程序中存在大量的长生命周期对象。

怎么排查

过专门的性能监控系统,查看 GC 的频率和堆内存的使用情况,然后根据监控数据分析 GC 的原因。

或者:我一般会使用 JDK 的自带工具,包括 jmap、jstat 等。

或者使用一些可视化的工具,比如 VisualVM、JConsole 等,查看堆内存的使用情况。

假如是因为大对象直接分配到老年代导致的 Full GC 频繁,可以通过 -XX:PretenureSizeThreshold 参数设置大对象直接进入老年代的阈值。

或者将大对象拆分成小对象,减少大对象的创建。比如说分页。

假如是因为内存泄漏导致的频繁 Full GC,可以通过分析堆内存 dump 文件找到内存泄漏的对象,再找到内存泄漏的代码位置。

假如是因为长生命周期的对象进入到了老年代,要及时释放资源,比如说 ThreadLocal、数据库连接、IO 资源等。

假如是因为 GC 参数配置不合理导致的频繁 Full GC,可以通过调整 GC 参数来优化 GC 行为。或者直接更换更适合的 GC 收集器,如 G1、ZGC 等。

类加载机制

类加载机制

JVM 的操作对象是 Class 文件,JVM 把 Class 文件中描述类的数据结构加载到内存中,并对数据进行校验、解析和初始化,最终转化成可以被 JVM 直接使用的类型,这个过程被称为类加载机制。

  • 类加载器:负责加载类文件,将类文件加载到内存中,生成 Class 对象。
  • 类加载过程:包括加载、验证、准备、解析和初始化等步骤。
  • 双亲委派模型:当一个类加载器接收到类加载请求时,它会把请求委派给父------类加载器去完成,依次递归,直到最顶层的类加载器,如果父------类加载器无法完成加载请求,子类加载器才会尝试自己去加载。

类的生命周期

一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 、验证、准备、解析、初始化、使用和卸载。

类装载的过程

类装载过程包括三个阶段:载入、链接和初始化。

①、载入:将类的二进制字节码加载到内存中。

②、链接可以细分为三个小的阶段:

  • 验证:检查类文件格式是否符合 JVM 规范
  • 准备:为类的静态变量分配内存并设置默认值。
  • 解析:将符号引用替换为直接引用。

③、初始化:执行静态代码块和静态变量初始化。

双亲委派

双亲委派模型要求类加载器在加载类时,先委托父加载器尝试加载,只有父加载器无法加载时,子加载器才会加载。

为什么使用双亲委派

①、避免类的重复加载:父加载器加载的类,子加载器无需重复加载。

②、保证核心类库的安全性 :如 java.lang.* 只能由 Bootstrap ClassLoader 加载,防止被篡改。

破坏双亲委派

重写 ClassLoader 的 loadClass() 方法。

解释执行和编译执行的区别

  • 解释:将源代码逐行转换为机器码。
  • 编译:将源代码一次性转换为机器码。

一个是逐行,一个是一次性,再来说说解释执行和编译执行的区别:

  • 解释执行:程序运行时,将源代码逐行转换为机器码,然后执行。
  • 编译执行:程序运行前,将源代码一次性转换为机器码,然后执行。

但 JIT 的出现打破了这种刻板印象,JVM 会将热点代码(即运行频率高的代码)编译后放入 CodeCache,当下次执行再遇到这段代码时,会从 CodeCache 中直接读取机器码,然后执行。

相关推荐
张人玉5 小时前
c#抽象类和接口的异同
java·jvm·c#
笑衬人心。9 小时前
对象的创建过程
java·jvm
Joker—H11 小时前
【Java】JVM虚拟机(java内存模型、GC垃圾回收)
java·开发语言·jvm·经验分享·个人开发·gc
极客BIM工作室11 小时前
C++异常捕获:为何推荐按引用(by reference)捕获?
java·jvm·c++
SoulruiA12 小时前
JVM 崩溃(Fatal Error)解决方法
jvm
loop lee13 小时前
【JVM】常见的 Java 垃圾回收算法以及常见的垃圾回收器介绍及选型
java·jvm·算法
kk在加油14 小时前
JVM指令集
jvm
小刘|15 小时前
JVM知识点(1)
jvm
回家路上绕了弯16 小时前
Java 堆深度解析:内存管理的核心战场
java·jvm