JVM面试题

252 阅读39分钟

其中
内存模型,类加载机制,GC是重点方面.
性能调优部分更偏向应用,重点突出实践能力.
编译器优化执行模式部分偏向于理论基础,重点掌握知识点.

需了解
内存模型各部分作用,保存哪些数据.
类加载双亲委派加载机制,常用加载器分别加载哪种类型的类.
GC分代回收的思想和依据以及不同垃圾回收算法的回收思路和适合场景.
性能调优常有JVM优化参数作用,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法.
执行模式解释/编译/混合模式的优缺点,Java7提供的分层编译技术,JIT即时编译技术,OSR栈上替换,C1/C2编译器针对的场景,C2针对的是server模式,优化更激进.新技术方面Java10的graal编译器
编译器优化javac的编译过程,ast抽象语法树,编译器优化和运行器优化.

1693468518275.png

零、下JVM 的主要组成部分及其作用?

1693544442351.png

JVM包含两个子系统和两个组件,分别为
Class loader(类装载子系统)
Execution engine(执行引擎子系统)
Runtime data area(运行时数据区组件)
Native Interface(本地接口组件)。

「Class loader(类装载):」

  • 根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区的方法区(JVM内存)中。

「Execution engine(执行引擎)」:

  • 方法执行时每行代码由解释器执行
  • 方法里的热点代码(频繁调用的代码)由JIT Compiler做优化后执行
  • GC对堆里不再被引用的对象进行垃圾回收

「Runtime data area(运行时数据区域)」:即我们常说的JVM的内存。

  • 类 : 方法区
  • 实例对象 :堆
  • 堆里的实例对象调用方法时会用到虚拟机栈、程序计数器、本地方法栈

「Native Interface(本地接口):」

  • 与native lib交互,是其它编程语言交互的接口。
  • 借助本地方法接口调用底层操作系统的一些方法

首先通过编译器把 Java源代码转换成字节码,Class loader(类装载)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

一、JVM内存模型

运行时数据区.png

一个进程就对应着一个JVM实例,一个JVM实例就有一个运行时数据区,只有一个方法区和堆,一个进程中的多个线程共享方法区和堆空间,每个线程有自己独立的程序计数器、本地方法栈、虚拟机栈

线程独占: 栈、本地方法栈、程序计数器
线程共享: 堆、方法区

1、栈:

又称方法栈,线程私有的,线程执行方法是都会创建一个栈帧,用来存储局部变量表,操作栈,动态链接,方法出口等信息.
调用方法时执行入栈,方法返回式执行出栈.
栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小),HotSpot虚拟机不支持动态扩展。
栈的大小可以是固定的,或者是动态扩展的。
如果请求的栈深度大于最大可用深度,则抛出stackOverflowError;
如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError。
使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:

1693540890106.png

image.png

2、本地方法栈 与栈类似,也是用来保存执行方法的信息.
执行Java方法时使用栈,执行Native方法时使用本地方法栈.

3、程序计数器
场景:当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,操作系统会动态地在不同的线程之间进行切换,从而实现多线程并发执行。
存储:保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器。
如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。

4、堆

JVM内存管理最大的一块,对被线程共享,目的是存放对象实例和数组,是垃圾回收的主要区域(在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除),分为新生代和老年代。刚创建的对象在新生代的Eden区中,经过GC后进入新生代的S0区中,再经过GC进入新生代的S1区中,15次GC后仍存在就进入老年代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则OutOfMemoryError。

所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)虚拟机通过 -XX:UseTLAB 设定它的。

内存细分: 1.png Eden 存放新生的对象
Survivor Space 有两个,存放每次垃圾回收后存活的对象(s0+s1)
Young Generation 即图中的Eden + From Space(s0) + To Space(s1)
Old Generation Tenured Generation 即图中的Old Space主要存放应用程序中生命周期长的存活对象

为什么需要把Java堆分代?

  • 经研究,不同对象的生命周期不同,70%-99%的对象是临时对象

    • 新生代:有Eden、两块大小相同的Survivor(又称为from/to,s0和s1)构成,to区总为空
    • 老年代:存放新生代中经历多次GC仍然存活的对象
  • 分代的唯一理由就是优化GC性能,如果没有分代,那所有的对象都在一块,GC时就会对堆的所有区域进行扫描;而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来

5、方法区

又称非堆区,用于存储类的元数据,如类的结构、字段、方法信息、静态变量、常量池等。 1.7的永久代和1.8的元空间都是方法区的一种实现。
如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收

java7-jvm-内存模型.png

java8-jvm-内存模型.png

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虛拟机同样会抛出内存溢出错误: java. lang . outofMemoryError:PermGen space 或者java.lang.OutOfMemoryError: Metaspace

方法区OOM的场景:

  • 加载大量的第三方的jar包
  • Tomcat部署的工程过多(30-50个)
  • 大量动态的生成反射类

方法区在JVM启动的时候被创建,关闭JVM就会释放这个区域的内存

元空间的特点及作用

1693559221078.png 6、JVM 内存可见性

JMM是定义程序中变量的访问规则,线程对于变量的操作只能在自己的工作内存中进行,而不能直接对主内存操作.
由于指令重排序,读写的顺序会被打乱,因此JMM需要提供原子性,可见性,有序性保证.

1693469869743.png

1693469909086.png

二、堆和栈的区别

  • 栈是运行时的单位,而堆是存储的单位
  • 栈解决程序的运行问题,即如何处理数据
  • 堆解决的是数据存储问题,即数据怎么放、放在哪儿

1、功能不同

栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象和数组。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。

2、共享性不同

栈内存是线程私有的。 堆内存是所有线程共有的。

3、异常错误不同

如果栈内存或者堆内存不足都会抛出异常。
栈空间不足java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。

4、空间大小

栈的空间大小远远小于堆的

三、虚拟机为什么使用元空间替换了永久代?

栈、堆、方法区的交互关系

在这里插入图片描述

HotSpot中方法区的演进

  • 在Jdk7及以前,习惯上把方法区称为永久代,jdk8开始,使用元空间取代了永久代
  • 《Java虚拟机规范》对如何实现方法区,不做统一的要求;可以把方法区看成所谓的接口,把永久代和元空间看成接口的不同的实现
  • 元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存

方法区在JDK6、JDK7、JDK8中的演进细节

版本HotSpot中方法区的变化
jdk1.6及以前有永久代(permanent generation),静态变量存放在永久代上
jdk1.7有永久代,但已经逐步“去永久代”,字符串常量池、静态变量被移除,保存在堆中
jdk1.8及以后无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中

在这里插入图片描述

image.png

在这里插入图片描述

永久代为什么要被元空间替换?

  • 在1.7版本里面,永久代内存是有上限的,虽然我们可以通过参数来设置,但是JVM加载的class总数、大小是很难确定的。所以很容易出现OOM问题。
    但是元空间是存储在本地内存里面,内存上限比较大,可以很好的避免这个问题。

  • 永久代的对象是通过FullGC进行垃圾收集,也就是和老年代同时实现垃圾收集。
    替换成元空间以后,简化了Full GC。可以在不进行暂停的情况下并发地释放类数据,同时也提升了GC的性能

StringTable为什么要调整位置?

  • 开发中会有大量的字符串被创建,而永久代的回收效率很低,在full GC的时候才会触发,这就导致StringTable回收效率不高,进而导致永久代内存不足;放到堆里,能及时回收内存

四、对象一定分配在堆中吗?有没有了解逃逸分析技术?

「对象一定分配在堆中吗?」 不一定的,JVM通过「逃逸分析」,那些逃不出方法的对象会在栈上分配。

「什么是逃逸分析?」

逃逸分析(Escape Analysis),是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。

逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。

「逃逸分析的好处」

  • 栈上分配,可以降低垃圾收集器运行的频率。
  • 同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
  • 标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈 上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并 且GC频率也会减少。

五、什么时候会触发FullGC

Full GC(Full Garbage Collection)是指对整个堆内存进行垃圾回收,包括年轻代和老年代的清理。相对于年轻代的Minor GC,Full GC通常需要更长的时间,因为它涉及到整个堆的回收。Full GC的触发时机通常有以下几种情况:

  1. 手动触发:开发者可以通过代码调用System.gc()方法来建议虚拟机进行垃圾回收,尽管调用并不一定会立即触发Full GC,但可能会影响垃圾回收的执行。
  2. 老年代空间不足:当老年代空间不足以容纳新的对象时,会触发Full GC。这可能是因为新的大对象无法在老年代分配成功,或者是在进行Minor GC后老年代中的存活对象太多,无法腾出足够的空间。
  3. 方法区空间不足:在旧的Java版本中,永久代(Permanent Generation)用于存储类的元数据和方法信息。在较新的Java版本中,这部分被替代为元空间(Metaspace)。如果永久代/元空间的空间不足,可能触发Full GC。
  4. 调用CMS收集器时发生Concurrent Mode Failure:CMS(Concurrent Mark-Sweep)收集器是一种老年代收集器,它在收集过程中尽量减少用户线程的
  5. 停顿时间。但在并发标记阶段,如果存活对象增长速度太快,可能会导致老年代空间不足,从而触发Full GC。
  6. 分配担保失败:在进行Minor GC时,虚拟机会根据历史数据判断老年代是否能够容纳新生代的存活对象。如果无法容纳,就会进行Full GC。

什么是老年代分配担保机制

1693558257369.png

1693558451951.png

六、JVM如何判断一个对象可以被回收

在JVM里面,要判断一个对象是否可以被回收,最重要的是判断这个对象是否还在被使用,只有没被使用的对象才能回收。

  1. 引用计数器,也就是为每一个对象添加一个引用计数器,用来统计指向当前对象的引用次数,如果当前对象存在应用的更新,那么就对这个引用计数器进行增加,一旦这个引用计数器变成0,就意味着它可以被回收了。

这种方法需要额外的空间来存储引用计数器,但是它的实现很简单,而且效率也比较高。

不过主流的JVM都没有采用这种方式,因为引用计数器在处理一些复杂的循环引用或者相互依赖的情况时,可能会出现一些不再使用但是又无法回收的内存,造成内存泄露的问题。 image.png

  1. 可达性分析,它的主要思想是,首先确定一系列肯定不能回收的对象作为GC root,比如虚拟机栈里面的引用对象、本地方法栈引用的对象等,然后以GC ROOT作为起始节点,从这些节点开始向下搜索,去寻找它的直接和间接引用的对象,当遍历完之后如果发现有一些对象不可到达,那么就认为这些对象已经没有用了,需要被回收。

在垃圾回收的时候,JVM会首先找到所有的GC root,这个过程会暂停所有用户线程,也就是stop the world,然后再从GC Roots这些根节点向下搜索,可达的对象保留,不可达的就会回收掉。

可达性分析是目前主流JVM使用的算法。 image.png

七、JVM分代年龄为什么是15次?

首先,在JVM的heap内存里面,分为Eden Space、Survivor Space、Old Generation。

当我们在Java里面使用new关键字创建一个对象的时候,JVM会在Eden Space分配一块内存空间来存储这个对象。

当Eden Space的内存空间不足的时候,会触发Young GC进行对象回收。

那些因为存在引用关系而无法回收的对象,JVM会把它们转移到Survivor Space。

image.png

Survivor Space内部又分为From区和To区,刚从Eden区转移过来的对象会分配到From区,每经历一次Young GC,这些没有办法被回收的对象就会在From区和To区来回移动,每移动一次,这个对象的GC年龄就加1。默认情况下GC年龄达到15的时候,JVM就会把这个对象移动到Old Generation。

image.png

其次呢,一个对象的GC年龄,是存储在对象头里面的,一个Java对象在JVM内存中的布局由三个部分组成,分别是对象头、实例数据、对齐填充。而对象头里面有4个bit位来存储GC年龄。

image.png

而4个bit位能够存储的最大数值是15,所以从这个角度来说,JVM分代年龄之所以设置成15次是因为它最大能够存储的数值就是15。

虽然JVM提供了参数来设置分代年龄的大小,但是这个大小不能超过15。

而从设计角度来看,当一个对象触发了最大值15次gc,还没有办法被回收,就只能移动到old generation了。

另外,设计者还引入了动态对象年龄判断的方式来决定把对象转移到old generation,也就是说

不管这个对象的gc年龄是否达到了15次,只要满足动态年龄判断的依据,也同样会转移到old generation。以上就是我对这个问题的理解。

jvm 动态年龄判断是怎么回事

1693822777829.png

1693558116115.png

八、什么情况下对象直接进入老年代

  1. 超过15次
  2. 动态年龄判断
  3. 老年代空间担保机制
  4. 大对象直接进入老年代

九、你知道哪些JVM调优参数?

-Xms 设置初始堆的大小
-Xmx 设置最大堆的大小
-Xmn 设置堆内存中年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值,扣除年轻代就是老年代的大小
-XX:MetaspaceSize 元空间大小 -Xss 每个线程的栈内存大小
-XX:SurvivorRatio Eden区与Survivor区的的比值,默认是8:1:1
-XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代
-XX:+UseConcMarkSweepGC:设置年老代为CMS垃圾收集器 -XX:+UseG1GC 指定使用G1垃圾回收器 -XX:+PrintCommandLineFlags -version 打印JVM默认初始堆和最大堆大小及默认的垃圾收集器 -XX:+PrintFlagsFinal -version 打印JVM所有的默认参数值

十、Eden区和Survivor区的空间大小比值为什么默认是8:1:1

1693561155909.png

十一、JVM 垃圾回收算法

  1. 标记-清除算法(Mark and Sweep):

    • 这是最基本的垃圾回收算法之一。它分为两个阶段:标记阶段,其中标记出所有存活的对象,和清除阶段,其中清除不再存活的对象。它有一个明显的问题,即会产生内存碎片。
  2. 复制算法(Copying):

    • 这个算法将内存分为两个区域,通常称为"年轻代"和"老年代"。在年轻代中,对象被分为两个等大小的区域,当一个区域填满时,存活的对象被复制到另一个区域,然后清除。这个算法适用于应用程序中对象的寿命较短的情况(年轻代),因为大多数对象都在年轻代被清除,从而减少了老年代的垃圾回收频率。
  3. 标记-整理算法(Mark and Compact):
    根据老年代的特点而产生的

    • 这个算法结合了标记阶段和整理阶段,它基于可达性分析算法标记出存活的对象,并将它们移动到内存的一端,然后清理掉其余的对象,从而消除了内存碎片。
  4. 分代算法(Generational):

    • 这是一种常见的垃圾回收策略,结合了复制算法和标记-整理算法。它将内存分为年轻代和老年代,JVM根据各个年代的特点采用不同的垃圾回收策略。年轻代使用复制算法(只有少量对象存活),老年代使用标记-清理、标记-整理算法。
  5. 并发标记清除算法(Concurrent Mark and Sweep):

    • 这是一种针对减少停顿时间的算法,它允许垃圾回收与应用程序线程并发执行。它通常与老年代一起使用,以减少长时间停顿。
  6. G1(Garbage-First)算法:

    • G1是一种新一代的垃圾回收器,旨在提供更可控的停顿时间。它将内存分为多个区域,并使用复制算法来管理年轻代,使用标记-整理算法来管理老年代。它会根据应用程序的需求,优先回收垃圾最多的区域。
  7. ZGC 和 Shenandoah 算法:

    • 这些是最新的垃圾回收器,旨在提供极低的停顿时间,并且适用于大内存应用程序。它们使用了一些复杂的技术,如并发标记、压缩、内存重分配等,以实现快速而可预测的垃圾回收。

十二、请介绍JVM垃圾回收器

1. Serial 收集器

在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial old GC。

2. Parallel 和 ParallelOld 收集器

Parallel

与ParaNew 收集器类似,Parallel收集器中Ergonomics负责自动的调节gc暂停时间和吞吐量之间的平衡,自动优化虚拟机的性能。自适应调节策略是Parallel收集器区别于ParaNew收集器的一个重要特征。

使用-XX:+UseParallelGC 指定使用 Parallel 垃圾收集器

使用-XX:+MaxGCPauseMills 该参数设置大于0的毫秒数,每次GC的时间将尽量不超过设置的值,但是这个值也不是越小越好,GC暂停时间越短,那么GC的次数会变得更频繁。

-XX:UseAdaptieSizePolicy自适应新生代大小策略,默认这个参数是开启的,这个参数开启之后,就不需要人工指定新生代大小,Eden与Survivor区的比例、晋升老年代对象大小等细节参数了

ParallelOld

使用 -XX:+UseParallelOldGC,指定使用 ParallelOld 垃圾收集器
Parallel 和 ParallelOld,任意指定一个,另一个默认绑定

  • 在Java 8中,默认采用Parallel + Parallel Old
  • JDK9默认垃圾回收器:G1,取代 Parallel + Parallel Old,成为服务端的默认回收器
  • JDK14默认的垃圾回收器:G1

3. CMS

  • -XX:+UseConcMarkSweepGC 指定使用 CMS 垃圾收集器,老年代使用CMS收集器,新生代默认使用ParaNew收集器

  • 在G1出现之前,CMS使用还是非常广泛的,直到今天,仍然有很多系统使用CMS GC

  • CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段并发标记阶段重新标记阶段并发清除阶段

阶段说明
初始(Initial-Mark)阶段这个阶段中,程序中所有的工作线程都将会因为"Stop-the-world"机制而出现短暂的暂停,这个阶段的 主要任务仅仅只是标记出GC Roots能直接关联到的对象,一旦标记完成之后就会恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里的速度非常快
并发标记(Concurrent-Mark)阶段从GC Roots的直接关联对象开始遍历整个对象图,这个过程耗时较长但不需要停顿用户线程,可以与垃圾收集线程一起并发运行
重新标记(remark)阶段需要"Stop-the-world" 是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
并发清除(concurrent sweep)阶段清理掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
  • 缺点
  1. 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC
  2. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低
  3. CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现"Concurrent Mode Failure"失败而导致另一次完全"Stop-the-world"的Full GC的产生。在并发标记和并发清理阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行处理,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间,这一部分垃圾就被称为浮动垃圾

4. G1

简介

  • 官方给G1设定的目标是:在延迟可控的情况下获得尽可能高的吞吐量,开创了收集器面向局部收集的设计思路和基于Region的内存布局形式

    • G1 (Garbage- First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征
  • 在JDK1.7版本正式启用,移除了Experimental的标识,是JDK 9以后的默认垃圾回收器,取代了CMS回收器以及Parallel + Parallel Old组合。被Oracle官方称为“全功能的垃圾收集器”

    • CMS已经在JDK 9中被标记为废弃(deprecated)。在jdk8中还不是默认的垃圾回收器,需要使用-XX: +UseG1GC来启用
  • 复制算法+标记-压缩算法、并发+并行回收和"Stop-the-world"机制

为什么叫做Garbage First(G1)?

  • G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region) (物理上不连续的)。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等
  • G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾收集的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
  • 由于这种方式的侧重点在于回收垃圾最大量的区间(Region)  ,所以给G1一个名字:垃圾优先(Garbage First)

回收特点

  • G1将内存划分为一个个的region,内存的回收是以region作为基本单位的,G1从整体来看是基于"标记-整理"算法实现的收集器,但从局部(两个Region之间)上看又是基于"标记-复制"算法实现,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显

  • 可预测的停顿时间模型(Pause Prediction Model) (即:软实时soft real-time)

    • 这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不得超过N毫秒

      • 由于分区的原因,G1可以只选取部分区域进行内存回收这样缩小了回收的范围,有计划的避免在整个Java堆中进行全区域的垃圾收集,因此对于全局停顿情况的发生也能得到较好的控制
  • G1跟踪各个Region里面的垃圾收集的价值大小(价值即回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据用户设定允许的收集时间,优先回收价值最大的Region

    • 这种使用Region划分内存空间以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率
    • 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多

Region的使用介绍

  • 使用G1收集器时,最多有2048个region,每个region=1~32M,且为2的N次幂,即1MB, 2MB, 4MB, 8MB, 16MB, 32MB。可以通过-XX: G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变
  • 默认新生代初始占堆内存5%,可以通过-XX:G1NewSizePercent设置,一般默认值即可,系统运行中,JVM会不停的增加更多的Region,新生代最大占堆内存60%,可以通过-XX:G1MaxNewSizePercent设置,一旦进行了垃圾回收,region的数量就会减少,这些都是动态的。
  • 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region (不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续 1693811110390.png
  • G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如果超过1.5个region,就放到H,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待

设置H的原因

对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待

G1的老年代垃圾回收

与CMS相似
老年代回收分为:初始标记、并发标记、最终标记、混合回收

image.png 除了并发标记不需要stw,其他三个阶段都需要stw

G1的混合垃圾回收

1693814276610.png

G1回收失败时的FullGC

1693814920982.png

G1应用场景

1693816050518.png

十三、7种经典垃圾回收器总结

截止JDK1.8,一共有7款不同的垃圾收集器,每一款不同的垃圾收集器都有不同的特点

垃圾收集器分类作用位置使用算法特点适用场景
Serial串行运行新生代复制算法响应速度优先适用于单CPU环境下的client模式
ParNew并行运行新生代复制算法响应速度优先多CPU环境Server模式下与CMS配合使用
Parallel并行运行新生代复制算法吞吐量优先适用于后台运算而不需要太多交互的场景
Serial Old串行运行老年代标记-压缩算法响应速度优先适用于单CPU环境下的client模式 或作为CMS出现"Concurrent Mode Failure"失败的后备预案
Parallel Old并行运行老年代标记-压缩算法吞吐量优先适用于后台运算而不需要太多交互的场景
CMS并发运行老年代标记-清除算法响应速度优先适用于互联网或B/S业务(Browser/Server(浏览器/服务器))
G1并发、并行新生代、老年代复制算法、 标记-压缩算法响应速度优先面向服务端应用

GC发展阶段:

  • Serial => Parallel (并行) => CMS(并发) => G1 => ZGC

垃圾回收器组合

在这里插入图片描述

  • 两个收集器间有连线,表明它们可以搭配使用:

    • Serial + Serial Old
    • ParNew + Serial Old
    • Serial + CMS
    • ParNew + CMS
    • Parallel Scavenge + Serial Old
    • Parallel Scavenge + Parallel Old
    • G1
  • Serial Old 作为CMS出现"Current Mode Failure"失败的后备预案

  • 红色虚线:

    • 由于维护和兼容性测试的成本,在JDK8时将Serial + CMS、ParNew + Serial Old这两个组合声明为Deprecated,并在JDK9中完全取消了这些组合的支持,即:移除
  • 绿色虚线:

    • JDK14中:弃用Parallel Scavenge + Serial Old组合
  • JDK14中,删除CMS垃圾回收器

十四、内存溢出(OOM)与内存泄露(Memory Leak)

内存溢出(OOM)

javadoc中对OutofMemoryError的解释是:

  • 没有空闲内存,而且垃圾收集器也无法提供更多内存

可能的原因:

  • Java虚拟机的堆内存设置不够(可以通过参数-Xms、-Xmx调整)
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

内存泄露(Memory Leak)

内存泄露(Memory Leak):

  • 对象不会再被程序用到了,但是GC又不能回收他们,长时间下去也会OOM

可能的原因:

  • 单例模式:单例的生命周期和应用程序一样长,单例程序中如果持有对外部对象的引用,那么这个外部对象是不能被回收的,则会导致内存泄露的产生
  • 一些提供close的资源未关闭导致内存泄露:数据库连接、网络连接和io连接必须手动close,否则不能被回收,必须在finally中close,否则不能被回收

对比

  • 内存溢出代码本身没有原则问题,最多算是代码品质不高,想要消除报错只能增加堆内存或优化代码
  • 内存泄露是由于代码存在缺陷,如果不修改代码问题,即使分配再大的内存最终也会报错

如何解决OOM

  1. 要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(MemoryLeak)还是内存溢出(Memory Overflow)
  2. 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置
  3. 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx-Xms) ,与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗

十五、线上常见问题

1. 线上环境的JVM都设置多大

线上:4C8G JVM:栈、堆、元空间

  1. 栈:1M默认(xss512k),一个线程是1M,线上tomcat可能有300个线程,300M
  2. 堆:大概把机器的一半内存给堆,4G 新生代:老年代=1:2(CMS)/6:4(G1)
  3. 元空间:一般512M就够了 此时JVM参数如下: -Xms4G -Xmx4G -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -XX:UseConcMarkSweepGC

日志打印参数: -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:d:/gc.log

堆转存文件生成: -XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpPath=d:/heap.hprof

2. 线上Java服务器内存飙升怎么回事

在负载均衡线移除该服务器 jmap -histo pid jmap -heap pid jmap -dump:format=b,file=heap.hprof pid

3. 线上cpu彪到100%

循环或线程阻塞等待 top top -H -p pid printf '%x' tid :输出pid下的线程16进制,如:8ef jstack pid > a.txt

4. java堆溢出后,其他线程是否还以继续运行

可以借助VisualVM工具观察 分情况

  • 局部变量:可以
  • 全局变量:不可以,比如bean的controller中有一个全局变量

5. 线上java项目OOM了,怎么回事

sql注入,插叙1000W数据导致堆溢出

十六、JVM调优

1. 系统预估

1693820744874.png

2. 流量洪峰场景

天猫双11每秒洪峰58.3w/s

1693820952986.png

3. 内存分配

1693821409557.png

4. 内存占用分配

1693821510778.png

5. 如何调优

1693821656595.png

1693821741040.png

1693821811513.png (4)指定垃圾回收器

1693822047167.png

1693822179396.png

1693822273117.png

十七、JVM一个类的加载过程

什么是类加载器: 就是将xxx.java文件编译成的xxx.class文件加载到jvm虚拟机内存中。
一个类在什么时候开始被加载,《Java虚拟机规范》中并没有进行强制约束,交给了虚拟机自己去自由实现,HotSpot虚拟机是按需加载,在需要用到该类的时候加载这个类。

一个类从加载到jvm内存,到从jvm内存卸载,它的整个生命周期会经历7个阶段: image.png

1.加载: classpath、jar包、网络、某个磁盘位置下的类的class二进制字节流读进来,在内存中生成一个代表这个类的java.lang.Class对象放入元空间,此阶段我们程序员可以干预,我们可以自定义类加载器来实现类的加载;

2.链接

  • 2.1验证: 验证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证虚拟机的安全; 比如检查字节码文件开头4个字节是否是0xCAFE BABE,版本检查,长度检查等。
  • 2.2准备: 虚拟机就会为这个类分配相应的内存空间,并为类变量赋初始值,int为0,long为0L,boolean为false,引用类型为null,常量赋正式值。
    // 常量:准备时赋值为123
    public static final int a = 123
    
    // 类变量:准备时赋初始值0
    public stati int b = 222
    
    //实例变量:创建实例时赋初始值
    public int abc;
    
  • 2.3解析: 把符号引用翻译为直接引用

3. 初始化 当我们new一个类的对象,访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,用反射API对一个类进行调用,初始化当前类,其父类也会被初始化...... 那么这些都会触发类的初始化;

4.使用: 使用这个类;比如new实例化这个类,调用该类的某个方法。

5.卸载: JVM很少卸载类
1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;
2.加载该类的ClassLoader已经被GC;
3.该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法;

十八、一个类被初始化的过程

类的初始化阶段,Java虚拟机才真正开始执行类中编写的Java代码
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,才真正初始化类变量和其他资源

1693884560883.png

1693884618279.png

十九、继承时父子类的初始化顺序

父类

  • 1.静态变量
  • 2.静态初始化块

子类

  • 1.静态变量
  • 2.静态初始化块

父类

  • 1.变量
  • 2.初始化块
  • 3.构造器

子类

  • 1.变量
  • 2.初始化块
  • 3.构造器

二十、JVM中不同的类加载器加载哪些文件

  1. 启动类加载器(Bootstrap ClassLoader):根类加载器,C++语言实现
    <JAVA_HOME>\jre\lib\rt.jar resources.jar charsets.jar
    被 -Xbootclasspath参数所指定的路径中存放的类库
  2. 扩展类加载器(Extension ClassLoader) sun.misc.Launcher$ExtClassLoader ExtClassLoader是Launcher类中的静态内部类
    <JAVA_HOME>\jre\ext
    被 java.ext.dirs 系统变量所指定的路径中的所有类库
  3. 应用程序类加载器(Application ClassLoader):系统的类加载器
    sun.misc.Launcher$AppClassLoader
    加载用户类路径(ClassPath)上的所有类库:该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。

二一、JVM类加载的双亲委派模型

1693891879935.png 层次结构:
在双亲委派模型中,类加载器形成了一个层次结构,其中顶层是启动类加载器(Bootstrap Class Loader),然后是扩展类加载器(Extension Class Loader),最后是应用程序类加载器(Application Class Loader)。每个类加载器都有自己的责任范围。
委派机制:
当一个类加载器收到加载类的请求时,它首先检查自己是否已经加载了这个类。如果加载了,则直接返回该类的引用。如果没有加载,它会将请求委派给其父类加载器。这个过程会一直持续,直到顶层的启动类加载器。只有当上一层加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,下一层类加载器才会尝试自己去加载

二二、JVM为什么要设计双亲委派模型,有什么好处

  1. 安全性和隔离
    因为核心类由启动类加载器加载,用户自定义的类由应用程序类加载器加载,这样可以避免用户代码意外覆盖核心类,提高了安全性和可靠性。

  2. 避免重复加载
    如果一个类已经被某个类加载器加载,其他类加载器会通过委派机制得知这个类已经存在,不会重复加载,从而减少了内存浪费。

  3. 保证类的唯一性

    1693892527881.png

    1693892451691.png

二三、可以打破JVM双亲委派模型吗?如何打破JVM双亲委派模型

可以;
想要打破这种模型,需要自定义一个加载器,重写其中的loadClass方法,使其不进行双亲委派即可;

二四、如何自定义自己的类加载器

  1. 继承 classLoader
  2. 覆盖 findClass(String name)方法 或者 loadClass(String name) 方法
    findClass(String name) 方法不会打破双亲委派
    loadClass(String name) 方法可以打破双亲委派

二五、classLoader类中的loadClss() findclass() defineclass() 有什么区别

loadClass() 就是主要进行类加载的方法,默认的双亲委派机器就实现在这个方法中
findClass() 根据名称或者位置加载.class 字节码
defineClass() 把字节码转化为 java.lang.Class

  1. 如果我们想定义一个类加载器,但是不想打破双亲委派模型的时候,可以重写loadClass()方法
  2. 如果我们想定义一个类加载器,但是不想打破双亲委派模型的时候,可以重写findClass()方法,findClass方法是JDK1.2之后的ClassLoader新添加的一个方法,这个方法只抛出了一个异常,没有默认实现。
  3. 可以打破,但是不建议打破

二六、加载一个类采用class.forName 和 classLoader有什么区别

类加载将类的信息加入到元空间(方法区) Class.forName 得到的 class 是已经初始化完成的 ClassLoader.loadClass 得到的 class 是还没有初始化的

二七、Tomcat 类加载机制

image.png

1693900343750.png

从tomcat6以后,就没有server.loader和shared.loader了
几个项目,就有几个 WebAppClassLoader 类加载器
几个jsp文件,就有几个 JasperLoader 类加载器

二八、Tomcat 是如何打破双亲委派机制的呢

WebAppClassLoader类中重写了 loadClass 方法,打破了双亲委派,它本身没有重写,基础类进行了重写。如果收到类加载请求,首先会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是优先加载web应用自己定义的类。

二九、tomcat为什么要破坏双亲委派模型

Tomcat 是 web 容器,那么一个 web 容器可能需要部署多个应用程序

  1. 部署在同一个 Tomcat 上的两个 Web 应用所使用的 Java类库的不同版本要相互隔离
  2. 部署在同一个 Tomcat 上的两个 Web 应用所使用的 Java类库的相同版本应该是共享的,否则就会出现大量相同的类加载到虚拟机中,浪费内存空间
  3. Tomcat本身也有依赖的类库,与应用程序依赖的类库可能会混淆,基于安全考虑,应该将两者进行隔离
  4. 需要支持 JSP 页面的热部署和热加载

三十、热部署和热加载

  • 热加载是在运行时重新加载class,后台会启动一个线程不断检测class是否发生改变
  • 热部署是在运行时重新部署整个项目,耗时相对较高

如何实现热加载

在程序代码更改且重新编译后,让运行的进程可以实时获取到新编译后的class文件,然后重新进行加载;

  1. 实现自己的类加载器
  2. 从自己的类加载器中加载要热加载的类
  3. 不断轮询要热加载的类class文件是否有更新,如果有更新,重新加载

三一、Java 代码是如何运行起来的

1693904058366.png