当前位置: 首页 > article >正文

【面试】JVM

JVM

  • 1、JVM 内存结构
  • 2、Java 的堆是如何分代的
  • 3、如果 YoungGC 存活的对象所需要的空间比 Survivor 区域的空间大怎么办
  • 4、YoungGC 和 FullGC 的触发条件是什么
  • 5、什么是 Stop The World
  • 6、JVM 如何判断对象是否存活
  • 7、JVM 有哪些垃圾回收算法
  • 8、什么是三色标记算法
  • 9、新生代和老年代的垃圾回收器有何区别
  • 10、Java 中的四种引用有什么区别
  • 11、Java 中类加载的过程是怎么样的
  • 12、JVM 中一次完整的 GC 流程是怎样的
  • 13、如何排查 OOM 的问题
  • 14、如何进行 JVM 调优

1、JVM 内存结构

根据Java虚拟机规范的定义,JVM的运行时内存区域注要由虚拟机栈本地方法栈方法区程序计数器以及运行时常量池组成。其中堆、方法区以及运行时常量池是线程之间共享的区域,而栈(本地方法栈+虚拟机栈)、程序计数器都是线程独享的。

演变过程
在这里插入图片描述

JVM 程序计数器

用于记录虚以机正在执行的字节码指令的地址。它是线程私有的,为每个线程维护一个独立的程序计数器,用于指示下一条将要被执行的字节码指令的位置。它保证线程执行一个字节码指令以后,才会去执行下一个字节码指令。

在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。在线程中程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。所以程序计数器一定是线程私有的。

JVM 虚拟机栈
在这里插入图片描述

JVM中的方法栈是线程私有的,每一个方法的调用会在方法栈中加入一个栈帧,比如这样启动 main 方法

public static void main(String[] args) {
    methodA();
}

public static void methodA() {
	int a = 0;
	int b = a + 3;
	methodB();
}

public static void methodB() {

}

栈中压入 main 方法的栈帧,执行到 methodA 方法,栈中压入 methodA 方法的栈帧,执行到 methodB 方法,栈中压入 methodB 方法的栈帧,每个方法执行完成之后,这个方法所对应的栈帧就会出栈,每个栈帧中大概存储这五个内容:局部变量表(存储局部变量的空间)、操作数栈(线程执行时使用到的数据存储空间)、动态链接(方法区的引用,例如类信息,常量、静态变量)、返回地址(存储这个方法被调用的位置,因为方法执行后还需要到方法被调用的位置)、附加信息(增加的一些规范里面没有的信息,可以添加自己的附加信息),这就是栈和栈帧。

本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

堆是存储对象实例的运行时内存区域。它是虚拟机运行时的内存总体的最大的一块,也一直占据着虚拟机内存总量的一大部分。Java堆由Java虚拟机管理,用于存放对象实例,几乎所有的对象实例都要在上面分配内存。此外,Java堆还用于垃圾回收,虚拟机发现没有被引用的对象时,就会对堆中对象进行垃圾回收,以释放内存空间。

现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

Java 7及之前堆内存逻辑上分为三部分:新生代+老年代+永久代,Java 8及之后堆内存逻辑上分为三部分:新生代+老年代+元空间

在这里插入图片描述
设置堆大小与OOM

Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项"-Xmx"和"-Xms"来进行设置

  • "-Xms"用于表示堆区的起始内存,等价于-XX: InitialHeapsize。
  • "-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapsize。

一旦堆区中的内存大小超过"-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。

通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。

默认情况下,初始内存大小:物理电脑内存大小/ 64 最大内存大小:物理电脑内存大小/4。

注意:设置的堆大小不包含元空间(或永久代)

TLAB

先来看对象的创建过程,为对象分配空间的任务等同于在java堆中划分一块大小确定的内存出来,假设java堆空间内存是绝对规整的,所有使用过的内存都放在一边,没有使用过的内存放在另一半,中间放着一个指针作为分界点的指示器,当分配内存就仅仅是把指针想空闲的空间移动一段与对象大小相等的距离,这种分配方法称为指针碰撞(Bump The Pointer)。

对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又使用了原来的指针来分配内存的情况。

解决方案其一就是使用本地线程分配缓冲(TLAB),对Eden区继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden区内,即每个线程在java堆中预先分配一小块内存,哪个线程需要分配内存,就在哪个线程的本地缓冲区中分配,当本地缓冲使用完,分配新的缓存区时才需要同步锁定。

在这里插入图片描述
尽管不是所有对象实例都能在TLAB中成功分配内存,但是JVM确定是将TLAB作为内存分配的首选,可以通过选项 -XX:UseTLAB 设置是否开启TLAB空间,默认的情况下TLAB占用的内存非常小,仅占用Eden空间的1%。

堆的内存分配过程

  1. 最开始应该使用线程分配缓冲区(tlab)来给对象分配空间,每个线程都有一个tlab,它可以保证线程的安全。
  2. 使用tlab分配空间失败时考虑通过加锁的方式(多线程),在eden区分配空间,如果eden区满了,就会触发一次minor gc,它会清除掉没有用的对象,判断一个对象是否能被搜集通常有两种算法:引用计数器法、可达性分析法;存活下来的对象将会进入eden的from区,然后清空eden区。
  3. 当eden区满了,会第二次触发minor gc,他会将eden存活下来的对象放入to区,from存活下来的对象年龄+1后也进入to区,然后清空eden区和from区。
  4. 当eden区再次满时,第三次执行minor gc,这一次eden区存活下来的对象进入from区,to区存活下来的对象年龄+1也会进入from区,然后清空eden区和to区。
  5. 随着对象的数量增加,不停的做上面两次操作,到对象的年龄到达老年带所规定的年龄阈值的时候,对象从新生代进入老年代。
  6. 随着对象增加,老年代满时会执行major gc操作,gc后对象仍然无法保存报内存溢出。

方法区

用于存诸已被加载的类信息、常量、静态变量、即时编译后的代码等数据的内存区域。每加载一个类,方法区就会分配一定的内存空间,用于存储该类的相关信息,这部分空间随着需要而动态变化。方法区的具体实现形式可以有多种,比如堆、永久代、元空间等。

在这里插入图片描述

运行时常量池

是方法区的一部分。用于存储编译阶段生成的信息,主要有字面量和符号引用常量两类。其中字面量包括了文本字符串、被声明final的常量值、基本数据类型的值和其他。其中符号引用常量包括了类的全限定名称、字段的名称和描述符、方法的名称和描述符。

堆和栈的区别

  1. 存储位置不同,堆是在堆内存中分配空间,而栈是在的栈内存中分配空间。
  2. 存储的内容不同,堆中主要存储对象,栈中主要存储本地变量。
  3. 堆是线程共享的,栈是线程独享的。
  4. 堆是垃圾回收的主要区域,不再引用这个对象,会被垃圾回收机制会自动回收。栈的内存使用是一种先进后出的机制,栈中的变量会在程序执行完毕后自动释放。
  5. 栈的大小比堆要小的多,一般是几百到几干字节。
  6. 栈的存储速度比堆快,代码执行效率高。
  7. 堆上会发生OutofMemoryError,栈上会发生StackOverflowError。

2、Java 的堆是如何分代的

Java的堆内存分代是指将不同生命周期的堆内存对象存储在不同的堆内存区域中,这里的不同的堆内存区域被定义为"代”。这样做有助于提升垃圾回收的效率,因为这样的话就可以为不同的"代"设置不同的回收策略。

一般来说,Java中的大部分对象都是朝生夕死的,同时也有一部分对象会持久存在。因为如果把这两部分对象放
到一起分析和回收,这样效率实在是太低了。通过将不同时期的对象存储在不同的内存池中,就可以节省宝贵的时
间和空间,从而改善系统的性能。

Java的堆由新生代(Young Generation)和老年代(Old Generation)组成。新生代存放新分配的对象,老年代存放长期存在的对象。新生代(Young)由年轻区(Eden)、Survivor区组成(From Survivor、To Survivor)。默认情况下,新生代的Eden区和Survivorl区的空间大小比例是8:2,可以通过-X:SurvivorRatio参数调整。

在这里插入图片描述
对象的分代晋升

一般情况下,对象将在新生代进行分配,首先会尝试在Eden区分配对象,当Eden内存耗尽,无法满足新的对象分
配请求时,将触发新生代的GC(Young GC、MinorGC),在新生代的GC过程中,没有被回收的对象会从Eden区被
般运到Survivo区,这个过程通常被称为"晋升"。

同样的,对象也可能会晋升到老年代,触发条件主要看对象的大小和年龄。对象进入老年代的条件有三个,满足一
个就会进入到老年代:

  1. 躲过15次GC。每次垃圾回收后,存活的对象的年龄就会加1,累计加到15次(Gdk8默认的),也就是某个对象躲过了1次垃圾回收,那么J小M就认为这个是经常被使用的对象,就没必要再待在年轻代中了。具体的次数可以通过-XX:MaxTenuringThreshold来设置在躲过多少次垃圾收集后进去老年代。
  2. 动态对象年龄判断。规则:在某个Survivor中,如果有一批对象的大小总是大于该Survivor的50%,那么此时大于等于该批对象年龄的对象就会直接到老年代中。
  3. 大对象直接进入老年代。-XX:PretenureSizeThreshold来设置大对象的l临界值,大于该值的就被认为是大对象,就会直接进入老年代。

什么是永久代

永久代(Permanent Generation)是HotSpot)虚拟机在以前版本中使用的一个永久内存区域,是VM中垃圾收集堆之外的另一个内存区域,它主要用来实现方法区的,其中存储了Class类信息、常量池以及静态变量等数据。

Java8以后,永久代被重构为元空间(MetaSpace)。但是,和新生代、老年代一样,永久代也是可能会发生GC的。而且,永久代也是有可能导致内存益出。只要永久代的内存分配超过限制指定的最大值,就会出现内存溢出。

3、如果 YoungGC 存活的对象所需要的空间比 Survivor 区域的空间大怎么办

毕竟一块Survivor区域的比例只是年轻的10%而已。这时候就需要把对象移动到老年代。

空间分配担保机制

如果Survivor区域的空间不够,就要分配给老年代,也就是说,老年代起到了一个兜底的作用。但是,老年代也是
可能空间不足的。所以,在这个过程中就需要做一次空间分配担保(CMS):

  • 剩余的存活对象大小,小于Survivorl区,那就直接进入Survivor区。
  • 剩余的存活对象大小,大于Survivor区,小于老年代可用内存,那就直接去老年代。
  • 剩余的存活对象大小,大于Survivor+老年代,触发"FullGC"。

4、YoungGC 和 FullGC 的触发条件是什么

YoungGC的触发条件比较简单,那就是当年轻代中的eden区分配满的时候就会触发。

FullGC的触发条件比较复杂也比较多,主要以下几种:

  • 老年代空间不足
    • 创建一个大对象,超过指定阈值会直接保存在老年代当中,如果老年代空间也不足,会触发FullGC。
    • YoungGC之后,发现要移到老年代的对象,老年代存不下的时候,会触发一次FullGC。
  • 空间分配担保失败(空间分配担保)
  • 永久代空间不足
    • 如果有永久代的话,当在永久代分配空间时没有足够空间的似乎还,会触发FullGC。
  • 代码中执行System.gc
    • 代码中执行System.gc的时候,会触发FullGC,但是并不保证一定会立即触发。

5、什么是 Stop The World

Java中Stop-The-Vorld机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。这是
Java中一种全局暂停现象,全局停顿,所有java代码停止,native代码可以执行,但不能与JVM交互。

不管选择哪种GC算法,stop-the-world都是不能彻底避免的,只能尽量降低STW的时长。

为什么需要STW呢

首先,如果不暂停用户线程,就意味着期间会不断有垃圾产生,永远也清理不干净。其次,用户线程的运行必然会导致对象的引用关系发生改变,这就会导致两种情况:漏标和错标。

  • 多标:原本不是垃圾,但是GC的过程中,用户线程将其引用关系修改,导致GC Roots不可达,成为了垃圾。这种情况还好一点,无非就是产生了一些浮动垃圾,下次GC再清理就好了。
  • 漏标:原本是垃圾,但是GC的过程中,用户线程将引用重新指向了它,这时如果GC一旦将其回收,将会导致程序运行错误。

6、JVM 如何判断对象是否存活

JVM有两种算法来判断对象是否存活,分别是引用计数法和可达性分析算法

  • 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
  • 可达性分析算法:这个算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的。

但是,并不是说当进行完可达性分析算法后,即可证明某对象可以被GC。对象是否存活,需要两次标记:

  1. 第一次标记通过可达性分析算法。如果没有GC Roots相连接的引用链,那么将第一次标记。
  2. 如果对象的finalize()方法被覆盖并且没有执行过,则放在F-Queuel队列中等待执行不一定会执行,如果一段时间后该队列的finalize()方法被执行且和GC Roots关联,则移出“即将回收”集合。如果仍然没有关联,则进行第二次标记,才会对该对象进行回收不过现在都不提倡覆盖finalize方法,它的本意是像Cpp一样在对象销毁前执行,但是它影响了JAVA的安全和GC的性能,所以第二种判断会越来越少。

7、JVM 有哪些垃圾回收算法

① 标记 - 清除算法(Tracing Collector)

标记-清除 算法是最基础的收集算法,它是由 标记 和 清除 两个步骤组成的。第一步是标记存活的对象,第二步是清除没有被标记的垃圾对象。

在这里插入图片描述

该算法的优点是当存活对象比较多的时候,性能比较高,因为该算法只需要处理待回收的对象,而不需要处理存活的对象。但是缺点也很明显,清除之后会产生大量不连续的内存碎片。导致之后程序在运行时需要分配较大的对象时,无法找到足够的连续内存。

② 标记 - 整理算法(Compacting Collector)

上述的 标记-清除 算法会产生内存区域使用的间断,所以为了将内存区域尽可能地连续使用, 标记-整理 算法应运而生。标记-整理 算法也是由两步组成,标记 和 整理。

和标记清除算法一样,先进行对象的标记,通过GC Roots节点扫描存活对象进行标记,将所有存活对象往一端空闲空间移动,按照内存地址依次排序,并更新对应引用的指针,然后清理末端内存地址以外的全部内存空间。

在这里插入图片描述

但是同样,标记整理算法也有它的缺点,一方面它要标记所有存活对象,另一方面还添加了对象的移动操作以及更新引用地址的操作,因此标记整理算法具有更高的使用成本。

③ 复制算法(Copying Collector)

无论是标记-清除算法还是垃圾-整理算法,都会涉及句柄的开销或是面对碎片化的内存回收,所以,复制算法 出现了。

复制算法将内存区域均分为了两块(记为S0和S1),而每次在创建对象的时候,只使用其中的一块区域(例如S0),当S0使用完之后,便将S0上面存活的对象全部复制到S1上面去,然后将S0全部清理掉。复制算法主要被应用于新生代,它将内存分为大小相同的两块,每次只使用其中的一块。在任意时间点,所有动态分配的对象都只能分配在其中一个内存空间,而另外一个内存空间则是空闲的。

在这里插入图片描述

但是缺点也是很明显的,可用的内存减小了一半,存在内存浪费的情况。所以 复制算法 一般会用于对象存活时间比较短的区域,例如 年轻代,而存活时间比较长的 老年代 是不适合的,因为老年代存在大量存活时间长的对象,采用复制算法的时候会要求复制的对象较多,效率也就急剧下降,所以老年代一般会使用上文提到的 标记-整理算法。

单纯的从时间长短上面来看:标记-清除 < 标记-复制 < 标记-整理。单纯从结果来看:标记-整理 > 标记-复制 >= 标记-清除

8、什么是三色标记算法

三色标记算法是一种垃圾回收的标记算法,它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的。 JVM中的CMS、G1垃圾回收器所使用垃圾回收算法即为三色标记法。

三色标记法将对象分为三种状态:白色、灰色和黑色。

  • 白色:该对象没有被标记过。
  • 灰色:该对象已经被标记过了,但该对象的引用对象还没标记完。
  • 黑色:该对象已经被标记过了,并且他的全部引用对象也都标记完了。
    在这里插入图片描述

三色标记法的标记过程可以分为三个阶段:初始标记(Initial Marking)、并发标记(Concurrent Marking)和
重新标记(Remark)。

  • 初始标记:遍历所有的根对象,将根对象和直接引用的对象标记为灰色。在这个阶段中,垃圾回收器只会扫描
    被直接或者间接引用的对象,而不会扫描整个堆。因此,初始标记阶段的时间比较短。(Stop The World)
  • 并发标记:在这个过程中,垃圾回收器会从灰色对象开始遍历整个对象图,将被引用的对象标记为灰色,并将
    已经遍历过的对象标记为黑色。并发标记过程中,应用程序线程可能会修改对象图,因此垃圾回收器需要使用 写屏障(Vrite Barrier)技术来保证并发标记的正确性。(不需要STW)
  • 重新标记:重新标记的主要作用是标记在并发标记阶段中被修改的对象以及未被遍历到的对象。这个过程中,
    垃圾回收器会从灰色对象重新开始遍历对象图,将被引用的对象标记为灰色,并将已经遍历过的对象标记为黑 色。(Stop The World)

在重新标记阶段结束之后,垃圾回收器会执行清除操作,将未被标记为可达对象的对象进行回收,从而释放内存空
间。这个过程中,垃圾回收器会将所有未被标记的对象标记为白色(White)。

以上三个标记阶段中,初始标记和重新标记是需要STW的,而并发标记是不需要STW的。其中最耗时的其实就是
并发标记的这个阶段,因为这个阶段需要遍历整个对象树,而三色标记把这个阶段做到了和应用线程并发执行,大
大降低了GC的停顿时长。

9、新生代和老年代的垃圾回收器有何区别

常见的垃圾回收器如下:

  1. 串行垃圾回收器(Serial Garbage Collector)如:Serial GC,Serial Old
  2. 并行垃圾回收器(Parallel Garbage Collector)如:Parallel Scavenge,Parallel Old,ParNew
  3. 并发标记扫描垃圾回收器(CMS Garbage Collector)
  4. G1垃圾回收器(G1 Garbage Collector,JDK7中推出,JDK9中设置为默认)
  5. ZGC垃圾回收器(The Z Garbage Collector,JDK11推出)
垃圾收集器分类作用位置使用算法特点适用场景
Serial串行新生代复制算法响应速度优先适用于单CPU环境下的client模式
ParNew并行新生代复制算法响应速度优先多CPU环境Server模式下与CMS配合使用
Parallel并行新生代复制算法吞吐量优先适用于后台运算而不需要太多交互的场景
Serial Old串行老年代标记-整理(压缩)算法响应速度优先适用于单CPU环境下的Client模式
Paraller Old并行老年代标记-整理(压缩)算法吞吐量优先适用于后台运算而不需要太多交互的场景
CMS并发老年代标记-清除算法响应速度优先适用于互联网或B/S业务
G1并发、并行新生代、老年代标记-整理(压缩)算法响应速度优先响应速度优先

新生代收集器有Serial、ParNew、Parallel Scavenge。
老年代收集器有Serial Old、Parallel Old、CMS。
整堆收集器有G1、ZGC。

在这里插入图片描述
jdk1.8默认使用ParallelGC。新生代采用的是Parallel Scavenge,老年代Parallel Old。

10、Java 中的四种引用有什么区别

  1. 强引用(Strong Reference):指向对象的引用称为强引用。如果一个对象具有强引用,那么垃圾回收器就不会回收这个对象,即使系统内存不足也不会回收它。例如,以下代码中的 obj 变量就是一个强引用。
java
Object obj = new Object();
  1. 软引用(Soft Reference):如果一个对象只被软引用所引用,则当系统内存不足时,垃圾回收器可能会回收这个对象。软引用通常用来实现缓存等功能。例如,以下代码中的 softRef 变量就是一个软引用。
SoftReference<Object> softRef = new SoftReference<>(new Object());
Object obj = softRef.get(); // 可以通过 get() 方法获取到被引用的对象
  1. 弱引用(Weak Reference):与软引用类似,如果一个对象只被弱引用所引用,则当垃圾回收器运行时,无论当前内存是否充足,都会回收这个对象。弱引用通常用来避免内存泄漏等问题。例如,以下代码中的 weakRef 变量就是一个弱引用。
WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object obj = weakRef.get(); // 可以通过 get() 方法获取到被引用的对象
  1. 虚引用(Phantom Reference):如果一个对象只被虚引用所引用,则无论该对象是否有其他引用,垃圾回收器都会将其回收,并且在回收前会通过队列通知一次程序。虚引用通常用来跟踪对象被垃圾收集器回收的状态,或者在对象被回收时执行某些操作。例如,以下代码中的 phantomRef 变量就是一个虚引用。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
Object obj = phantomRef.get(); // 返回值始终为 null
Reference<?> ref = queue.remove(); // 可以从队列中获取到被回收的虚引用

需要注意的是,软引用、弱引用和虚引用都继承自 Reference 类,并且它们在垃圾回收器运行时都可能会被回收。因此,在使用这些引用时,需要特别小心,避免由于引用被回收而导致程序出错。

11、Java 中类加载的过程是怎么样的

在这里插入图片描述

类加载主要分为三个阶段,加载,链接,初始化

  1. 加载

将类的字节码文件加载到内存中,并创建一个对应的 Class 对象来表示该类。

  1. 链接

又分为三个子阶段,验证,准备和解析

  • 验证阶段主要验证加载的class是否正确,例如,验证字节码文件的格式、方法调用是否正确。
  • 准备阶段为类的静态变量分配内存,并设置默认值。例如,如果一个类有一个静态变量 int i,则在准备阶段会为 i 分配内存,并将其初始化为 0。
  • 解析阶段会将符号引用解析为直接引用,在一个字节码文件中会用到其他类,但是字节码文件只会存用到类的类名,解析阶段就是会根据类名找到该类加载后在方法区的地址,也就是直接引用,并替换符号引用,这样运行到字节码时,就能直接找到某个类了。
  1. 初始化

在链接阶段之后,就可以开始初始化了。在初始化阶段,通常情况下,初始化代码包括静态变量的赋值、静态代码块的执行等。如果一个类有父类,则需要先初始化父类,然后再初始化子类。

Java中的类什么时候会被加载

  1. 当创建类的实例时,如果该类还没有被加载,则会触发类的加载。例如,通过关键字new创建一个类的对象
    时,JVM会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。
  2. 当使用类的静态变量或静态方法时,如果该类还没有被加载,则会触发类的加载。例如,当调用某个类的静态方法时,JVM会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。
  3. 当使用反射机制问类时,如果该类还没有被加载,则会触发类的加载。例如,当使用Class.forName方法加载某个类时,M会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。
  4. 当JVM启动时,会自动加载一些基础类,例如java.lang.Object类和java.lang.Class类等。

总之,Java中的类加载其实是延迟加载的,除了一些基础的类以外,其他的类都是在需要使用类时才会进行加载。同时,Java还支持动态加载类,即在运行时通过程序来加载类,这为ava程序带来了更大的灵活性。

12、JVM 中一次完整的 GC 流程是怎样的

  • GC 通常在以下情况下触发:

    • 年轻代空间不足:当 Eden 区满时,触发 Young GC。
    • 老年代空间不足:当老年代空间不足时,触发 Full GC。
    • 系统调用:通过 System.gc() 显式触发 GC(不推荐)。
    • 元空间不足:当元空间(Metaspace)不足时,触发 Full GC。
  • GC 的分类

    • Young GC(Minor GC):只回收年轻代(Young Generation)的内存。
    • Full GC(Major GC):回收整个堆内存(包括年轻代和老年代)以及元空间。

一次完整的 GC 流程(以 G1 为例)

(1)Young GC(Minor GC)Young GC 主要回收年轻代的内存,分为以下几个阶段:

  1. 初始标记(Initial Mark)

标记 GC Roots 直接引用的对象。需要暂停所有应用线程(Stop-The-World,STW)。

  1. 并发标记(Concurrent Mark)

从 GC Roots 开始,遍历对象图,标记所有存活对象。与应用线程并发执行,不需要暂停应用线程。

  1. 最终标记(Remark)

处理并发标记阶段遗漏的对象。需要暂停所有应用线程(STW)。

  1. 清理(Cleanup)

统计存活对象,释放完全空闲的区域。需要暂停所有应用线程(STW)。

  1. 复制(Copying)

将存活对象从 Eden 区和 Survivor 区复制到另一个 Survivor 区。如果对象年龄达到阈值(默认 15),则晋升到老年代。

(2)Full GC(Major GC)Full GC 回收整个堆内存和元空间,通常发生在以下情况:

  • 老年代空间不足。

  • 元空间不足。

  • 显式调用 System.gc()。

Full GC 的流程如下:

  1. 初始标记(Initial Mark)

标记 GC Roots 直接引用的对象。需要暂停所有应用线程(STW)。

  1. 并发标记(Concurrent Mark)

从 GC Roots 开始,遍历对象图,标记所有存活对象。与应用线程并发执行。

  1. 最终标记(Remark)

处理并发标记阶段遗漏的对象。需要暂停所有应用线程(STW)。

  1. 清理(Cleanup)

统计存活对象,释放完全空闲的区域。需要暂停所有应用线程(STW)。

  1. 压缩(Compaction)

将存活对象移动到堆的一端,释放连续的内存空间。需要暂停所有应用线程(STW)。

13、如何排查 OOM 的问题

OOM(OutOfMemoryError) 是 Java 程序中常见的错误之一,表示 JVM 的内存资源已经耗尽,无法再分配新的内存。OOM 可能会导致程序崩溃,因此理解 OOM 的原因和排查方法非常重要。

OOM 的常见原因

  • 内存泄漏:对象不再使用,但仍被引用,无法被垃圾回收。
  • 内存配置不足:堆、栈、元空间等内存区域配置过小。
  • 程序逻辑问题:创建了大量对象或线程,超出内存容量。
  • 外部资源未释放:如文件、数据库连接等未关闭。

排查 OOM 问题的步骤

  1. 确认 OOM 类型,查看错误日志,确认 OOM 的具体类型(如 Java Heap Space、Metaspace 等)
  2. 分析堆内存,使用工具(如 jmap、jvisualvm、MAT)生成堆内存快照(Heap Dump),分析堆内存快照,找出内存占用最多的对象。检查是否存在内存泄漏。
生成堆内存快照:
jmap -dump:format=b,file=heapdump.hprof <pid>
查看堆内存摘要:
jmap -heap <pid>
  1. 分析线程,使用 jstack 工具生成线程快照。检查线程数量是否过多,是否存在死锁或线程阻塞。
生成线程快照:
jstack <pid> > threaddump.txt
  1. 检查元空间的使用情况,确认是否加载了过多的类。调整元空间的大小(如 -XX:MetaspaceSize、-XX:MaxMetaspaceSize)。

  2. 检查代码中是否存在内存泄漏(如未关闭的资源、静态集合类等),检查是否创建了过多的对象或线程。

  3. 调整 JVM 参数,据 OOM 类型,调整 JVM 参数:

    • 堆内存:-Xms、-Xmx
    • 元空间:-XX:MetaspaceSize、-XX:MaxMetaspaceSize
    • 栈内存:-Xss
    • 直接内存:-XX:MaxDirectMemorySize

14、如何进行 JVM 调优

JVM 调优 是通过调整 JVM 参数、优化代码和使用合适的垃圾回收器,以提升 Java 应用程序的性能和稳定性的过程。JVM 调优的核心目标是减少 GC(垃圾回收) 的开销、降低 内存占用、提高 吞吐量 和 响应速度。以下是 JVM 调优的详细步骤和常用方法:

JVM 调优的目标

  • 减少 GC 停顿时间:降低 Full GC 的频率和持续时间。
  • 提高吞吐量:增加应用程序的处理能力。
  • 降低内存占用:减少堆内存的使用,避免内存泄漏。
  • 提高响应速度:减少应用程序的延迟。

JVM 调优的常用工具

(1)监控工具

  • jstat:查看 JVM 的 GC 和内存使用情况。
jstat -gc <pid> 1000 10
  • jmap:生成堆内存快照。
jmap -dump:format=b,file=heapdump.hprof <pid>
  • jstack:生成线程快照。
jstack <pid> > threaddump.txt

(2)分析工具

  • VisualVM:图形化工具,支持实时监控和分析堆内存、线程、CPU 使用情况。
  • MAT(Memory Analyzer Tool):分析堆内存快照,找出内存泄漏。
  • JProfiler:商业工具,支持深度性能分析。

(3)GC 日志分析工具

  • GCViewer:分析 GC 日志,生成可视化报告。
  • GCEasy:在线工具,上传 GC 日志后生成分析报告。

JVM 调优的步骤

  1. 分析应用程序
  • 了解应用程序的类型:

    • CPU 密集型:如计算密集型任务。
    • I/O 密集型:如网络请求、文件读写。
    • 内存密集型:如缓存、大数据处理。
  • 确定性能瓶颈:

使用性能分析工具(如 JProfiler、VisualVM)找出 CPU、内存、I/O 等方面的瓶颈。

  1. 监控 JVM 状态
  • 使用工具监控 JVM 的运行状态:
    • GC 日志:通过 -Xloggc 参数记录 GC 日志,分析 GC 的频率和耗时。
    • 堆内存使用情况:使用 jmap、jstat 等工具查看堆内存的使用情况。
    • 线程状态:使用 jstack 查看线程的状态,检查是否存在死锁或线程阻塞。
  1. 调整 JVM 参数
  • 根据应用程序的需求,调整 JVM 参数,主要包括以下几类:

    • 初始堆大小:-Xms
    • 最大堆大小:-Xmx
    • 年轻代大小:-Xmn
    • 老年代与年轻代的比例:-XX:NewRatio
    • Eden 区与 Survivor 区的比例:-XX:SurvivorRatio
  • 垃圾回收器选择

    • Serial GC:单线程 GC,适合小型应用。
    • Parallel GC:多线程 GC,适合高吞吐量应用。
    • CMS GC:低延迟 GC,适合响应速度要求高的应用。
    • G1 GC:适用于大内存、低延迟的应用。
    • ZGC:适用于超大内存、极低延迟的应用(JDK 11+)。
  • GC 日志和监控

    • 启用 GC 日志:
    • 启用堆内存转储:
-Xloggc:/path/to/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/heapdump.hprof
  • 元空间设置

    • 初始元空间大小:-XX:MetaspaceSize
    • 最大元空间大小:-XX:MaxMetaspaceSize
  • 栈内存设置

    • 每个线程的栈大小:-Xss
  • 直接内存设置

    • 最大直接内存大小:-XX:MaxDirectMemorySize
  1. 优化代码
  • 减少对象创建:避免不必要的对象创建,使用对象池等技术。
  • 及时释放资源:关闭文件、数据库连接等资源。
  • 避免内存泄漏:检查静态集合类、缓存等是否未释放。
  • 优化数据结构:选择合适的数据结构(如 ArrayList vs LinkedList)。
  1. 测试和验证
  • 在调整 JVM 参数后,进行压力测试和性能测试,验证调优效果。
  • 使用工具(如 JMeter、Gatling)模拟高并发场景,检查系统的稳定性和性能。

http://www.kler.cn/a/581037.html

相关文章:

  • 计算机考研C语言
  • C++设计模式-工厂模式:从原理、适用场景、使用方法,常见问题和解决方案深度解析
  • 工作记录 2017-01-04
  • 【CXX】6 内置绑定
  • Redis--Set类型
  • JVM、MySQL常见面试题(尽力局)
  • vue3中的深度选择器
  • Python----数据可视化(Seaborn合集:介绍,应用,绘图,使用FacetGrid绘图)
  • 每天一道算法题【蓝桥杯】【最长递增子序列】
  • MVCC的理解(Multi-Version Concurrency Control,多版本并发控制)
  • Spring (十)事务
  • golang从入门到做牛马:第十三篇-Go语言指针:内存的“导航仪”
  • 【day10】智慧导览:学习LBS定位精度标准
  • QwQ-32B企业级本地部署:结合XInference与Open-WebUI使用
  • PySide(PyQT),QGraphicsItem的pos()和scenePos()区别
  • 【Agent】Windows 和 CentOS 安装 Conda
  • 代理模式的C++实现示例
  • 54. 螺旋矩阵(C++)
  • 无头浏览器与请求签名技术-Cloudflare防护
  • windows下docker的安装