八股文——JVM
1. JVM组成
1.1 JVM由哪些部分组成?运行流程?
- Java Virtual Machine:Java 虚拟机,Java程序的运行环境(java二进制字节码的运行环境)
- 好处:一次编写,到处运行;自动内存管理,垃圾回收机制
- 程序运行之前,需要先通过编译器将 Java 源代码文件编译成 Java 字节码文件;
- 程序运行时,JVM 会对字节码文件进行逐行解释,翻译成机器码指令,并交给对应的操作系统去执行。
好处:一次编写,到处运行;自动内存管理,垃圾回收机制
JVM <---> 操作系统(windows、linux)<---> 计算机硬件(cpu、内存条)
java跨平台是因JVM屏蔽了操作系统的差异,真正运行代码的不是操作系统
- JVM 主要由四个部分组成: 运行流程:
- Java 编译器(javac)将 Java 代码转换为字节码(.class 文件)
- 1. 类加载器(ClassLoader)
- 负责加载 .class 文件,将 Java 字节码加载到内存中,并交给 JVM 执行
- 2. 运行时数据区(Runtime Data Area)
- 管理JVM使用的内存。主要包括:
- 方法区(Method Area):存储类的元数据、常量、静态变量等。
- 堆(Heap):存储所有对象和数组,垃圾回收器主要回收堆中的对象。
- 栈(Stack):每个线程都有一个栈,用于存储局部变量、方法调用等信息。
- 程序计数器(PC Register):每个线程有一个程序计数器,指示当前线程正在执行的字节码指令地址。
- 本地方法栈(Native Method Stack):支持本地方法的调用(通过 JNI)。
- 其中方法区和堆是线程共享的,虚拟机栈、本地方法栈和程序计数器是线程私有的。
- 3. 执行引擎(Execution Engine)
- 负责执行字节码,包含:
- 解释器:逐条解释执行字节码。
- JIT 编译器:将热点代码编译为机器码,提高执行效率。
- 垃圾回收器:回收堆中的不再使用的对象,释放内存。
- 4. 本地库接口(Native Method Library)
- 允许 Java 程序通过 java本地接口JNI(Java Native Interface)调用本地方法(如 C/C++ 编写的代码),与底层系统或硬件交互。
1.2 什么是程序计数器
- 程序计数器:线程私有的,每个线程一份,内部保存字节码的行号。用于记录正在执行的字节码指令的地址。
- 每个线程都有自己的程序计数器,确保线程切换时能够继续执行未完成的任务。
1.3 你能给我详细的介绍java堆吗?
- Java堆是 JVM 中用于存储所有对象和数组的内存区域。线程共享的区域。当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
- 它被分为:
- 年轻代(存储新创建的对象),被划分为三部分:
- Eden区:大多数新对象的分配区域;
- S0 和 S1(两个大小严格相同的Survivor区):Eden 空间经过 GC 后存活下来的对象会被移到其中一个 Survivor 区域;
- 老年代:在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
- 永久代:JDK 7 及之前,JVM 的方法区(也称永久代),保存的类信息、静态变量、常量、编译后的代码;
- 元空间:JDK 8 及之后,永久代被 Metaspace(元空间)取代,移除了永久代,把数据存储到了本地内存的元空间中,且其大小不再受 JVM 堆的限制,防止内存溢出。
1.4 什么是虚拟机栈
Java Virtual machine Stacks (java 虚拟机栈)
- 每个线程在 JVM 中私有的一块内存区域,称为虚拟机栈,先进后出,用于存储方法的局部变量和方法调用信息;
- 每个栈由多个栈帧(frame)组成,当线程执行方法时,为该方法分配一个栈帧(Stack Frame);
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法;
垃圾回收是否涉及栈内存?
- 垃圾回收主要指就是堆内存,
- 栈内存中不会有垃圾回收的概念,因为栈内存是由 JVM 自动管理的,方法执行完成时,栈帧弹栈,内存就会释放;
栈内存分配越大越好吗?
- 未必,默认的栈内存通常为1024k;
- 栈内存过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的线程数减半;
方法内的局部变量是否线程安全?
- 方法内的局部变量 本身是线程安全的,因为它们存储在每个线程独立的栈中,不会被其他线程共享。
- 但如果局部变量是引用类型,并且该引用指向的对象逃离了方法作用范围(例如被返回或传递到外部),则需要考虑该对象的线程安全性。
- 如果对象是可变的,并且被多个线程访问,可能会引发线程安全问题。
栈内存溢出情况(StackOverflowError)
栈帧过多导致栈内存溢出;
典型问题:递归调用会在栈中创建新的栈帧,如果递归深度过大,可能会导致栈空间耗尽,从而抛出
栈帧过大导致栈内存溢出
堆栈的区别是什么?
- 栈内存用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。
- 堆会GC垃圾回收,而栈不会;
- 栈内存是线程私有的,而堆内存是线程共有的;
- 两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常
- 栈空间不足:java.lang.StackOverFlowError
- 堆空间不足:java.lang.OutOfMemoryError
1.5 说一下JVM运行时数据区
JVM 运行时数据区包括方法区、堆、栈、程序计数器和本地方法栈。
- 方法区存储类的元数据、常量池、静态变量和 JIT 编译后的代码。
- 类的结构信息:每个类的信息,如类名、父类名、接口、方法、字段的名称和描述等。常量池:存储常量值,如字符串常量、类常量等。静态变量:属于类的变量,而不是某个实例的变量。JIT编译后的代码是指 Jvm在运行时将热点代码从字节码编译为本地机器代码。方法区在 JDK 7 之前被称为 "永久代",从 JDK 8 开始,永久代被移除,改为使用元空间
- 堆是 JVM 中最大的内存区域,负责存储所有的对象实例和数组,并进行垃圾回收。
- 栈存储每个线程的局部变量、方法调用信息和返回地址等。
- 堆和栈是线程共享和线程私有的区域。
- 程序计数器是每个线程私有的,用于存储当前线程正在执行的字节码指令的地址。
- 本地方法栈支持 JNI 本地方法调用,线程私有。
- 专门为本地方法调用而设计。它用于执行本地代码时所需的栈空间。
1.6 能不能介绍一下方法区
- 方法区 是 JVM 运行时数据区的一部分,主要用于存储类的信息、常量、静态变量以及 JIT 编译后的代码。
- 在 JDK 7 之前,这部分内存称为永久代(PermGen),而在 JDK 8 以后,永久代被移除,取而代之的是元空间(Metaspace),它位于本地内存中,不再受堆内存限制。
- 虚拟机启动的时候创建,关闭虚拟机时释放。
- 方法区的内存由 JVM 管理,并在类卸载时进行垃圾回收。
- 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspac
1.7 介绍一下运行时常量池?
常量池
- 可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池
- 常量池是 .class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
1.8 你听过直接内存吗?
- 它不受 JVM 内存回收管理,是虚拟机的系统内存;
- 常见于在 NIO 中使用直接内存,不需要在堆中开辟空间进行数据的拷贝,jvm可以直接操作直接内存,从而使数据读写传输更快。但分配回收成本较高。
- 使用传统的IO的时间要比NIO操作的时间长了很多,也就说NIO的读性能更好。
- 这个是跟我们的JVM的直接内存是有一定关系,如下图,传统阻塞IO的数据传输流程和NIO传输数据的流程。
2. 类加载器
2.1 什么是类加载器,类加载器有哪些?
JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将.class字节码文件加载到JVM内存,生成对应的Class对象,供程序使用。
- 启动类加载器(BootStrap ClassLoader):
- 该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。
- 扩展类加载器(ExtClassLoader):
- 该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。
- 应用类加载器(AppClassLoader):
- 该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。
- 自定义类加载器:
- 开发者自定义类继承ClassLoader,实现自定义类加载规则。
类加载器的体系并不是“继承”体系,而是委派体系,类加载器首先会到自己的parent中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次
2.2 什么是双亲委派模型?
- 双亲委派模型要求类加载器在加载某一个类时,先委托父加载器尝试加载。
- 如果父加载器可以完成类加载任务,就返回成功;
- 只有父加载器无法加载时,子加载器才会加载。
2.3 JVM为什么采用双亲委派机制?
避免类的重复加载:父加载器加载的类,子加载器无需重复加载。
保证核心类库的安全性:为了安全,保证类库API不会被修改。如
java.lang包下的类
只能由 启动类加载器Bootstrap ClassLoader 加载,防止被篡改。
2.4 说一下类的生命周期
- 一个类从被加载到虚拟机内存中开始,到从内存中卸载,它的整个生命周期包括了:
加载、验证、准备、解析、初始化、使用和卸载这7个阶段。- 其中,验证、准备和解析这三个部分统称为连接(linking)。
2.5 说一下类装载的执行过程?
类装载过程包括三个阶段:载入、连接和初始化,连接细分为 验证、准备、解析,这是标准的 JVM 类装载流程。
- 加载(Loading):通过类加载器找到 .class 文件读取到内存,生成 Class 对象。
- 连接(Linking):
验证:检查字节码是否合法,防止恶意代码破坏 JVM;
准备:为类的静态变量分配内存并设置默认初始值,但不执行赋值逻辑;
解析:将常量池中的 符号引用(如类名、方法名)转为 直接引用(内存地址)。
- 初始化(Initialization):执行类的静态代码块和静态变量赋值。
在准备阶段,静态变量已经被赋过默认初始值了,在初始化阶段,静态变量将被赋值为代码期望赋的值。比如说 static int a = 1;,在准备阶段,a 的值为 0,在初始化阶段,a 的值为 1
类装载完成后的阶段:加载完成后,类进入‘使用阶段’。当 Class 对象不再被引用时,可能触发‘卸载’。
- 使用:JVM 通过 Class 对象创建实例、调用方法,进入正常运行阶段。
- 卸载:当 Class 对象不再被引用时,由 GC 回收,但 JVM 核心类(如 java.lang.*)不会被卸载。
3. 垃圾回收
3.1 简述java垃圾回收机制?(GC是什么?为什么要GC)
- GC(Garbage Collection,垃圾回收)是 Java 中自动管理内存的机制,负责回收不再使用的对象,以释放内存空间。
- 垃圾回收是 Java 程序员不需要显式管理内存的一大优势,它由 JVM 自动进行。
GC 的主要目的是:
自动管理内存:程序运行过程中会创建大量的对象,但一些对象在使用完后不再被引用。手动管理这些对象的内存释放非常繁琐且容易出错;
防止内存泄漏:如果不及时释放无用对象的内存,系统的可用内存会越来越少,最终可能导致 OutOfMemoryError;
避免内存溢出:GC 机制能够保证内存不会因为长期积累未回收的对象而耗尽。
3.2 对象什么时候可以被垃圾器回收?
如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,
1. 引用计数法:通过计数引用的数量,当引用为 0 时回收。但不能处理循环引用
这种方法通过给每个对象维护一个引用计数器。当有一个新的引用指向该对象时,引用计数加 1;当引用离开时,计数减 1。
2. 可达性分析算法:通过检查对象是否从根对象可达,无法访问的对象可以回收。是 Java 使用的主要方法。
根对象是那些肯定不能当做垃圾回收的对象,就可以当做根对象
根对象包含:虚拟机栈中的局部变量;静态变量;活动线程的引用;JNI 引用的对象。