JVM 基础面试题总结

97 阅读7分钟

本文转自微信公众号(程序员cxuan)

本文主要讲解基础的JVM相关面试题

1、JVM 的主要作用

JVMJava Vitual Machine(Java虚拟机) 的缩写,JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在不同的平台上运行。

2、Java 的内存区域?

JVM 在执行 Java 程序的过程中会把它管理的内存分为若干不同的区域,这些组成部分有些是线程私有的,有些则是线程共享的, Java 内存区域也叫运行时数据区,他的具体划分如下:

内存区域划分

  • 虚拟机栈 :Java虚拟机栈是线程私有的数据区, Java虚拟机栈与线程相同,虚拟机栈也是局部变量的存储位置。方法在执行过程中,会在虚拟机中创建一个 栈帧(stack frame)。每个方法执行的过程就对应了一个入栈和出栈的过程。

    当前栈帧

  • 本地方法栈:本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是 Java 中使用 native 关键字修饰的方法所存储的区域。

  • 程序计数器: 程序计数器也是线程私有的数据区,这部分区域用于存储线程的指令地址,用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都是通过程序计数器来完成。

  • 方法区: 方法区是各个线程共享的内存区域,它用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • :堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,所有的对象实例都会分配在堆上。 JDK 7 之后,字符串常量池从永久代中剥离出来,存放在堆中。

堆空间的内存分配(默认情况下): 命令行上执行如下命令,会查看默认的 JVM 参数

java -XX:+PrintFlagsFinal -version

输出的内容非常多,但是只有两行能够反映出上面的内存分配结果

uintx NewRatio                              = 2                                {product}  
uintx InitialSurvivorRatio                  = 8                                {product}

堆空间内存分配

名称空间分配
eden区8/10的年轻代空间
survivor01/10的年轻代空间
survivor11/10的年轻代空间
老年代三分之二的堆空间
年轻代三分之一的堆空间
  • 运行时常量池: 运行时常量池又被称为 Runtime Constant Pool,这块区域是方法区的一部分,他的名字非常有一起,通常被称为 非堆。它并不要求常量一定只有在编译期才能产生,他就是并非编译期间将常量放在常量池中,运行期间也可以将新的常量放入常量池中,String 的 intern 方法就是一个典型的例子。

3、Java 中的类加载机制?

Java虚拟机负责把描述类的数据从 Class 文件加载到系统内存中,并对类的数据进行 校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称为 Java 的 类加载机制

一个类从被加载到虚拟机内存开始,到卸载出内存为止,一共会经历下面这些过程。

类的加载过程

类的加载机制一共有五个步骤,分别是 加载、链接、初始化、使用和卸载 阶段,这五个阶段的顺序是确定的。

其中链接阶段会细分为三个阶段,分别是 验证、准备、解析 阶段,这三个阶段的顺序是不确定的,这三个阶段通常交互进行。解析阶段通常会在初始化之后再开始,这是为了支持 Java 语言的运行时绑定特征(也被称为 动态绑定)。

下面我们来聊一下这几个过程。

3.1 加载

关于什么时候开始加载这个过程, 《Java虚拟机规范》并没有强制约束,所以这一点我们可以自由实现。加载是整个类加载过程的第一个阶段,在这个阶段,Java 虚拟机需要完成三件事情:

  • 通过一个类的全限定名(一个类的全限定名是将类全名的.全部替换为/)来获取定义此类的二进制字节流。
  • 将这个字节流表示的一种存储结构转换为运行时数据区中方法区的数据结构。
  • 在内存中生成一个 Class 对象,这个对象就代表了这个数据结构的访问入口。

《Java 虚拟机规范》并未规定全限定名是如何获取的,所以业界中有很多获取全限定名的方式

  • 从 ZIP 包中读取,最终会改变为 JAR、EAR、WAR 格式。
  • 从网络中获取,最常见的应用就是 Web Applet。
  • 运行时动态获取,使用最多的就是动态代理技术。
  • 由其他文件生成,比如 JSP 应用场景,由 JSP 文件生成对应的 Class 文件。
  • 从数据库中读取,这种场景就比较小了。
  • 可以从加密文件中获取,这是典型的防止 Class 文件被反编译的保护措施。

加载阶段既可以使用虚拟机内置的引导类加载器完成,也可以使用用户自定义的类加载器来完成。程序员可以通过自己定义类加载器来控制字节流的访问方式。

数组的加载不需要通过类加载器来创建,他是可以在内存中分配,但是数组的元素类型(数组去掉所有维度的类型)最终还是要靠类加载器来完成加载。

3.2 链接

3.2.1 验证

加载过后的下一个阶段就是验证,因为我们上一步讲到在内存中生成了一个 Class 对象,这个对象是访问其代表数据结构的入口,所以这一步验证的工作就是确保 Class 文件的字节流中的内容符合《Java 虚拟机规范》中的要求,保证这些信息被当做代码运行后,他不会威胁到虚拟机的安全。

验证阶段主要分为四个阶段的校验:

  • 文件格式验证。
  • 元数据验证。
  • 字节码验证。
  • 符合引用验证。

3.2.1.1 文件格式验证

这一阶段可能会包含下面这些验证点

  • 魔数是否以 0xCAFEBABE 开头。
  • 主、次版本号是否在当前 Java 虚拟机接受范围之内。
  • 常量池的常量中是否有不支持的常量类型。
  • 指向常量的各种索引中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据。
  • Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息。

实际上验证点远远不止这些,上面这些只是从 HotSpot 源码中摘抄的一小段内容。

3.2.1.2 元数据验证

这一阶段主要是对字节码描述的信息进行语义分析,以确保描述的信息符合《Java 语言规范》,验证点包括:

  • 验证的类是否有父类(除了 Object 类之外,所有的类都应该有父类)。
  • 要验证类的父类是否继承了不允许继承的类。
  • 如果这个类不是抽象类,那么这个类是否实现了父类或者接口中要求的所有方法。
  • 是否覆盖了 final 字段,是否出现了不符合规定的重载等。

需要记住这一阶段是对《Java 语言规范》德验证。

3.2.1.3 字节码验证

字节码验证阶段是最复杂的一个阶段,这个阶段主要是确定程序语意是否合法、是否是符合逻辑的。这个阶段主要是对类的方法体(Class文件中的 Code 属性)进行校验分析。这部分验证包括:

  • 确保操作数栈的数据类型和实际执行时的数据类型是否一致。
  • 保证任何跳转指令不会跳出到方法体外的字节码指令上。
  • 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是不能把父类数据类型赋值子类等诸如此不安全的类型转换。
  • 其他验证。

如果没有通过字节码验证,就说明验证出问题。但是不一定通过了字节码验证,就能确保程序是安全的。