JVM

面试

1.JVM是什么?JVM的内存 区域分为哪些?

2.什么是OOM ?什么是StackoverflowError?有哪些方法分析?

3.JVM 的常用参数调优你知道哪些?

4.GC是什么?为什么需要GC?

5.什么是类加载器?

什么是JVM

是一种规范:规定了字节码文件的格式

JDK=JRE+工具集(java.exe,javac.exe)

JRE=JVM+系统类库

==屏蔽了平台和语言相关性==

JVM:Java Virtual Machine,Java虚拟机

**位置:**JVM是运行在操作 系统之上的,它与硬件没有直接的交互。

主流虚拟机

  • JCP组织(Java Community Process 开放的国际组织 ):Hotspot虚拟机(Open JDK版),sun2006年开源
  • Oracle:Hotspot虚拟机(Oracle JDK版),闭源,允许个人使用,商用收费
  • BEA:JRockit虚拟机
  • IBM:J9虚拟机
  • 阿里巴巴:Dragonwell JDK(龙井虚拟机),电商物流金融等领域,高性能要求。

JVM结构图

**JVM的作用:**加载并执行Java字节码文件(.class) - 加载字节码文件、分配内存(运行时数据区)、运行程序

**JVM的特点:**一次编译到处运行、自动内存管理、自动垃圾回收

  • 类加载器子系统:将字节码文件(.class)加载到内存中的方法区

  • 运行时数据区:(Runtime Data Area)

    • 方法区:存储已被虚拟机加载的类的元数据信息(元空间)。也就是存储字节码信息。
    • 堆:存放对象实例,几乎所有的对象实例都在这里分配内存。
    • 虚拟机栈(java栈):虚拟机栈描述的是Java方法执行的内存模型。每个方法被执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
    • 本地方法栈:本地方法栈则是记录虚拟机当前使用到的native方法
    • 程序计数器:当前线程所执行的字节码的行号指示器
  • 本地方法接口:虚拟机使用到的native类型的方法,负责调用操作系统类库。(例如Thread类中有很多Native方法的调用)

  • 执行引擎:包含解释器、即时编译器和垃圾收集器 ,负责执行加载到JVM中的字节码指令。

    1. 功能区: 类加载器子系统、垃圾回收器、字节码执行引擎
    2. 线程共享区:堆、方法区、直接内存、运行时常量池
    3. 线程私有区:栈、本地方法栈、程序计数器

注意:

  • 多线程共享方法区和堆;
  • Java栈、本地方法栈、程序计数器是每个线程私有的。

执行引擎Execution Engine

Execution Engine执行引擎负责解释命令(将字节码指令解释编译为机器码指令),提交操作系统执行。

JVM执行引擎通常由两个主要组成部分构成:解释器和即时编译器(Just-In-Time Compiler,JIT Compiler)。

  1. 解释器:当Java字节码被加载到内存中时,解释器逐条解析和执行字节码指令。解释器逐条执行字节码,将每条指令转换为对应平台上的本地机器指令。由于解释器逐条解析执行,因此执行速度相对较慢。但解释器具有优点,即可立即执行字节码,无需等待编译过程。
  2. 即时编译器(JIT Compiler):为了提高执行速度,JVM还使用即时编译器。即时编译器将字节码动态地编译为本地机器码,以便直接在底层硬件上执行。即时编译器根据运行时的性能数据和优化技术,对经常执行的热点代码进行优化,从而提高程序的性能。即时编译器可以将经过优化的代码缓存起来,以便下次再次执行时直接使用。

JVM执行引擎还包括其他一些重要的组件,如即时编译器后端、垃圾回收器、线程管理器等。这些组件共同协作,使得Java程序能够在不同的操作系统和硬件平台上运行,并且具备良好的性能。

本地方法接口

本地接口的作用是融合不同的编程语言为 Java 所用,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。

本地方法栈Native Method Stack

本地方法栈存储了从Java代码中调用本地方法时所需的信息。是线程私有的。

PC寄存器(程序计数器)

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,即 将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

类加载器

  • 负责加载class文件,class文件在文件开头有特定的文件标识(cafe babe)。
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
  • 加载的类信息存放到方法区的内存空间。

类加载的过程

类加载过程主要分为三个步骤:加载链接初始化,而其中链接过程又分为三个步骤:验证准备解析,加上卸载使用两个步骤统称为为类的生命周期

阶段一:加载

通过双亲委派模型加载类的字节码文件

  • 通过一个类的全限定名获取定义此类的二进制字节流
  • 将这个字节流代表的静态存储结构转为方法区运行时数据结构
  • 在内存中生成一个代码这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

结论:类加载为懒加载

阶段二:链接

  • 验证:验证阶段主要是为了为了确保Class文件的字节流中包含的信息符合虚拟机要求,并且不会危害虚拟机
  • 准备:
    • 为类的静态变量分配内存并 且设置该类变量的默认初始值,即赋初值
    • 实例变量是在创建对象的时候完成赋值,且实例变量随着对象一起分配到Java堆中
    • final修饰的常量在编译的时候会分配,准备阶段直接完成赋值,即没有赋初值这一步。被所有线程所有对象共享
  • 解析:将符号引用替换为直接引用
    • 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
    • 直接引用:可以直接指向目标的指针,而直接引用必须引用的目标已经在内存中存在

阶段三:初始化

​ 初始化阶段是执行类构造器 的过程。这一步主要的目的是:根据程序员程序编码制定的主观计划去初始化类变量和其他资源。

执行类构造器<cinit>()方法,给类静态变量赋值(比如:int i=5),并执行静态代码块,如果一个类中没有静态代码块,也没有静态变量的赋值操作,那编译器可以不为这个类生成<init>()方法

类加载器的作用

负责加载class文件,class文件在文件开头有的文件标识**(CA FE BA BE)**,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

类加载器的分类

分为四种,前三种为虚拟机自带的加载器。

  • 启动类加载器(BootstrapClassLoader):由C++实现。
  • 扩展类加载器(ExtClassLoader/PlatformClassLoader):由Java实现,派生自ClassLoader类。
  • 应用程序类加载器(AppClassLoader):也叫系统类加载器。由Java实现,派生自ClassLoader类。
  • 自定义加载器 :程序员可以定制类的加载方式,派生自ClassLoader类。

Java 9之前的ClassLoader

  • Bootstrap ClassLoader加载$JAVA_HOME中jre/lib/rt.jar,加载JDK中的核心类库
  • ExtClassLoader加载相对次要、但又通用的类,主要包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
  • AppClassLoader加载-cp指定的类,加载用户类路径中指定的jar包及目录中class

Java 9及之后的ClassLoader

  • Bootstrap ClassLoader,使用了模块化设计,加载lib/modules启动时的基础模块类,java.base、java.management、java.xml

  • ExtClassLoader更名为PlatformClassLoader,使用了模块化设计,加载lib/modules中平台相关模块,如java.scripting、java.compiler。

    平台类加载器加载的类示例:

    • java.util.logging.Logger
    • javax.xml.parsers.DocumentBuilderFactory
    • javax.sql.DataSource
  • AppClassLoader加载-cp,-mp指定的类,加载用户类路径中指定的jar包及目录中class

双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上

  • 1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器PlatformClassLoader去完成。
  • 2、当PlatformClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器BootStrapClassLoader去完成。
  • 3、如果BootStrapClassLoader加载失败,会用PlatformClassLoader来尝试加载;
  • 4、若PlatformClassLoader也加载失败,则会使用AppClassLoader来加载
  • 5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

其实这就是所谓的双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把**请求委托给父加载器去完成

目的:

一,性能,避免重复加载;

二,安全性,避免核心类被修改。

方法区Method Area

存储

它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等:

方法区演进

方法区(永久代(JDK7及以前)、元空间(JDK8以后))

  • 方法区是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
  • 永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
  • 元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间

-XXMateSpaceSize

-XXMAxMateSpaceSize

元空间溢出OOM,(OutOfMenmoryError)

虚拟机栈

  • 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,每个线程都有自己的栈,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,是线程私有的
  • 线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)

栈存储什么?

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 方法出口

局部变量表(Local Variables)

也叫本地变量表。

**作用:**存储方法参数和方法体内的局部变量:8种基本类型变量、对象引用(reference)。

操作数栈(Operand Stack)

**作用:**也是一个栈,在方法执行过程中根据字节码指令记录当前操作的数据,将它们入栈或出栈。用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。供cpu使用

动态链接(Dynamic Linking)

**作用:**可以知道当前帧执行的是哪个方法。**指向运行时常量池中方法的符号引用。**程序真正执行时,类加载到内存中后,符号引用会换成直接引用。

将符号引用指向元空间的地址

方法返回地址(Return Address)

**作用:**可以知道调用完当前方法后,上一层方法接着做什么,即“return”到什么位置去。存储当前方法调用完毕

栈溢出

常见问题栈溢出:Exception in thread “main” java.lang.StackOverflowError通常出现在递归调用时。

问题辨析:

  • 垃圾回收是否涉及栈内存?

    不涉及,因为栈内存在方法调用结束后都会自动弹出栈。

  • 方法内的局部变量是线程安全的吗?

    当方法内局部变量没有逃离方法的作用范围时线程安全,因为一个线程对应一个栈,每调用一个方法就会新产生一个栈桢,都是线程私有的局部变量,当变量是static时则不安全,因为是线程共享的。

设置栈的大小

1
2
3
4
-Xss1m  
-Xss1024k
-Xss1048576
完整的写法是: -XX:ThreadStackSize=1m
1
java -Xss1m YourClassName

堆heap

堆、栈、方法区的关系

HotSpot是使用指针的方式来访问对象:

  • Java堆中会存放指向类元数据的地址

  • Java栈中的reference存储的是指向堆中的对象的地址

分带空间

堆空间划分

堆内存逻辑上分为三部分:

  • Young Generation Space 新生代/年轻代 Young/New
  • Tenured generation space 养老代/老年代 Old/Tenured
  • Permanent Space/Meta Space 永久代/元空间 Permanent/Meta

新生代又划分为:

  • 新生代又分为两部分: 伊甸园区(Eden space)和幸存者区(Survivor pace) 。

  • 幸存者区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。

JDK1.7及之前的堆空间

JDK1.8及之后的堆空间

**注意:**方法区(具体的实现是永久代和元空间)逻辑上是堆空间的一部分,但是虚拟机的实现中将方法区和堆分开了,如下图:

堆空间分为:年轻代、老年代

年轻代:伊甸园区+survivor区

​ suvivor区:s0(from)+s1(to)

​ 空的为to区,两个suvivor区是为了解决内存碎片

分代空间的工作流程

存储在JVM中的Java对象可以被划分为两类:

  • 一类是生命周期较短的对象,创建在新生代,在新生代中被垃圾回收。
  • 一类是生命周期非常长的对象,创建在新生代,在老年代中被垃圾回收,甚至与JVM生命周期保持一致。
  • 几乎所有的对象创建在伊甸园区,绝大部分对象销毁在新生代,大对象直接进入老年代。

新生代

(1)新创建的对象先放在伊甸园区。

(2)当伊甸园的空间用完时,程序又需要创建新对象,此时,触发JVM的垃圾回收器对伊甸园区进行垃圾回收(Minor GC,也叫Young GC),将伊甸园区中不再被引用的对象销毁。

(3)然后将伊甸园区的剩余对象移动到空的幸存0区。

(4)此时,伊甸园区清空。

(5)被移到幸存者0区的对象上有一个年龄计数器,值是1。

(6)然后再次将新对象放入伊甸园区。

(7)如果伊甸园区的空间再次用完,则再次触发垃圾回收,对伊甸园区和s0区进行垃圾回收,销毁不再引用的对象。

(8)此时s1区为空,然后将伊甸园区和s0区的剩余对象移动到空的s1区。

(9)此时,伊甸园区和s0区清空。

(10)从伊甸园区被移到s1区的对象上有一个年龄计数器,值是1。从s0区被移到s1区的对象上的年龄计数器+1,值是2。

(11)然后再次将新对象放入伊甸园区。如果再次经历垃圾回收,那么伊甸园区和s1区的剩余对象移动到s0区。对象上的年龄计数器+1。

(12)当对象上的年龄计数器达到15时(-XX:MaxTenuringThreshold),则晋升到老年代。

总结:

  • 针对幸存者s0,s1,GC之后有交换,谁空谁是to

  • 垃圾回收时,伊甸园区和from区对象会被移动到to区

老年代

经历多次Minor GC仍然存在的对象(默认是15次)会被移入老年代,老年代的对象比较稳定,不会频繁的GC。若老年代也满了,那么这个时候将产生Major GC(同时触发Full GC),进行老年代的垃圾回收。若老年代执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常OutOfMemoryError

永久代/元空间

方法区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,方法区的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、方法区不足时才会触发。如果出现 java.lang.OutOfMemoryError:PermGen space/java.lang.OutOfMemoryError:Meta space,说明是Java虚拟机对永久代内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。

尽管方法区在逻辑上属于堆的一部分,对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。对于HotSpot虚拟机,很多开发者习惯将方法区称之为永久代 ,但严格说两者不同,或者说是使用永久代来实现方法区而已。

GC总结

  • 频繁回收新生代
  • 很少回收老年代
  • 几乎不动方法区

部分收集:

  • 年轻代收集(Minor GC / Young GC):新生代垃圾收集(伊甸园区 + 幸存者区)
  • 老年代收集(Major GC / FullGC):老年代垃圾收集
  • 混合收集(Mixed GC):收集整个新生代以及部分老年代。G1垃圾收集器有这种方式

整堆收集(Full GC):

  • 整个Java堆的垃圾收集和方法区的垃圾收集

年轻代GC触发机制(Minor GC ):

年轻代的Eden空间不足,触发Minor GC。

每次Minor GC在清理Eden的同时会清理Survivor From区。

Minor GC非常频繁,回收速度块。

引发STW(Stop The World),暂停其他用户线程,垃圾回收结束,用户线程恢复。

老年代GC触发机制(Full GC ):

老年代满了,对象从老年代消失是因为发生了Major GC 。

Major GC比Minor GC速度慢10倍以上,STW时间更长。

如果Major GC后,内存还不足,就报OOM。

Full GC触发机制:

Full GC(Full Garbage Collection)是Java虚拟机对堆内存中的所有对象进行全面回收的过程。Full GC的执行时机取决于Java虚拟机的实现和具体的垃圾回收策略。

一般情况下,Full GC发生的情况包括:

  1. 当堆内存空间不足以分配新对象时,会触发一次Full GC。这种情况下,Java虚拟机会先执行一次新生代的垃圾回收(Minor GC),如果仍然无法满足内存需求,则会执行Full GC。
  2. 在某些垃圾回收器中,当老年代空间不足以容纳晋升到老年代的对象时,会执行Full GC。这通常发生在长时间运行的应用程序中,随着对象的逐渐增加,老年代空间可能会变得不足。
  3. 手动调用System.gc()方法或Runtime.getRuntime().gc()方法可以触发Full GC。但值得注意的是,这只是建议Java虚拟机进行垃圾回收的请求,并不能保证立即执行Full GC。

需要注意的是,Full GC是一项资源密集型的操作,会导致应用程序的停顿时间增加,因为在Full GC期间,应用程序的线程会被挂起。因此,在设计和开发应用程序时,应尽量避免频繁触发Full GC,以减少对应用程序性能的影响。

堆参数

  • -Xms表示堆的起始内存,等价于-XX:InitialHeapSize,默认是物理电脑内存的1/64。
  • -Xmx表示堆的最大内存,等价于-XX:MaxHeapSize,默认是物理电脑内存的1/4。
  • -Xmn 表示新生代堆大小,等价于-XX:NewSize,默认新生代占堆的1/3空间,老年代占堆的2/3空间

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

OOM错误

OOM异常:

JVM启动时,为堆分配起始内存,当堆中数据超过-Xmx所指定的最大内存时,将会抛出java.lang.OutOfMemoryError: Java heap space 异常,此时说明Java虚拟机堆内存不够。

原因有二:

(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

VisualVM的使用

OOM时自动生成堆内存快照

VisualVM工具:打开jvisualvm工具 ----> 载入文件 ----> 查看类实例数最多的并且和业务相关的对象 ----> 查看线程的报错信息

1 自己本机新建路径D:\myDump,目的是对应参数-XX:HeapDumpPath=D:\myDump
2 -Xms20m -Xmx20m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\myDump
-XX:+HeapDumpOnOutOfMemoryError:开启内存溢出时自动生成内存快照
-XX:HeapDumpPath=/xxx/dump.hprof:指定dump文件的位置和文件名称

GC垃圾回收

方法区的垃圾回收

方法区中能回收的内容主要就是不再使用的类。判定一个类可以被卸载。需要同时满足下面三个条件:

1、此类所有实例对象没有在任何地方被引用,在堆中不存在任何该类的实例对象以及子类对象。

1
2
Car car = new Car();
car = null;

2、该类对应的 java.lang.Class 对象没有在任何地方被引用。

1
2
3
4
Car car = new Car(); 
Class<? extends Car> aClass = car.getClass();
car = null;
aClass = null;

3、加载该类的类加载器没有在任何地方被引用。

1
2
3
4
5
6
7
Car car = new Car(); 
Class<? extends Car> aClass = car.getClass();
ClassLoader classLoader = aClass.getClassLoader();

car = null;
aClass = null;
classLoader = null;

**总结:**方法区的回收通常情况下很少发生,但是如果通过自定义类加载器加载特定的是少数的类,那么可以在程序中释放自定义类加载器的引用,卸载当前类,垃圾回收及会对这部分内容进行回收

判断对象是否为垃圾的算法

引用计数法

  • 思路:给每一个对象加上引用计数器,每当有一个地方引用该对象,计数器就加1,计数器为0的就是不可能被使用的。
  • 优点:简单、高效
  • 缺点:当两个对象的属性循环引用时,计数器的值永远不可能为0,就无法释放。

可达性分析算法(根可达)

  • 思路:通过将一些特定的对象设置为起始点GC Roots,这个根对象能到达的对象则为可用的,不能到达的对象则为垃圾。
  • GC Roots:
1 栈帧中的局部变量表中的reference引用所引用的对象
2 方法区中static静态引用的对象
3 方法区中final常量引用的对象
4 本地方法栈中JNI(Native方法)引用的对象

垃圾回收算法-清除垃圾

复制算法(年轻代)

核心思想:

1.将堆内存分割成两块From空间 To空间,对象分配阶段,创建对象。

2.GC阶段开始,将GC Root搬运到To空间

3.将GC Root关联的对象,搬运到To空间

4.清理From空间,并把名称互换

标记清除(老年代)

(1)**标记:**使用可达性分析算法,标记出可达对象。

(2)**清除:**对堆内存从头到尾进行线性便遍历,如果发现某个对象没有被标记为可达对象,则将其回收。

缺点:

  • 效率问题(两次遍历)

  • 空间问题(标记清除后会产生大量不连续的碎片。JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。)

标记压缩(标记整理清除)(老年代)

优点:

标记整理算法不仅可以弥补标记清除算法中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。

缺点:

如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。

四种引用

强引用

只要这个对象的引用存在,当空间不足时,垃圾回收器不会回收

软引用

内存足够不回收,内存不足够就回收

弱引用

无论内存是否足够,只要发送垃圾回收,就一定会回收

虚引用

与弱引用一样,但是在垃圾回收之前,会将对象添加到一个与之关联的引用队列中。

可以通过引用队列来判断是否发生垃圾回收或这个对象被垃圾回收了

垃圾收集器

现在JDK17默认是G1垃圾回收器,可以使用效率更高的ZGC垃圾回收器

-XX:+UseZGC

G1垃圾回收器

G1之前分为年轻代和老年代,且是连续的空间。G1之后也存在年轻代、老年代,多了一个HUmongous:用来存储内存超过region一半的大对象,如果不够则两个H合并,但是他们被分为2048个空间(region)。

1 年轻代和老年代是各自独立且连续的内存块
2 年轻代收集使用伊甸园区+幸存零区+幸存一区进行复制算法;
3 老年代收集必须扫描整个老年代区域;
4 都是以尽可能少而快速地执行GC为设计原则。
初始标记 和CMS一样只标记GC Roots直接关联的对象
并发标记 进行GC Roots Traceing过程
最终标记 修正并发标记期间,因程序运行导致发生变化的那一部分对象
筛选回收 根据时间来进行价值最大化收集