🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,
15年
工作经验,精通Java编程
,高并发设计
,Springboot和微服务
,熟悉Linux
,ESXI虚拟化
以及云原生Docker和K8s
,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
技术合作请加本人wx(注明来自csdn):foreast_sea
Java虚拟机面试题:内存管理(上)
1. 能说一下 JVM 的内存区域吗?
按照 Java 的虚拟机规范,JVM 的内存区域(JVM 的内存结构/JVM 运行时数据区)可以细分为程序计数器
、虚拟机栈
、本地方法栈
、堆
、方法区
等。
其中方法区
和堆
是线程共享的,虚拟机栈
、本地方法栈
和程序计数器
是线程私有的。
介绍一下程序计数器?
程序计数器(Program Counter Register
)也被称为 PC 寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码行号指示器。
介绍一下 Java 虚拟机栈?
Java 虚拟机栈(Java Virtual Machine Stack
),通常指的就是“栈”,它的生命周期与线程相同。
当线程执行一个方法时,会创建一个对应的栈帧
,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,然后栈帧会被压入栈中。当方法执行完毕后,栈帧会从栈中移除。
一个什么都没有的空方法,完全空的参数什么都没有,那局部变量表里有没有变量?
对于静态方法,由于不需要访问实例对象(this),因此在局部变量表中不会有任何变量。
对于非静态方法,即使是一个完全空的方法,局部变量表中也会有一个用于存储 this 引用的变量。this 引用指向当前实例对象,在方法调用时被隐式传入。
比如说有这样一段代码:
public class VarDemo1 {public void emptyMethod() {// 什么都没有}public static void staticEmptyMethod() {// 什么都没有}
}
用 javap -v VarDemo1
命令查看编译后的字节码:
在非静态方法 emptyMethod 的输出中,你会看到类似这样的内容:
这里的 locals=1 表示局部变量表有一个变量,即 this,Slot 0 位置存储了 this 引用。
而在静态方法 staticEmptyMethod
的输出中,你会看到类似这样的内容:
这里的 locals=0 表示局部变量表为空,因为静态方法没有 this 引用,也没有其他局部变量。
介绍一下本地方法栈?
本地方法栈(Native Method Stacks
)与虚拟机栈相似,区别在于虚拟机栈是为 JVM 执行 Java 编写的方法服务的,而本地方法栈是为 Java 调用本地(native)方法
服务的,由 C/C++ 编写。
在本地方法栈中,主要存放了 native 方法的局部变量、动态链接和方法出口等信息。当一个 Java 程序调用一个 native 方法时,JVM 会切换到本地方法栈来执行这个方法。
介绍一下本地方法栈的运行场景?
当 Java 应用需要与操作系统底层或硬件交互时,通常会用到本地方法栈。
比如调用操作系统的特定功能,如内存管理、文件操作、系统时间、系统调用等。
举例:System.currentTimeMillis()
就是调用本地方法来获取操作系统的当前时间。
再比如 JVM 自身的一些底层功能也需要通过本地方法来实现。像 Object 类中的 hashCode()
方法、clone()
方法等。
native 方法解释一下?
Native 方法是在 Java 中通过 native 关键字声明的,用于调用非 Java 语言(如 C/C++)编写的代码。Java 可以通过 JNI(Java Native Interface)与底层系统、硬件设备、或高性能的本地库进行交互。
介绍一下 Java 堆?
堆是 JVM 中最大的一块内存区域,被所有线程共享,在 JVM 启动时创建,主要用来存储对象的。
Java 中“几乎”所有的对象都会在堆中分配,堆也是垃圾收集器
管理的目标区域,因此一些资料中也会把 Java 堆称作“GC 堆”(Garbage Collected Heap
)。
从内存回收的角度来看,由于垃圾收集器大部分都是基于分代收集理论设计的,所以堆也会被划分为新生代
、老年代
、Eden空间
、From Survivor空间
、To Survivor空间
等。
但随着JIT 编译器
的发展和逃逸技术的逐渐成熟,“所有的对象都会分配到堆上”就不再那么绝对了。
从 JDK 7 开始,JVM 已经默认开启逃逸分析了,意味着如果某些方法中的对象引用没有被返回或者未被方法体外使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
堆和栈的区别是什么?
堆属于线程共享的内存区域,几乎所有的对象都在堆上分配,生命周期不由单个方法调用所决定,可以在方法调用结束后继续存在,直到不再被任何变量引用,然后被垃圾收集器回收。
栈属于线程私有的内存区域,主要存储局部变量、方法参数、对象引用等,通常随着方法调用的结束而自动释放,不需要垃圾收集器处理。
介绍一下方法区?
方法区并不真实存在,属于 Java 虚拟机规范中的一个逻辑概念,用于存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。
在 HotSpot 虚拟机中,方法区的实现称为永久代(PermGen),但在 Java 8 及之后的版本中,已经被元空间(Metaspace)所替代。
变量存在堆栈的什么位置?
对于局部变量来说,它存储在当前方法的栈帧中的局部变量表中。当方法执行完毕,栈帧被回收,局部变量也会被释放。
public void method() {int localVar = 100; // 局部变量,存储在栈帧中的局部变量表里
}
对于静态变量来说,它存储在 Java 规范中的方法区中,也就是元空间(Metaspace)。
public class StaticVarDemo {public static int staticVar = 100; // 静态变量,存储在方法区中
}
2. 说一下 JDK1.6、1.7、1.8 内存区域的变化?
JDK1.6、1.7/1.8 内存区域发生了变化,主要体现在方法区的实现:
- JDK1.6 使用永久代实现方法区:
- JDK1.7 时发生了一些变化,将字符串常量池、静态变量,存放在堆上
- 在 JDK1.8 时彻底干掉了永久代,而在直接内存中划出一块区域作为元空间,运行时常量池、类常量池都移动到元空间。
3. 为什么使用元空间替代永久代作为方法区的实现?
Java 虚拟机规范规定的方法区只是换种方式实现。有客观和主观两个原因。
-
客观上使用永久代来实现方法区的决定的设计导致了 Java 应用更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize 的上限,即使不设置也有默认大小,而 J9 和 JRockit 只要没有触碰到进程可用内存的上限,例如 32 位系统中的 4GB 限制,就不会出问题),而且有极少数方法 (例如 String::intern())会因永久代的原因而导致不同虚拟机下有不同的表现。
-
主观上当 Oracle 收购 BEA 获得了 JRockit 的所有权后,准备把 JRockit 中的优秀功能,譬如 Java Mission Control 管理工具,移植到 HotSpot 虚拟机时,但因为两者对方法区实现的差异而面临诸多困难。考虑到 HotSpot 未来的发展,在 JDK 6 的 时候 HotSpot 开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了 JDK 7 的 HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终于完全废弃了永久代的概念,改用与 JRockit、J9 一样在本地内存中实现的元空间(Meta-space)来代替,把 JDK 7 中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
4. 对象创建的过程了解吗?
当我们使用 new 关键字创建一个对象的时候,JVM 首先会检查 new 指令的参数是否能在常量池中定位到一个类的符号引用,然后检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就先执行相应的类加载过程。
如果已经加载,JVM 会为新生对象分配内存,内存分配完成之后,JVM 将分配到的内存空间初始化为零值(成员变量,数值类型是 0,布尔类型是 false,对象类型是 null),接下来设置对象头,对象头里包含了对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。
最后,JVM 会执行构造方法(<init>
),将成员变量赋值为预期的值,这样一个对象就创建完成了。
对象的销毁过程了解吗?
对象创建完成后,就可以通过引用来访问对象的方法和属性,当对象不再被任何引用指向时,对象就会变成垃圾。
垃圾收集器会通过可达性分析算法判断对象是否存活,如果对象不可达,就会被回收。
垃圾收集器会通过标记清除、标记复制、标记整理等算法来回收内存,将对象占用的内存空间释放出来。
常用的垃圾收集器有 CMS、G1、ZGC 等,它们的回收策略和效率不同,可以根据具体的场景选择合适的垃圾收集器。