Skip to content

Latest commit

 

History

History
1190 lines (477 loc) · 37 KB

面试篇.md

File metadata and controls

1190 lines (477 loc) · 37 KB

什么是JVM?

1、定义:指的是Java虚拟机 (Java Virtual Machine )。JVM 本质上是一个运行在计算机上的程序,他的职责是运行Java字节码文件,Java虚拟机上可以运行Java、Kotlin、Scala、Groovy等语言

2、作用: 为了支持lava中Write Once,Run Anywhere: 编写一次,到处运行的跨平台特性

image-20240421153513947

image-20240421153612453

3、JVM功能

image-20240421153745274

4、JVM组成

image-20240421154039671

5、常见JVM

image-20240421154148391

总结

1、JVM 指的是Java虚拟机,本质上是一个运行在计算机上的程序,他的职责是运行Java字节码文件,作用是为了支持跨平台特性。

2、JVM的功能有三项:

  • 第一是解释执行字节码指令;
  • 第二是管理内存中对象的分配,完成自动的垃圾回收;
  • 第三是优化热点代码提升执行效率

3、JVM组成分为类加载子系统、运行时数据区、执行引擎、本地方接口这四部分。

4、常用的JVM是Oracle提供的Hotspot虚拟机,也可以选择GraalVM、龙井OpenJ9等虚拟机。

了解过字节码文件的组成吗?

image-20240421154332962

image-20240421154611530

说一下运行时数据区

image-20240421215319241

运行时数据区指的是JVM所管理的内存区域,其中分成两大类:

线程共享 - 方法区、堆

线程不共享 - 本地方法栈、虚拟机栈、程序计数器

直接内存主要是NIO使用,由操作系统直接管理,不属于JVM内存。

image-20240421215403030

程序计数器

程序计数器(Program Counter Register) 也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的的字节码指令的地址。主要有两个作用: 1、程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。

image-20240421215440686

2、在多线程执行情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到那一句指令并继续解释运行

image-20240421215518644

栈 - Java虚拟机栈

Java虚拟机栈采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈来保存。每个线程都会包含一个自己的虚拟机栈,它的生命周期和线程相同。

image-20240421215606911

栈帧

1、局部变量表,在方法执行过程中存放所有的局部变量。

image-20240421215733369

2、操作数栈,虚拟机在执行指令过程中用来存放临时数据的一块区域

image-20240421220058194

3、帧数据,主要包含动态链接、方法出口、异常表等内容

动态链接: 方法中要用到其他类的属性和方法,这些内容在字节码文件中是以编号保存的,运行过程中需要替换成内存中的地址,这个编号到内存地址的映射关系就保存在动态链接中。

方法出口: 方法调用完需要弹出栈,回到上一个方法,程序计数器要切换到上一个方法的地址继续执行,方法出口保存的就是这个地址

异常表: 存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置

本地方法栈

  • Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧
  • 在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来

image-20240421220323457

  • 一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上
  • 栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享
  • 堆是垃圾回收最主要的部分,堆结构更详细的划分与垃圾回收器有关。

image-20240421220411127

方法区

方法区是java虚拟机规范中提出来的一个虚拟机概念,在Hotspot不同版本中会用永久代或者元空间来实现。

方法区主要存放的是基础信息,包含:

1、每一个加载的类的元信息 (基础信息)

image-20240421220527702

2、运行时常量池,保存了字节码文件中的常量池内容,避免常量内容重复创建减少内存开销

3、字符串常量池,存储字符串的常量

直接内存

直接内存并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。

在JDK 1.4 中引入了 NIO机制,由操作系统直接管理这部分内容,主要为了提升读写数据的性能。在网络编程框架如Netty中被大量使用。要创建直接内存上的数据,可以使用ByteBuffer。

语法: ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);

总结

运行时数据区指的是JVM所管理的内存区域,其中分成两大类

线程共享- 方法区、堆

  • 方法区: 存放每一个加载的类的元信息、运行时常量池、字符串常量池。

  • 堆: 存放创建出来的对象

线程不共享 -本地方法栈、虚拟机栈、程序计数器

本地方法栈和虚拟机栈都存放了线程中执行方法时需要使用的基础数据

程序计数器存放了当前线程执行的字节码指令在内存中的地址

直接内存主要是NIO使用,由操作系统直接管理,不属于JVM内存

那些区域会出现内存溢出?

内存溢出指的是内存中某一块区域的使用量超过了允许使用的最大值,从而使用内存时因空间不足而失败,虚拟机一般会抛出指定的错误。

在Java虚拟机中,只有程序计数器不会出现内存溢出的情况,因为每个线程的程序计数器只保存一个固定长度的地址

image-20240421220936716

堆内存溢出

堆内存溢出指的是在堆上分配的对象空间超过了堆的最大大小,从而导致的内存溢出。堆的最大大小使用-Xmx参数进行设置,如-Xmx10m代表最大堆内存大小为10m。

溢出之后会抛出OutofMemoryError,并提示是]ava heap Space导致的

image-20240421221050101

栈内存溢出

栈内存溢出指的是所有栈帧空间的占用内存超过了最大值

最大值使用-Xss进行设置,比如-Xss256k代表所有栈帧占用内存大小加起来不能超过256k

溢出之后会抛出StackOverflowError:

image-20240421221121387

方法区溢出

方法区内存溢出指的是方法区中存放的内容比如类的元信息超过了方法区内存的最大值,

JDK7及之前版本方法区使用永久代(-XX:MaxPermsize=值) 来实现

JDK8及之后使用元空间(-XX:MaxMetaspaceSize=值) 来实现

image-20240421221338481

直接内存溢出

直接内存溢出指的是申请的直接内存空间大小超过了最大值,使用-XX:MaxDirectMemorySize=值 设置最大值. 溢出之后会抛出OutofMemoryError:

image-20240421221611183

内存溢出指的是内存中某一块区域的使用量超过了允许使用的最大值,从而使用内存时因空间不足而失败,虚拟机一般会抛出指定的错误

堆: 溢出之后会抛出OutOfMemoryError,并提示是Java heap Space导致的。

栈: 溢出之后会抛出StackOverflowError。

方法区: 溢出之后会抛出OutOfMemoryError,JDK7及之前提示永久代,JDK8及之后提示元空间。

直接内存: 溢出之后会抛出OutOfMemoryError。

JVM在JDK6 - 8 在内存区域有什么不同?

1、方法区的实现

2、字符串常量池的位置

方法区位置的实现

方法区是《Java虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同。Hotspot设计如下:

  • JDK7及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。

  • JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。也可以手动设置最大大小。

image-20240421221845824

使用元空间替换永久代的原因

1、提高内存上限: 元空间使用的是操作系统内存,而不是JVM内存。如果不设置上限,只要不超过操作系统内存上限,就可以持续分配。而永久代在堆中,可使用的内存上限是有限的。所以使用元空间可以有效减少OOM情况的出现。

2、优化垃圾回收的策略: 永久代在堆上,垃圾回收机制一般使用老年代的垃圾回收方式,不够灵活。使用元空间之后单独设计了一套适合方法区的垃圾回收机制。

字符串常量池位置

早期设计时,字符串常量池是属于运行时常量池的一部分,他们存储的位置也是一致的。后续做出了调整将字符串常量池和运行时常量池做了拆分

image-20240421222157117

JDK7之前放在堆上,运行时常量池一直在方法区中。

字符串常量池从方法区移动到堆的原因

1、垃圾回收优化: 字符串常量池的回收逻辑和对象的回收逻辑类似,内存不足的情况下,如果字符串常量池中的常量不被使用就可以被回收;方法区中的类的元信息回收逻辑更复杂一些。移动到堆之后,就可以利用对象的垃圾回收器,对字符串常量池进行回收。

2、让方法区大小更可控: 一般在项目中,类的元 区中,会让方法区的空间大小变得不可控

3、intern方法的优化: JDK6版本中intern ()方法会把第一次遇到的字符串实例复制到久代的字符串常量池中。JDK7及之后版本中由于字符串常量池在堆上,就可以进行优化:字符串保存在堆上,把字符串的引用放入字符串常量池,减少了复制的操作。

总结

image-20240421222701129

image-20240421222746595

image-20240421222755455

类的生命周期

image-20240421223430839

image-20240421223750769

加载(Loading) 阶段

1、第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。

image-20240421223523783

2、类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到内存的方法区中。在方法区生成一个InstanceKlass对象,保存类的所有信息。

3、在堆中生成一份与方法区中数据类似的iava.lang.Class对象,作用是在Java代码中去获取类的信息

image-20240421223719154

链接阶段

验证

Java字节码文件是否遵守了《Java虚拟机规范》中的约束。

  • 1.文件格式验证,比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求
  • 2.元信息验证,例如类必须有父类 (super不能为空)
  • 3.验证程序执行指令的语义,比如方法内的指令执行到一半强行跳转到其他方法中去
  • 4.符号引用验证,例如是否访问了其他类中private的方法等

准备

准备阶段为静态变量(static)分配内存并设置初值。

final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值。

image-20240421224016255

解析

解析阶段主要是将常量池中的符号引用替换为直接引用。

符号引用就是在字节码文件中使用编号来访问常量池中的内容。直接引用不在使用编号,而是使用内存中地址进行访问具体的数据。

image-20240421224148278

初始化阶段

  • 初始化阶段会执行静态代码块中的代码,并为静态变量赋值
  • 初始化阶段会执行字节码文件中clinit部分的字节码指令。

image-20240421224219143

案例

public class Demo1 {
    
public static int value = 1;
    
    static {
        value = 2;

    }
    
    {
        //插入到初始化方法里面
        	value = 3;
    } 

	public static void main(String[] args) {
        new Demo1();
        System.out.println( value);
        //3 
        //注释new 结果为2
    }
    
}

1.连接的准备阶段value初值为0

2.初始化阶段执行clinit 方法中的指今value值为2

3.如果创建对象,会执行构造init 方法,value = 3,(类中代码块中的内容被放到了构造方法中)

卸载

判定一个类可以被卸载。需要同时满足下面三个条件 1、此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象

2、加载该类的类加载器已经被回收

3、该类对应的iava.lang.Class 对象没有在任何被引用

image-20240421224829800

总结

image-20240421224922594

什么是类的加载器

image-20240421225014112

类加载器负责在类的加载过程中将字节码信息以流的方式获取并加载到内存中。JDK8及之前如下:

image-20240421225056071

启动类加载器

  • 启动类加载器(Bootstrap ClassLoader) 是由Hotspot虚拟机提供的类加载器,JDK9之前使用C++编写的、JDK9之后使用Java编写。

  • 默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等。

扩展类加载器

  • 扩展类加载器(Extension Class Loader) 是JDK中提供的、使用Java编写的类加载器。
  • JDK9之后由于采用了模块化,改名为Platform平台类加载器
  • 默认加载Java安装目录/jre/lib/ext下的

应用程序类加载器

  • 应用程序类加载器(Application Class Loader)是JDK中提供的、使用Java编写的类加载器。默认加载为应用程序classpath下的类

  • 自定义类加载器允许用户自行实现类加载的逻辑,可以从网络、数据库等来源加载类信息。自定义类加载器需要继承自ClassLoader抽象类,重写findClass方法。

image-20240421230127108

总结

1.启动类加载器 (Bootstrap ClassLoader) 加载核心类

2.扩展类加载器 (Extension ClassLoader) 加载扩展类

3.应用程序类加载器 (Application ClassLoader) 加载应用classpath中的类

4.自定义类加载器,重写findClass方法

JDK9及之后扩展类加载器 (Extension ClassLoader) 变成了平台类加载器 (PlatformClassLoader)

什么是双亲委派机制

image-20240422151655266

类加载有层级关系,上一级称之为下一级的父类加载器。

image-20240422151720157

双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会向上查找是否加载过,再由顶向下进行加 载。

image-20240422151933342

每个类加载器都有一个父类加载器,在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器。

作用

1、保证类加载的安全性

通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java. Lang.string,确保核心类库的完整性和安全性。

2、避免重复加载

双亲委派机制可以避免同一个类被多次加载。

总结

双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会向上交给父类加载器查找是否加载过,再由顶向下进行加载。

双亲委派机制的作用:保证类加载的安全性,避免重复加载。

image-20240422151933342

如何打破双亲委派机制

image-20240407140509013

ClassLoader中包含了4个核心方法,对ave程序员来说,打破双亲委派机制的唯一方法就是实现自定义类加载器重写loadClass方法,将其中的双亲委派机制代码去掉。

image-20240422155059576

Tomcat自定义类加载器

Tomcat中,实现了一套自定义的类加载器。这一小节使用目前应用比校广泛的Tomcat9(9.0.84) 源码进行分析。

image-20240422160319947

common类加载器

主要加载tomcat自身使用以及应用使用的jar包,默认配置在catalina.properties文件中。 commonloader= ""${catalina.base}/lib" "${catalina.base}/lib/*.jar

catalina类加载器

catalina类加载器主要加载tomcat自身使用的jar包,不让应用使用,默认配置在catalina.properties文件中。 serverloader=空(默认),为空时catalina加载器和common加载器是同一个

shared类加载器

shared类加载器主要加载应用使用的jar包,不让tomcat使用,默认配置在catalina.properties文件中。 sharedloader=空(默认),为空时shared加载器和common加载器是同一个。

ParallelwebappclassLoader类加载器

可以多线程并行加载应用中使用到的类,每个应用都拥有一个自己的该类加载器。

为什么每个应用会拥有一个独立的ParallelwebappclassLoader类加载器呢? 同一个类加载器,只能加载一个同名的类。两个应用中相同名称的类都必须要加载。

image-20240422162912777

执行流程

image-20240422163044481

默认这里打破了双亲委派机制,应用中的类如果没有加载过。会先从当前类加载器加载,然后再交给父类加载器通过双亲委派机制加载。

jsp文件加载器

JasperLoader类加载器负责加载JSP文件编译出来的class字节码文件,为了实现热部署(不重启让修改的 jsp生效),每一个jsp文件都由一个独立的JasperLoader负责加载。

修改后,JasperLoader被替换。

总结

image-20240422163930207

如何判断堆上的对象没有被引用?

image-20240422164816155

如何判断堆上的对象没有被引用?

常见的有两种判断方法:引用计数法和可达性分析法。

引用计数法

引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。

引用计数法的优点是实现简单,缺点有两点:

1.每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响

2.存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题。

可达性分析法

Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GC Root) 和普通对象,对象与对象之间存在引用关系。

image-20240422165719959

GC ROOT对象

  • 线程Thread对象,引用线程栈帧中的方法参数、局部变量等。
  • 系统类加载器加载的java.lang.Class对象,引用类中的静态变量。
  • 监视器对象,用来保存同步锁synchronized关键字持有的对象。
  • 本地方法调用时使用的全局对象。

总结

引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1;

存在循环引用问题所以Java没有使用这种方法。

Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象。

可达性分析算法指的是如果从某个到GC Root对象是可达的,对象就不可被回收。

最常见的是GC Root对象会引用栈上的局部变量和静态变量导致对象不可回收。

JVM中都有那些引用类型?

image-20240422170110315

强引用: JVM中默认号引用关系,即是对象被局部变量、静态变量等GC Root关联的对象引用,只要这层关系存在,普通对象就不会被回收。

软引用:相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。软引用主要在缓存框架中使用

弱引用:的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收,弱引用主要在ThreadLocal中使用。

虚引用(幽灵引用/幻影引用):不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。

终结器引用:终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象finalize方法,在对象第二次被回收时,该对象才真正的被回收。

ThreadLocal中为什么要使用弱引用?

ThreadLocal可以在线程中存放线程的本地变量,保证数据的线程安全。

image-20240422171845472

ThreadLocal中是这样去保存对象的:

1、在每个线程中,存放了一个ThreadLocalMap对象,本质上就是一个数组实现的哈希表,里边存放多个Entry对象。

2、每个Entry对象继承自弱引用,内部存放ThreadLocal对象。同时用强引用,引用保存的ThreadLocal对应的value值。

image-20240422171936124

set方法:

image-20240422172201360

get方法

image-20240422172242764

不再使用Threadlocal对象时,threadlocal =null;由于是弱引用,那么在垃圾回收之后,ThreadLocal对象就可以被回收。

image-20240422173021761

此时还有Entry对象和value对象没有能被回收,所以在ThreadlLocal类的set.get、remove方法中,在某些特定条件满足的情况下,会主动删除这两个对象。

image-20240422173303851

如果一直不调用set、get、remove方法或者调用了没有满足条件,这部分对象就会出现内存泄漏。强烈建议在ThreadLocal不再使用时,调用remove方法回收将Entry对象的引用关系去掉,这样就可以回收这两个对象了。

总结

当threadlocal对象不再使用时,使用弱引用可以让对象被回收;因为仅有弱引用没有强引用的情况下,对象是可以被回收的。

弱引用并没有完全解决掉对象回收的问题,Entry对象和value值无法被回收,所以合理的做法是手动调用remove方法进行回收,然后再将threadlocal对象的强引用解除

有哪些场景的垃圾回收算法?

image-20240422174111879

  • 1960年John McCarthy发布了第一个GC算法:标记-清除算法。
  • 1963年Marvin L. Minsky 发布了复制算法。

本质上后续所有的垃圾回收算法,都是在上述两种算法的基础上优化而来。

标记清除算法

标记清除算法的核心思想分为两个阶段:

1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。

2.清除阶段,从内存中删除没有被标记也就是非存活对象。

image-20240422174350323

优缺点

优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。

缺点:

1、碎片化问题

由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一 个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。

2、分配速度慢。

由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才 能获得合适的内存空间。

复制算法

复制算法的核心思想是:

1.准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)

2.在垃圾回收GC阶段,将From中存活对象复制到To空间。

3.将两块空间的From和To名字互换。

image-20240422174710839

优缺点

image-20240422175125198

标记整理算法

也叫标记压缩算法,

是对标记清理算法中容易产生内存碎片问题的一种解决方案。

1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引1用链遍历出所有存活对象。

2.整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。

image-20240422175242354

优缺点

image-20240422175327944

分代GC

内存区域划分

  • 年轻代

    • Eden区

    • 幸存区 / survivor(s0)

    • S1

  • 老年代

image-20240410115501302

分代回收算法流程

  • 分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。
  • 随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。
  • Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区。
  • 接下来,S0会变成To区,S1变成From区。 当eden区满时再往里放入对象,依然会发生Minor GC。

注意:

  • 每次Minor GC 会为对象记录年龄,初值为0,GC完加1。达到条件后对象会晋升到老年代
  • 当老年代中空间不足,无法放入新的对象时,先尝试minor gc,如果不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。STW停顿时间较长
  • 如果Full GC依然无法回收掉老年代的对象,当对象继续放入老年代时,抛出Out Of Memory异常
  • eden和survivor区满了,即时不满足年龄也会放入到老年。

优缺点

1、可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。

2、新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法效率高、不会产生内存碎片,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高。

3、分代的设计中允许只回收新生代(minorgc),如果能满足对象分配的要求就不需要对整个堆进行回收(full gc),STw (Stop The World)由垃圾回收引起的停顿时间就会减少。

常见的垃圾回收器

image-20240422180600183

由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器心须成对组合进行使用。 具体的关系图如下:

image-20240410140451377

Serial + SerialOld

image-20240422180844620

Parallel Scavenge + Parallel Old

image-20240422180945029

年轻代-ParNew

image-20240422181106417

老年代-CMS(Concurrent Mark Sweep)

image-20240422181204032

缺点:

1、 CMS使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,CMS会在Full GC时进行碎片的整理这样会导致用户线程暂停,可以使用 -XX:CMSFuL1GCsBeforecompaction=N 参数(默认0)调整N次Full GC:后再整理。

2.、无法处理在并发清理过程中产生的“浮动垃圾〞,不能做到完全的垃圾回收。

3、如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代。并发阶段会影响用户线程执行的性能

G1

image-20240422181427916

Shenandoah

Shenandoah 是由Red Hat开发的一款低延迟的垃圾收集器,Shenandoah 并发执行大部分 GC 工作,包括并发的整理,堆大小对STW的时间基本没有影响。

image-20240422181915042

总结

image-20240422182051863

垃圾回收器的组合关系虽然很多,但是针对几个特定的版本,比较好的组合选 择如下: JDK8及之前: ParNew + CMS(关注暂停时间)、Parallel Scavenge + Parallel Old (关注吞吐量)、

G1 (JDK8之前不建议,较大堆并且关注暂停时间) JDK9之后: G1(默认)

从JDK9之后,由于G1日趋成熟,JDK默认的垃圾回收器已经修改为G1,所以强烈建议在生产环境上使用G1。

如果对低延迟有较高的要求,可以使用Shenandoah或者ZGC。

如何解决内存泄漏问题?

image-20240422214216563

内存泄漏(memory leak) : 在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。

少量的内存泄漏可以容忍,但是如果发生持续的内存泄漏,内存被消耗完,最终导致的结果就是内存溢出

解决内存泄漏步骤

image-20240422214637088

发现问题

堆内存状况对比

image-20240411143355684

生产环境通过运维提供的Prometheus + Grafana等监控平台查看

开发、测试环境通过visualvm查看

诊断 -生成内存快照

当堆内存溢出时,需要在堆内存溢出时将整个堆内存保存下来,生成内存快照(Heap Profile )文件 生成方式有两种 1、内存溢出时自动生成,添加生成内存快照的Java虚拟机参数:

  • XX:+HeapDumpOnOutOfMemoryError,发生OutOfMemoryError错误时,自动生成hprof内存快照文件

  • XX:HeapDumpPath=:指定hprof文件的输出路径

2、导出运行中系统的内存快照,比较简单的方式有两种,注意只需要导出标记为存活的对象: 通过JDK自带的imap命令导出,格式为:

  • jmap -dump:live,format=b,file=文件路径和文件名 进程ID

  • 通过arthas的heapdump命令导出,格式为: heapdump--live 文件路径和文件名

3、使用MAT打开hprof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏的根源。

直方图、支配树

修复问题

修复内存溢出问题的要具体问题具体分析,问题总共可以分成三类

image-20240422215836842

常用的JVM工具

JDK自带的命令行工具

  • jps 查看java进程,打印main方法所在类名和进程id
  • jmap 生成堆内存快照、打印类的直方图
  • jstack 导出堆栈信息

第三方工具

  • VisualVM 监控
  • Arthas 综合性工具
  • MAT 堆内存分析工具

监控工具: Prometheus + grafana

常见的JVM参数

image-20240422220111306

参数1:- Xmx 和 -Xms

-Xmx参数设置的是最大堆内存,但是由于程序是运行在服务器或者容器上,计算可用内存时,要将元空间、操作

系统、其它软件占用的内存排除掉。

案例:服务器内存4G,操作系统+元空间最大值+其它软件占用1.5G,-Xmx可以设置为2g。

最合理的设置方式应该是根据最大并发量估算服务器的配置,然后再根据服务器配置计算最大堆内存的值。

image-20240412142402569

参数2: -XX: MaxMetaspaceSize -XX:MetaspaceSize

MAX参数指的是最大元空间大小,默认值比较大,如果出现元空间内存泄漏会让操作系统可用内存不可控,建议根据测试情况设置最大值,一般设置为256m。

-XX:Metaspacesize=值 指:达到该值之后会触发FULLGC(网上很多文章的初始元空间大小是错误的),后续什么时候再触发JVM会自行计算。如果设置为和MaxMetaspacesize一样大,就不会FULLGC,但是对象也无法 回收。

参数3:-Xss虚拟机栈大小

  • 语法:-Xss栈大小或-XX:ThreadStackSzie=1024
  • 单位:字节(默认,必须是 1024 的倍数)、K或者K(KB)、m或者M(MB)、g或者G(GB)

参数4: -Xmn 年轻代的大小

默认值为整个堆的1/3,可以根据峰值流量计算最大的年轻代大小,尽量让对象只存放在年 轻代,不进入老年代。但是实际的场景中,接口的响应时间、创建对象的大小、程序内部还会有一些定时任务等不确定因素都会导致这个值的大小并不能仅凭计算得出,如果设置该值要进行大量的测试。

G1垃圾回收器尽量不要设置该值,G1会动态调整年轻代的大小。

其他参数

  • -XX:+DisableExplicitGC 禁止在代码中使用System.gc(),System.gc()可能会引起FULLGC,在代码中尽量不要使用。使用DisableExplicitGc参数可以禁止使用System.gc()方法调用。
  • -XX:+HeapDumpOnoutofMemoryError:发生OutofMemoryError错误时,自动生成hprof内存快照文件。
  • -XX:HeapDumpPath=:指定hprof文件的输出路径。
  • 打印GC日志
    • JDK8及之前:-XX:+PrintGCDetails -xX:+PrintGCDatestamps-XLoggc:文件路径
    • JDK9及之后:-xlog:gc*:file=文件路径

JVM参数模板

# 初始堆内存1g
-Xmslg
# 最大堆内存1g
-Xmxlg
# 每个线程的栈内存最大256k
-Xss256k
# 最大元空间大小512m
-XX: MaxMetaspaceSize=512m
# 代码中System.gc()无效
-XX:+DisableExplicitGC
# OutofMemory错误时生成堆内存快照
-XX:+HeapDumpOnoutofMemoryError 
# 堆内存快照生成位置
-XX:HeapDumpPath=/opt/Logs/my-service.hprof
#  打印详细垃圾回收日志
-XX:+PrintGCDetails
# 打印垃圾回收时间
-XX:+PrintGCDateStamps
# 日志文件输出位置
-Xloggc:文件路径