Java虚拟机面试题总结

标题 地址
Java虚拟机面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/125881033
Java集合面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/126058839
Mysql数据库面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/125901358
Spring面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/126354473
Redis面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/126111149
Java并发面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/125984564
分布式面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/126256455

1.JVM内存模型

在这里插入图片描述

1.1 堆

1.java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
2.在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
3.java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。
4.从内存回收角度来看java堆可分为:新生代和老生代。
5.从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
6.无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。
7.根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

1.2 方法区

1.方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
2.它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

1.3 程序计数器

1.程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号)
2.由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

1.4 虚拟机栈

1.java虚拟机是线程私有的,它的生命周期和线程相同。
2.虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息

解释:每虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。
在这里插入图片描述
解析栈帧:
1.局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
2.操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
3.动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
4.出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常落

1.5 本地方法栈

1.本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字
2.它是虚拟机栈为虚拟机执行Java方法(也就是字节码)的服务
3.native关键字的方法是看不到的,必须要去oracle官网去下载才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。
4.同理可得,本地方法栈中就是C和C++的代码

2.JVM垃圾回收机制

2.1 如何判断对象是否存活

  • 引用计数算法
    1.引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。
    2.首先需要声明,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存。
    3.什么是引用计数法:每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,计数器为0就代表该对象死亡

  • 可达性分析
    通过一系列称为 GC Roots 的对象作为起始点,从这些点向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则认为对象不可达。

即使在可达性分析中不可达的对象,也并非是非死不可,只是处于缓刑阶段,要真正死亡至少要经历两次标记,这跟 finalize 有关。

2.2 垃圾回收算法

常见的算法有新生代的复制算法,以及老年代的标记清除和标记整理算法。

1.复制算法

为了解决效率问题,出现了“复制”收集算法,它将内存容量划分为大小相等的两块,每次只用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间清理掉。这样每次都对半个区进行内存回收,内存分配时也就不用考虑内存碎片的问题,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效,代价就是内存缩小了为原来的一半。
在这里插入图片描述

2.标记-清除算法

最基本的收集算法,分为“标记”和“清除”两个阶段,首先标记 出所需要回收的对象,在标记之后统一回收所有被标记的对象,之所有被称为标记算法是应为,后续的收集算法都是基于这种思路对其该善的。

这个算法的不足有两个方面
1.效率问题,标记和清除效率问题都不高
2.空间问题,标记清除后会产生大量的不连续的内存碎片,当在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作
在这里插入图片描述

3.标记-整理算法

复制收集算法在对象存活率比较高时就要进行较多的复制操作,效率将会变低。更关键如果不想浪费50%的空间就要使用额外 的空间进行分配担保,以应对被使用的内存中所有的对象100%存活的极端情况
对于“标记-整理”算法,标记过程与“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有的存活对象都向一端移动,然后清除掉边界以外的内存
在这里插入图片描述

4.分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照 1:1 的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将 Eden 和 Survivor 中还存活的对象复制到另一块 Survivor 空间中,然后清理掉 Eden 和刚才使用过的 Survivor 空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)。

2.3 常见的垃圾回收器

在这里插入图片描述

图中展示了 7 种作用于不同分代的收集器,分别是新生代收集器 Serial、ParNew、Parallel Scavenge,老年代收集器 CMS、Serial Old、Parallel Old 以及整堆收集器 G1。如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。

Serial 收集器
单线程、简单高效,对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程回收效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。

ParNew 收集器
ParNew 收集器其实就是Serial收集器的多线程版本。除了使用多线程外其余行为均和 Serial 收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等),同样存在 STW 问题。

可以使用 -XX:ParallelGCThreads 参数来设置垃圾收集的线程数。许多运行在 Server 模式下的虚拟机中首选的新生代收集器,因为它是除了 Serial 外,唯一能与 CMS 收集器配合工作的。

Parallel Scavenge 收集器
这是一个可以控制吞吐量的多线程收集器,故也称为吞吐量优先收集器。相比 ParNew 收集器,Parallel Scavenge 可以使用 XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间以及 XX:GCRatio 直接设置吞吐量的大小。

同时支持 GC 自适应调节策略。即 Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。当打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等信息,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量。

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

Parallel Old 收集器
是 Parallel Scavenge收集器的老年代版本,采用标记-整理算法。适用于注重高吞吐量以及 CPU 资源敏感的场合。

CMS 收集器
一种以获取最短回收停顿时间为目标的收集器。也是基于标记-清除算法的并发收集器。此处并发同步于前面提及的几种多线程并行收集器,是指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。

非常适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景。

G1 收集器
一款面向服务端应用的垃圾收集器。

4.类加载机制

4.1 什么是类加载

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。

4.2 类加载的过程

在 Java 中,一个类从定义到使用可以分为加载、验证、准备、解析、初始化、使用以及卸载几个步骤。其中验证、准备和解析又可以统称为连接。
在这里插入图片描述

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

  • 验证
    验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
    1)文件格式验证:此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
    2)元数据验证:此阶段保证不存在不符合 Java 语言规范的元数据信息。如是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
    3)字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
    4)符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。

    可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

  • 准备
    为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。

  • 解析
    虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
    同时为支持运行时绑定,解析过程在某些情况下可在初始化之后再开始。

  • 初始化
    到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 () 方法的过程。该方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。

此过程不包括构造器中的语句。构造器是初始化对象的,类加载完成后,创建对象时候将调用的 () 方法来初始化对象。

4.3 类加载顺序

1)父类静态变量/静态初始化块 - 子类静态变量/静态初始化块;
2)父类变量/初始化块 - 父类构造器;
3)子类变量/初始化块 - 子类构造器。

4.4 类加载时机

对于加载,Java 虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则严格规定了以下几种情况必须立即对类进行初始化,如果类没有进行过初始化,则需要先触发其初始化。

1)遇到 new(用 new 实例对象),getStatic(读取一个静态字段),putstatic(设置一个静态字段),invokeStatic(调用一个类的静态方法)这四条指令字节码命令时;

2)使用 Java.lang.reflect 反射包的方法对类进行反射调用时,如果此时类没有进行 init,会先 init;

3)当初始化一个类时,如果其父类没有进行初始化,先初始化父类;

4)JVM 启动时,用户需要指定一个执行的主类(包含 main 的类),虚拟机会先执行这个类;

5)当使用 JDK 1.7 的动态语言支持的时候,当 java.lang.invoke.MethodHandler 实例后的结果是 REF-getStatic/REF_putstatic/REF_invokeStatic 的句柄,并且这些句柄对应的类没初始化的话应该首先初始。

以上这 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用,如:

1)通过子类引用父类的静态字段,不会导致子类初始化。

2)通过数组定义来引用类,不会触发此类的初始化。MyClass[] cs = new MyClass[10];

3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

4.5 什么是类加载器

类加载器负责加载所有的类,一个应用程序由N多个类组成,java程序启动时并不是一次性把所有的类全部加载后在运行,它总是先把保证程序运行的基础类一次性加载到Jvm中,其它类等到jvm用到的时候在加载,这样好处就是节省了内存的开销,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

4.6 有哪些类加载器

  • Bootstrap ClassLoader:(启动类加载器):由C++实现,是虚拟机自身的一部分。负责将存放在lib目录,并被虚拟机识别的类库加载到虚拟机内存中它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

  • Extension ClassLoader(扩展类加载器):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

  • Application ClassLoader(应用程序类加载器/系统类加载器):它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

4.7 类加载机制

JVM的类加载机制主要有如下3种。
1.全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
2.双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
3.缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

4.8 双亲委派模型

在这里插入图片描述
双亲委派机制工作原理
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势:
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

4.9 如何破坏双亲委派模型?

双亲委派机制原则在 loadClass() 方法中,只需要绕开该方法中即可。

  • 使用自定义类加载器,重写 loadClass() 方法。(注意不是 findClass() 方法)
  • 给当前线程设定关联类加载器(线程上下文类加载器),使用 SPI(Service Provider Interface ) 机制绕开 loadclass() 方法。
    第二种方法已被广泛应用,如 JDBC、Dubbo 等。

4.10 类加载方式

1.由 new 关键字创建一个类的实例(静态加载)
在由运行时刻用new 方法载入
如:Dog dog = new Dog();

2.调用 Class.forName()方法(动态加载)
通过反射加载类型,并创建对象实例
如:Class clazz = Class.forName(“Dog”);
Object dog =clazz.newInstance();

3.调用某个 ClassLoader 实例的 loadClass() 方法(动态加载)
通过该 ClassLoader 实例的 loadClass() 方法载入。应用程序可以通过继承 ClassLoader 实现自己的类装载器。
如:Class clazz = classLoader.loadClass(“Dog”);
Object dog =clazz.newInstance()

5.调优

5.1 调优工具?

1.jdk自带工具

首先,JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
1)jps:与 Linux 上的 ps 类似,用于查看有权访问的虚拟机的进程,并显示他们的进程号。当未指定 hostid 时,默认查看本机 JVM 进程。

2)jinfo:可以输出并修改运行时的 Java 进程的一些参数。

3)jstat:可以用来监视 JVM 内存内的各种堆和非堆的大小及其内存使用量。

4)jstack:堆栈跟踪工具,一般用于查看某个进程包含线程的情况。

5)jmap:打印出某个 JVM 进程内存内的所有对象的情况,一般用于查看内存占用情况。

6)jconsole:一个 GUI 监视工具,可以以图表化的形式显示各种数据,并支持远程连接。

5.2 常用的调优参数?

1.常用的设置

1)-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。

2)-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。

3)-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。

4)-XX:NewSize=n:设置年轻代初始化大小大小。

5)-XX:MaxNewSize=n:设置年轻代最大值。

6)-XX:NewRatio=n:设置年轻代和年老代的比值。如 n = 3 时表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4。

7)-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8。

8)-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。

9)-XX:ThreadStackSize=n:线程堆栈大小。

10)-XX:PermSize=n:设置持久代初始值。

11)-XX:MaxPermSize=n:设置持久代大小。

12)-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。

2.不常用的设置

1)-XX:LargePageSizeInBytes=n:设置堆内存的内存页大小。

2)-XX:+UseFastAccessorMethods:优化原始类型的 getter 方法性能。

3)-XX:+DisableExplicitGC:禁止在运行期显式地调用S ystem.gc(),默认启用。

4)-XX:+AggressiveOpts:是否启用 JVM 开发团队最新的调优成果。如编译优化,偏向锁,并行年老代收集等,JDK 6 之后默认启动。

5)-XX:+UseBiasedLocking:是否启用偏向锁,JDK 6 默认启用。

6)-Xnoclassgc:是否禁用垃圾回收。

7)-XX:+UseThreadPriorities:使用本地线程的优先级,默认启用。

3.回收器相关设置

1)-XX:+UseSerialGC:设置串行收集器,年轻带收集器。

2)-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK 5 以上 JVM 会根据系统配置自行设置,所以无需再设置此值。

3)-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量。

4)-XX:+UseParallelOldGC:设置并行年老代收集器,JDK 6 支持对年老代并行收集。

5)-XX:+UseConcMarkSweepGC:设置年老代并发收集器。

6)-XX:+UseG1GC:设置 G1 收集器,JDK 9 默认垃圾收集器

猜你喜欢

转载自blog.csdn.net/qq_37924396/article/details/125881033