JVM组成
# 什么是JVM,为什么要使用
- jvm指的是java虚拟机,本质上是一个运行在计算机上的程序,他的职责就是运行java字节码文件,作用是为了支持跨平台特性
- jvm的功能有三项:第一是负责将字节码 解释 成当前操作系统能执行的机器指令,从而实现跨平台运行,实现一次编译到处运行;第二是管理内存中对象的分配,完成自动的垃圾回收,第三是优化热点代码提升执行效率
- jvm的组成分为类加载子系统、运行时数据区、执行引擎、本地方法接口
- 常用jvm是Oracle提供的hotspot虚拟机,还有graalVM、龙井、OpenJ9
# 字节码文件的组成
https://blog.csdn.net/m0_71386740/article/details/140822318
字节码是java虚拟机执行的一种指令格式
字节码文件本质上是二进制文件,无法直接打开,需要用专业的工具
- 开发环境中用jclasslib插件
- 服务器环境使用javap -v命令
五个组成部分
- 基本信息:魔数、字节码文件对应的java版本号、访问标识、父类和接口
- 字符串常量池:字符串常量、类和接口名、字段名,主要在字节码指令中使用
- 字段:当前类或接口声明的字段信息
- 方法:当前类或接口声明的方法信息,包含字节码指令
- 属性:类的
# 说一下 JVM 运行时数据区
组成部分:堆、方法区、栈、本地方法栈、程序计数器
1、堆解决的是对象实例存储的问题,垃圾回收器管理的主要区域。
2、方法区可以认为是堆的一部分,用于存储已被虚拟机加载的信息,常量、静态变量、即时编译器编译后的代码。
3、栈解决的是程序运行的问题,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息。
4、本地方法栈与栈功能相同,本地方法栈执行的是本地方法,一个Java调用非Java代码的接口。
5、程序计数器(PC寄存器)程序计数器中存放的是当前线程所执行的字节码的行数。JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。
# Java堆
- 线程共享的区域:主要用来保存对像实例,数组等,内存不够则抛出OutOfMemoryError异常.
- 组成:年轻代+老年代
- 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
- 老年代主要保存生命周期长的对像,一般是一些老的对象
- Jdk1.7和1.8的区别
- 1.7中有有一个永久代,存储的是类信息、静态变量、常量、编译后的代码
- 18移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出
- 元空间保存的类信息、静态变量、常量、编译后的代码
# jvm为什么使用元空间代替永久代
在 Java 7 及之前的版本中,HotSpot 虚拟机 使用 永久代(PermGen) 来实现 方法区(Method Area),主要用于存储运行时常量池,class类元信息等等,
永久代属于 JVM 运行时数据区 的一部分,可以通过 -XX:MaxPermSize
设置其大小。当内存不够的时候,就会触发垃圾回收,且由于其内存固定,当加载的类过多时,容易触发 OutOfMemoryError: PermGen space
错误。
在 Java 8 中,HotSpot 移除了 永久代,改用 元空间(Metaspace) 来存储方法区的数据。元空间 不在 JVM 堆内存中,而是直接使用 本地内存(Native Memory),默认情况下不受 JVM 内存限制,可以无限制使用本地内存(但也可通过参数 -XX:MaxMetaspaceSize
设置上限)。因此不需要考虑GC的一个问题
为什么使用元空间来替换永久代
永久代大小固定(
-XX:MaxPermSize
),而 JVM 加载的类数量难以预估,容易导致 OOM(PermGen space)。元空间使用本地内存,理论上仅受系统可用内存限制,大大降低了 OOM 风险。
永久代的回收依赖 Full GC(与老年代一起回收),导致 STW(Stop-The-World)时间较长,影响应用性能。
元空间的垃圾回收独立于堆,由 Metaspace GC 管理,可以 并发清理无用的类元数据,减少 Full GC 频率,提升 GC 效率。
Oracle 收购 Sun 后,合并了 HotSpot 和 JRockit 的代码,而 JRockit 没有永久代,采用类似元空间的机制。为了统一 JVM 实现,HotSpot 也改用 元空间,使 JVM 架构更加一致。
# 什么是虚拟机栈
每个线程运行时所需要的内存,称为虚拟机栈 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
# 垃圾回收是否涉及栈内存?
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放
# 栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k,而机器总内存是固定的,单个栈内存过大会导致线程数变少
# 方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
# 什么情况下会导致栈内存溢出?
栈帧过多导致栈内存溢出,典型问题:递归调用 栈帧过大导致栈内存溢出
# 堆栈的区别是什么?
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储)ava对象和数组的的。堆会GC垃圾回收,而栈不会。
- 栈内存是线程私有的,而堆内存是线程共有的。
- 栈内存或者堆内存不足都会抛出异常,但两者异常错误不同。
- 栈空间不足:java.lang.StackOverFlowError..
- 堆空间不足:java.lang.OutOfMemoryError.
为什么说是几乎所有对象实例都存在于堆中呢? 这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存
# 基本数据类型存放在栈中是一个常见的误区!
基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆/方法区/元空间中。
# 能不能解释一下方法区
方法区(Method Area)是各个线程共享的内存区域
主要存储类的信息、运行时常量池
虚拟机启动的时候创建,关闭虚拟机时释放
如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError:Metaspace
# 运行时常量池
常量池:可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
# OOM怎么解决
当遇到 OOM 时,我会首先明确是哪种内存区域的 OOM。如果是堆内存 OOM,我会检查代码是否有内存泄漏,并使用工具分析堆转储文件。同时,我会调整 JVM 参数,增加堆内存大小。如果是元空间 OOM,我会检查是否有过多的类加载,并调整元空间大小。对于栈内存 OOM,我会优化线程使用,并调整线程栈大小。最后,我会通过监控工具实时监控 JVM 内存使用情况,确保问题不再发生。
加分项
- 提到具体的工具(如 MAT、VisualVM、JProfiler)。
- 提到具体的 JVM 参数(如
-Xmx
、-Xms、-XX:PrintGCDetails
、-XX:HeapDumpOnOutOfMomoryError
)。 - 提到如何预防 OOM(如代码优化、监控)。
# jdk6-8内存区域的不同
jdk6堆中有方法区,方法区有永久代实现,字符串常量池在方法区中
jdk7字符串常量池移到堆中
jdk8去掉永久代,改用元空间,元空间在直接内存中,
# 一个空的object对象占多大内存空间
Java对象在堆内存中的存储结构分为三部分:
对象头(Header)
Mark Word(8字节):存储运行时数据(哈希码、GC分代年龄、锁状态等)
Klass Pointer(类指针):指向对象的类元数据
- 开启压缩指针(默认):4字节
- 关闭压缩指针:8字节
数组长度(当前是数组对象时存在):4字节(空
Object
不涉及)
实例数据(Instance Data):空对象无实例字段,占用0字节
对齐填充(Padding):为了避免伪共享的问题,需要确保对象大小是8字节的倍数
在开启了压缩指针的一个情况下呢,object默认会占用12个字节,但是为了避免伪共享的问题啊,JVM会按照八个字节的倍数去填充四个字节,变成16个字节的一个长度。在关闭压缩指针一个情况下呢,object默认会占16个字节,而16个字节呢正好是八的整数倍,所以不需要填充,因此一个object对象都只占用16个字节的空间
# 什么是伪共享
伪共享(False Sharing) 是多线程编程中的一种性能问题,指 多个线程同时修改位于同一缓存行(Cache Line)的不同变量,导致CPU缓存频繁失效,降低程序性能。
所以需要对齐填充,使java对象独占一个缓存行,避免缓存频繁失效