面试题005-Java-JVM(上)
目录
题目自测
- 1. JVM由哪几部分组成?
- 2. 运行时数据区中包含哪些区域?
- 3. 栈和堆中分别存放什么数据?
- 4. 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) ?
- 5. 堆空间的基本结构了解吗?什么情况下对象会进入老年代?
- 6. 大对象放在哪个内存区域?
- 7. Java对象的创建过程?
题目答案
1. JVM由哪几部分组成?
答:JVM是一个可以执行字节码(.class)文件的虚拟计算机,同时提供了内存管理,垃圾回收等机制。它包含了以下几个主要部分。
- 类加载子系统:负责将字节码文件(.class)加载到JVM。
- 运行时数据区:是JVM在执行期间使用的内存区域。
- 执行引擎:负责解释或编译字节码为机器码,供处理器执行。
- 本地库接口:提供了一组调用操作系统或其他语言编写的本地库的API。
2. 运行时数据区中包含哪些区域?
答:运行时数据区是JVM在执行Java程序时为其分配的内存区域。
- 程序计数器:是一块较小的内存空间,是当前线程正在执行的那条字节码指令的地址。如果线程正在执行本地方法,这这个计数器的值是未定义的。
- Java虚拟机栈:每个线程在创建的时候都会创建一个虚拟机栈,用于存储线程的局部变量表、操作数帧、动态链接、方法出口信息等。Java虚拟机栈中包含多个栈帧,每个方法被调用到执行完成的过程,都对应着虚拟机中一个栈帧的入栈到出栈的过程。
- 本地方法栈:是JVM运行Native方法准备的空间,它与Java虚拟机栈实现的功能类似,它是描述本地方法运行过程的内存模型。
- 堆:用于存放几乎所有的对象实例和数组,是垃圾回收器主要工作的区域。
- 方法区:用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等。JDK1.8之前,被实现为永久代。JDK1.8开始永久代被原空间取代。元空间使用的是本地内存而非堆内存。
3. 栈和堆中分别存放什么数据?
答:栈(Java虚拟机栈)中存放的数据:
- 局部变量表:主要用于存储方法参数,方法内的局部变量,数据类型包括基本数据类型和对象的引用。
- 操作数栈:用于临时存储操作指令和方法执行过程中中间结果。
- 动态链接:指向方法所属类的常量池的引用,用于解析方法中的符号引用。
- 方法返回地址:存储方法调用后执行的下一条指令地址。
堆中存放的数据: - 对象实例:在程序中通过new关键字创建的对象实例,包括对象的属性和方法。
- 数组:所有类型的数组,包括基本类型数组和对象数组。
4. 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) ?
答:将永久代替换为元空间主要是为了解决永久代的一些固有问题和限制,提高JVM的性能和灵活性。
- 提高内存管理的灵活性和效率:永久代的内存大小是在JVM启动时就已经设定好的,不能动态调整。元空间使用的是本地内存而不是Java堆内存,它的大小可以根据需要动态调整。
- 解决类卸载和垃圾回收问题:永久代的GC行为较为复杂且不可预测,并且回收效率偏低。
- 提供更好的性能和稳定性:使用元空间使得JVM内存管理更加统一和一致,因为元空间和其他内存区域一样,都是使用本地内存进行管理。这样可以简化内存管理策略,提升整体性能和稳定性。
- 简化JVM的内存管理
5. 堆空间的基本结构了解吗?什么情况下对象会进入老年代?
答:堆空间的基本结构主要由新生代、老年代和永久代组成。JDK8以后永久代被元空间取代,使用本地内存来存储。
- 新生代:新生代进步一细分为Eden区和两个幸存者区(Survivor 0 和 Surivivor 1)
- Eden区:新创建的对象首先在Eden区分配内存。
- 幸存者区(S0, S1):用于存放在新生代垃圾回收时存活下来的对象。每次Minor GC后,存活的对象会在这两个区来回复制。
- 老年代:经过多次Minor GC后仍然存活的对象。老年代进行垃圾回收(Major GC 或 Full GC)的频率较低。
- 永久代/元空间:用于存储类的元数据,包括类的定义、常量、静态变量、即时编译后的代码等。
对象进行老年代的情况:
- 年龄阈值达到:每个对象在新生代分配内存时都有一个年龄,每次Minor GC后年龄都会加1。当年龄达到一定阈值(默认是15)后,对象会被提升到老年代。
- 大对象:如果对象太大,超过了JVM设定的阈值,对象会直接在老年代分配空间。
- Survivor区空间不足:如果在进行Minor GC时,幸存者区没有足够的空间来容纳所有存活的对象,这些对象会被
- 动态对象年龄判定:如果Survivor空间中相同年龄的所有对象大小超过了Survivor空间的一半,那么年龄大于或等于该年龄的对象可以直接进入老年代。
// 动态年龄计算代码
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
//survivor_capacity是survivor空间的大小
size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);//TargetSurvivorRatio 为50
size_t total = 0;
uint age = 1;
while (age < table_size) {
total += sizes[age];//sizes数组是每个年龄段对象大小
if (total > desired_survivor_size) break;
age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
...
}
6. 大对象放在哪个内存区域?
答:大对象(非常大的数组和字符串)通常会直接分配在老年代内存区域。这是为了避免新生代进行频繁的垃圾回收时,大对象频繁地在Eden区和Survivor区之间复制,从而提高垃圾收集效率。
配置大对象直接进入老年代的阈值:
# 将大于1MB的对象直接分配在老年代
java -XX:PretenureSizeThreshold=1m -jar your-application.jar
7. Java对象的创建过程?
答:
- 类加载检查
- 如果类没有被加载、连接和初始化,JVM会先进行类加载。这包括以下步骤:
- 加载:通过类加载器读取类文件,并将类的字节码加载到内存中。
- 连接:包括验证、准备和解析三个阶段。验证类文件的正确性,准备类的静态变量并分配内存,解析符号引用为直接引用。
- 初始化:执行类的静态初始化块和静态变量的初始化。
- 如果类没有被加载、连接和初始化,JVM会先进行类加载。这包括以下步骤:
- 内存分配
- JVM在堆中为新对象分配内存。分配的内存大小由对象的结构决定,包括对象头和实例数据。
- JVM有两种主要的内存分配方式:
- 指针碰撞(Bump-the-pointer):如果堆内存是规整的,分配指针只需向空闲内存区域移动指定大小的距离。
- 空闲列表(Free List):如果堆内存是非规整的,JVM需要维护一个空闲列表,分配内存时从空闲列表中找到合适的块。
- 初始化零值
- JVM会将对象的所有实例变量初始化为默认值。例如,数值类型变量会被初始化为0,布尔类型变量初始化为false,引用类型变量初始化为null。
- 设置对象头
- 在对象的内存空间中设置对象头信息,这包括对象的哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。
- 构造方法初始化
- 调用对象的构造函数来完成对象的初始化工作。这包括执行实例变量的显式初始化操作以及构造方法体中的代码。具体步骤如下:
- 执行类的实例初始化块。
- 按照继承层次从上到下执行父类的构造方法。
- 初始化实例变量为显式指定的值。
- 执行类的构造方法的主体部分。
- 调用对象的构造函数来完成对象的初始化工作。这包括执行实例变量的显式初始化操作以及构造方法体中的代码。具体步骤如下:
/**
* 创建对象的示例代码
*/
public class MyClass {
private int value;
public MyClass(int value) {
this.value = value;
}
public static void main(String[] args) {
MyClass obj = new MyClass(10);
}
}
- 上面代码示例的执行过程如下:
- 类加载:JVM检查 MyClass 是否已加载。如果未加载,则加载 MyClass 类。
- 内存分配:在堆中为 MyClass 的新实例分配内存。
- 内存初始化:将分配的内存初始化为默认值。
- 设置对象头:在对象头中设置元数据。
- 构造方法初始化:执行 MyClass 的构造方法,初始化实例变量 value 为 10。