关于JVM中的几大面试题

一、介绍

本文介绍JVM中的几个面试题,十分有用

主要有几题

二、答疑

1)Java类的加载过程

image-20230830182103851

简单来说,可以这样理解分类

  1. 类的加载

    1. 获取二进制文件,将.class文件加载至JVM
  2. 类的连接,验证,准备,解析合称连接

    1. 分配空间
    2. 静态属性赋值(赋初始值,而不是我们给予的值,如int是0,包装类为null
  3. 类的初始化,(是初始化,不是实例化)

    1. 静态属性赋值,这时候就是赋我们给予的值了

什么是符号引用,什么又是直接引用

可以这样进行理解,我们有一个A类和B类,A类中使用到了B

在字节码中,会用一个符号代表这是B类,这就是符号引用

而在B类进行类加载后,JVM成功的加载了这个B类,使得堆内存中有对应的B.class的对象,同时方法区中有静态方法与属性。

这个时候,A类就会将之前的符号引用,改为直接引用,设置为上面堆内存的B.class对象,或者方法区中的静态方法与属性

类加载的时机

  1. 实例化类对象
  2. 调用类的静态方法
  3. 使用类的静态属性

2)双亲委派机制是什么

在了解双亲委派机制之前,我们先设想一个问题,就是如果我们用户自己写一个String这样一个的类,会出现什么样的情况?

这个问题说简单也简单,说复杂就比较复杂了,这个问题正好是由双亲委派机制来进行解决的。


在了解双亲委派机制之前,我们先得了解几个ClassLoader类加载器

类加载器 说明 加载类的范围
Bootstrap ClassLoader 启动类加载器,最顶层的类加载器,这个加载器,Java中不能获取,返回的是一个null <JAVA HOME>/lib
Extension ClassLoader 扩展类加载器 <JAVA HOME>/lib/ext
Application ClassLoader 应用程序类加载器,也是我们最常用的类加载器 classpath/java.class.path
User ClassLoader 用户自定义的类加载器 任意来源的类

好的,当了解完上面的四种类加载器之后,我们将进行验证,看下面代码

1
2
3
4
5
6
7
8
9
10
package com.banmoon.parentsappoint;

public class ParentsAppointTest {

public static void main(java.lang.String[] args) {
System.out.println("java.lang.String:" + "abc".getClass().getClassLoader());
System.out.println("com.banmoon.parentsappoint.String:" + String.class.getClassLoader());
}

}

image-20230812165237418

为什么,他们的类加载器是不同的呢。有人说了,是因为类加载器本身就是有不同的加载类职责范围。

那么当我们进行类加载的时候,程序怎么知道这个类要用什么类加载器。然而就是这段不同的类,确定使用不同类加载器的过程,就是我们将的双亲委派机制。

我们先看这段代码,正是双亲委派机制的代码,在ClassLoader.java中可以找到这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,先检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// parent是父亲加载器,这里仅仅是逻辑层面上的,并不是指继承方面的父类
if (parent != null) {
// 如果父亲加载器不为空,则先交给父亲加载器进行类加载
c = parent.loadClass(name, false);
} else {
// 如果父亲加载器为空,那说明接下来是Bootstrap ClassLoader了,直接交给特殊的方法进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 如果上面的父亲类加载器没有加载成功,那就自己查找
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

这就是双亲委派机制,下面可以画一个图

image-20230812182014362

可以看到,类加载的时候,永远都是Bootstrap ClassLoader(启动类加载器)尝试去加载

当然,如果类不合适,将会向下进行委派加载

上面的这种行为可以这样概括,向上检查,向下委托加载

3)JVM内存模型

下面这个就是JVM的内存模型,有些细节没有完全画出来,后续会补上

image-20230813094052500

需要讲一下,其中的这些是什么意思

  • 堆内存:这个先简单概括一下,基本对象的创建都会存储在该内存区域

  • 方法区:方法区是一个概念,然而不同的虚拟机会有不同的实现。在这里,我们只需要关注方法区究竟是干什么的即可。在方法区中,主要存储以下三个内容,

    • 类签名:记录权限名,访问权限,版本号
    • 属性:记录类属性,访问权限
    • 方法:方法字节码记录
  • 常量池:在上面讲到过符号引用改为直接引用,那些这一块常量池就是存储这一块东西的。可以理解为,他是由字节码中一个指针指向另一个字节码。比如说定义了一个String的属性,那么在类加载的连接阶段,常量池中会存储这么一个指针常量。

  • 运行时常量池:这是再上面模型图中没有体现的,需要单独讲解。它里面主要存储两个内容。可以看到的是,运行时常量池是包括了常量池的。所以这一块知识点,不需要额外去记忆。

    • 运行时产生的:如字符串,如上面的符号引用改为直接引用
    • 编译期间产生的:主要是字节码中定义的静态信息,各个类的Class对象。还有就是开发者编写的静态变量。
  • :对象主要存储在堆内存中,这里也是垃圾回收GC的主战场。下面篇幅会提到

  • 程序计数器:用来存储字节码的指令地址,提供给执行引擎去读取执行。简单的来说就是执行到哪一步了

  • 虚拟机栈:换个名字叫Java方法栈,这样好理解一下。Java在调用方法时,会将字节码方法入栈,这个东西叫做栈帧。栈这种数据结构,就是先入后出。类似的,一个A方法压入栈,这个方法调用一个B方法,就会将B方法压入栈。结构展示A在最底下,B在上。在结束的时候,是B方法栈帧先结束,然后才是A方法的栈帧。符合先入后出原则。在栈帧结构内部,我们可以如下进行划分,分别是

    • 局部变量表
      1. 主要存储方法的参数、定义在方法内的局部变量,包括八大基本数据类型,对象的引用地址,返回值地址。
      2. 局部变量表中存储的基本单元为变量槽(Sot),32位(4字节)以内的数据类型占一个slot,64位(long,double)的占两个slot。
      3. 局部变量表是一个数字数组,byte、short、char都会被转化为int,boolean类型也会被转化为int,0代表alse、非0代表true。
      4. 局部变量表的大小是在编译期间决定下来的,所以在运行时它的大小是不会变的。
      5. 局部变量表中含有直接或者间接指向的引用类型变量时,不会被垃圾回收处理。
    • 操作数栈:除了上面的局部变量表,还有一个操作数栈。这个操作数栈是在方法执行的过程中,根据字节码的指令,将上面的变量入栈,再执行指令。如执行复制、交换、求和等操作
    • 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
    • 方法出口:存放调用该方法的计数器的值;有两种情况,一种是方法正常返回,另一种是方法出先异常的返回。存储在一个异常处理表,方便再发生异常的时候找到处理异常的代码。
  • 本地方法栈:由于Java是由C++语言编写的,里面肯定会调用到C++,故本地方法栈就是存储的是调用C++方法时的变量存储。

三、最后

我是半月,你我一同共勉!!!