文章目录
- 1、说下对JVM内存模型的理解
- 2、运行时常量池的位置
- 3、常量池和运行时常量池的区别
- 4、内存溢出和内存泄漏
- 5、Java 对象大小计算
- 6、GCROOT都有什么
- 7、常用的JVM启动参数有哪些
- 8、TLAB
1、说下对JVM内存模型的理解
1)线程私有区域
- 程序计数器(Program Counter Register)
- 作用:记录
当前线程正在执行的字节码指令地址
。 - 存储内容:当前线程所执行的字节码指令地址。每个线程都有一个独立的程序计数器,用于线程切换后能恢复正确的执行位置。
- 作用:记录
- Java 虚拟机栈(Java Virtual Machine Stack)
- 作用:管理线程的方法调用和执行。
- 存储内容:栈帧(Stack Frame),包含
局部变量表
、操作数栈
、动态链接
、方法出口
等信息。每个方法调用都会创建一个栈帧
。
- 本地方法栈(Native Method Stack)
- 作用:管理本地(Native)方法的调用。
- 存储内容:与 Java 虚拟机栈类似,但用于本地方法的执行。
2)线程共享区域
- 堆(Heap)
- 作用:存储所有对象实例和数组。
- 存储内容:所有对象实例和数组。
堆是垃圾收集的主要区域
。
- 方法区(Method Area)
- 作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
- 存储内容:类信息(元数据)、常量池、静态变量、JIT 编译后的代码。
- 元空间(Metaspace):JDK 8 之后,方法区改为使用
本地内存
的元空间来存储类元数据。 - 运行时常量池(Runtime Constant Pool)
- 作用:存储编译期生成的各种字面量和符号引用。
- 存储内容:字面量、符号引用。这部分内容是方法区的一部分,但因为其重要性,通常单独列出。JDK 8 及之后挪到了堆中。
2、运行时常量池的位置
- JDK 7 之前:
运行时常量池位于永久代
中。
永久代存储类元数据、常量池、静态变量、类方法等。 - JDK 7:
运行时常量池从永久代移到了堆
中。
永久代依然存在,用于存储类元数据和其他信息,但常量池不再存放在永久代中。 - JDK 8 及之后:
方法区被移除,取而代之的是元空间(Metaspace)。
元空间使用本地内存
存储类元数据(如类名、方法、字段等),不再受限于堆大小。
运行时常量池依然位于堆中,不在元空间内。
3、常量池和运行时常量池的区别
常量池和运行时常量池是 Java 中两个相关但不同的概念,它们分别用于不同的阶段和存储不同类型的数据。
1)常量池
(Constant Pool)
常量池是在编译期
生成的,是 Class 文件的一部分
。每个 Class 文件都包含一个常量池,存储了编译期间生成的各种字面量和符号引用。
特点和存储内容
- 位置:Class 文件中。
- 存储内容:
字面量(如整数、浮点数、字符串常量)。
符号引用(如类名、方法名、字段名、接口方法名)。
作用:用于支持 Java 虚拟机在类加载时和运行时对这些字面量和符号引用的解析。
2)运行时常量池
(Runtime Constant Pool)
运行时常量池是从常量池加载而来的,是每个类或接口在加载后
存储在 JVM 内存中的一部分。运行时常量池存在于方法区中(在 JDK 8 之后,方法区被元空间取代,但运行时常量池仍然位于堆内存
中)。
特点和存储内容
- 位置:方法区的一部分(JDK 8 之后为堆内存)。
- 存储内容:
从 Class 文件的常量池中加载的字面量和符号引用。
动态生成的常量(如字符串池中的字符串常量)。 - 作用:在运行时为 JVM 提供字面量和符号引用的解析与存储支持,允许在运行时添加新的常量。
主要区别:生成时机、位置、内容、作用不同
4、内存溢出和内存泄漏
内存溢出(OutOfMemory)和内存泄漏(Memory Leak)是两种常见的内存管理问题,它们在性质、原因和影响方面有所不同。
1)内存溢出
定义:内存溢出是指程序在运行过程中,申请内存时没有足够的内存可用,导致 JVM 或操作系统抛出内存不足的错误。
原因
- 堆内存溢出:程序创建了太多对象,导致堆内存耗尽。例如,使用了一个大集合而没有适时清除无用对象。
- 方法区溢出:加载了过多的类或方法,导致方法区(元空间)耗尽。这在频繁动态生成类的应用(如使用大量代理或反射)中常见。
- 栈内存溢出:递归调用过深或方法调用栈过大,导致栈内存耗尽。例如,递归没有适时结束。
表现
- 堆内存溢出:JVM 抛出 java.lang.OutOfMemoryError: Java heap space 错误。
- 方法区溢出:JVM 抛出 java.lang.OutOfMemoryError: Metaspace(JDK 8 及以后)或 java.lang.OutOfMemoryError: PermGen space(JDK 7 及以前)错误。
- 栈内存溢出:JVM 抛出 java.lang.StackOverflowError 或 java.lang.OutOfMemoryError: unable to create new native thread 错误。
解决方法
- 优化代码:避免创建过多的对象,使用更有效的数据结构。
- 调整 JVM 参数:增加堆内存(-Xmx),增加元空间大小(-XX:MaxMetaspaceSize)。
- 改进算法:减少递归深度,使用循环替代深度递归。
2)内存泄漏
定义:内存泄漏是指程序在运行过程中,已经不再使用的对象或资源仍然被引用,导致这些对象或资源无法被垃圾收集器回收,逐渐耗尽内存。
原因
- 长生命周期对象引用短生命周期对象:如静态集合类持有对动态创建对象的引用,导致动态对象不能被回收。
- 未正确关闭资源:如数据库连接、文件流未关闭。
- 事件监听和回调:注册的事件监听器或回调未被适时移除,导致对象无法被回收。
表现
- 内存使用持续增加:应用程序在运行过程中,内存使用持续增加,直至耗尽内存。
- 性能下降:随着内存使用增加,GC 的频率和时间增加,导致性能下降。
解决方法
- 检查引用:确保不再使用的对象不再被引用。
- 及时关闭资源:使用 try-with-resources 语句或在 finally 块中关闭资源。
- 移除监听器:在不再需要时,移除注册的事件监听器或回调。
5、Java 对象大小计算
计算 Java 对象的大小涉及对象头、实例数据和对齐填充。以下是具体的计算方法:
1)对象头(Object Header):
- Mark Word:32 位 JVM 中占 4 字节,64 位 JVM 中占 8 字节。
- Class Pointer:32 位 JVM 中占 4 字节,64 位 JVM 中占 8 字节(使用压缩指针时为 4 字节)。
2)实例数据(Instance Data):
- 对象的字段数据。基本类型字段的大小如:int 4 字节,byte 1 字节。引用类型字段在 32 位 JVM 中占 4 字节,64 位 JVM 中占 8 字节(使用压缩指针时为 4 字节)。
3)对齐填充(Padding):
- 为满足对象大小是 8 字节的倍数,JVM 可能会在对象结尾添加填充字节。
示例:
public class ComplexObject {
int intField; // 4 字节
double doubleField; // 8 字节
Object refField; // 4 字节(压缩指针)
byte byteField; // 1 字节
}
计算步骤:
- 对象头:64 位 JVM 使用压缩指针时为 16 字节(8 字节 Mark Word + 8 字节 Class Pointer)。
- 实例数据:intField 4 字节,doubleField 8 字节,refField 4 字节,byteField 1 字节。
- 对齐填充:当前大小为 16 + 4 + 8 + 4 + 1 = 33 字节,需填充 7 字节。
总大小:16 + 4 + 8 + 4 + 1 + 7 = 40 字节。
⭐️总结
- 对象头:64 位 JVM 压缩指针时为 16 字节。
- 实例数据:基本类型按大小分配,引用类型使用压缩指针为 4 字节。
- 对齐填充:确保总大小为 8 字节的倍数。
6、GCROOT都有什么
在 Java 中,GCROOT(垃圾回收根)是指可以直接或间接引用到活动对象的引用链的起点。GC ROOT 对象是 GC 不会回收的对象,因为它们在程序执行期间仍然可访问。
GC ROOT 可能包括以下几种类型的对象:
静态变量(Static Variables):类的静态变量引用的对象。
本地变量(Local Variables):当前活动线程的栈帧中的局部变量引用的对象。
活动线程(Active Threads):尚未终止的线程引用的对象。
JNI 引用(JNI References):由本地方法(Native Method)引用的 Java 对象。
虚拟机内部引用(JVM Internal References):由虚拟机自身引用的对象,如常量池、类的引用等。
7、常用的JVM启动参数有哪些
JVM (Java虚拟机)的启动参数用于配置和调整Java应用程序的运行时行为。以下是一些常用的JVM启动参数:
- .-Xmx: 指定Java堆内存的最大限制。例如,-Xmx512m 表示最大堆内存为512兆字节
- -Xms: 指定Java堆内存的初始大小。例如,-Xms256m 表示初始堆内存为256兆字节
- -Xss: 指定每个线程的堆栈大小。例如,-Xss256k 表示每个线程的堆栈大小为256千字节。
- -XX:MaxPermSize (对于Java 7及之前的版本)或-XX:MaxMetaspaceSize(对于Java 8及以后的版本): 指定永久代 ava 7及之前)或元空间 (Java8及以后) 的最大大小。
- -XX:PermSize(对于Java 7及之前的版本)或 -XXMetaspaceSize (对于Java 8及以后的版本): 指定永久代 Java 7及之前)或元空间 (Java 8及以后) 的初始大小。
- -Xmn: 指定年轻代的大小。例如,-Xmn256m 表示年轻代大小为256兆字节,
- -XX:SurivorRatio: 指定年轻代中Eden区与Survivor区的大小此例。例如-XX:SuvivorRatio=8 表示Eden区与每个Surivor区的大小比例为8:1.
- -XX:NewRatio: 指定年轻代与老年代的大小比例。例如,-XX:NewRatio=2 表示年轻代和老年代的比例为1:2.
- -XX:MaxGCPauseMilis: 设置垃圾回收的最大暂停时间目标。例如,-XX:MaxGCPauseMilis=100 表示垃圾回收的最大暂信时间目标为100毫秒
- -XX:ParallelGCThreads: 指定并行垃圾回收线程的数量。例如,-XX:ParallelGCThreads=4 表示使用4个线程进行并行垃圾回收,
- -XX:+UseConcMarkSweepGC: 启用并发标记清除垃圾回收器。
- -XX:+UseG1GC: 启用G1 (Garbage First) 垃圾回收器。
- -Dproperty=value: 设置Java系统属性,可以在应用程序中使用 System.getProperty"property")来获取这些属性的值。这些是一些常见的JM启动参数,可以根据应用程序的需和性能调优的目标进行调整。JVM启动参数的使用可以显著影响应用程序的性能和行为,因此在设置这些参数时需要谨慎。同时,JVM支持的启动参数因不同的JVM版本和供应商而有所不同,建议查阅相关文档以获取更详细的信息。
8、TLAB
Thread Local AllocationBuffer,线程本地分配缓冲区。
1)原理:
在多线程并发情况下,对象的分配会存在竞争,多个线程同时申请内存可能会导致频繁的加锁和解锁操作,从而降低分配效率。TLAB 的出现就是为了解决这个问题。
TLAB 为每个线程分配了一块独立的内存区域,每个线程在自己的 TLAB 区域内进行对象的分配,不再涉及到全局的内存分配锁,从而避免了多线程之间的竞争。
2)TLAB 的优点包括:
- 降低内存分配竞争:每个线程有自己的 TLAB,避免了多线程之间的内存分配竞争,提高了分配效率。
- 减少同步开销:减少了因对象分配而引起的同步开销,提高了多线程并发时的性能。
- 提高缓存局部性:TLAB 中的对象是连续分配的,有利于提高 CPU 缓存的命中率。