4356 字
6 分钟
3 次
- 2025-10-23
JVM的内存模型介绍一下
程序计数器记录执行位置,线程私有,不会 OOM;
虚拟机栈和本地方法栈管理方法调用,其中本地方法栈是为 Native 方法服务,线程私有,可能栈溢出;
堆是最大的一块内存区域,是共享区,用于存放对象实例,会 OOM;
元空间在本地内存,存已被虚拟机加载的类信息、常量、静态变量等数据,也会 OOM;
运行时常量池是元空间的一部分,用于存放编译期生产的字面量和符号引用,会 OOM。
另外,还有直接内存,通过 NIO 使用,不归 JVM 管,但也可能 OOM。
OMM:Out Of Memory,来源为 OutOfMemoryError。直译为内存用完了,意思是 JVM 没有足够的内存来为对象分配空间且垃圾回收器没有空间可回收时,会抛出错误
JVM内存模型里的堆和栈有什么区别?
堆和栈的主要区别在用途和生命周期:
栈是线程私有的,用来执行方法,存储局部变量和调用信息,这些信息在方法结束时就释放,速度快,空间小,但可能出现栈溢出;
堆是线程共享的,用来存放对象实例,生命周期由垃圾回收机制控制,空间大但速度慢,对象不再引用时才会被回收。
简单说:栈管方法执行,堆管对象存储。
栈中存的到底是指针还是对象?
栈中不存储对象本身,只存储基本类型变量和对象的引用,对象实例本身是在堆上分配的。
堆分为哪几部分呢?
Java 堆主要分为新生代和老年代。
新生代又分为 Eden 区和 Survivor 区,新对象通常在 Eden 区分配,经历 Minor GC (新生代垃圾回收)后存活的进入 Survivor 区。
经过多次回收仍存活的对象会移动到老年代。老年代存放长期存活对象,发生 Major GC 时才会回收。
除此之外呢,还有元空间和大对象区。 元空间用于存储类的元数据信息,如类的结构信息(如字段、方法信息等)。
对于需要大量连续内存空间的对象,例如大数组会被直接分配在老年代或专门的大对象区。
如果有个大对象一般是在哪个区域?
大对象通常会直接分配到老年代。
大对象通常需要连续的内存空间,如果在新生代中频繁分配和回收大对象,容易产生内存碎片,导致后续分配大对象时可能因为内存不连续而失败。老年代的空间相对较大,更适合存储大对象,有助于减少内存碎片的产生。
程序计数器的作用,为什么是私有的?
程序计数器的作用是记录当前线程所执行的字节码指令的地址,但每个线程都有自己的执行路径,切换线程时需要独立保存执行位置,确保恢复时能继续正确执行,所以需要线程私有。
方法区中的方法的执行过程?
当调用一个方法时,JVM 会在 Java 虚拟机栈中为它创建一个栈帧,里面包含局部变量表、操作数栈等;然后解释器执行该方法的字节码指令;执行完毕后,可能会返回一个结果给调用者,清理当前栈帧,恢复调用者的执行环境。
方法区中还有哪些东西?
- 类信息:包括类的结构信息、类的访问修饰符、父类与接口等信息。
- 常量池:存储类和接口中的常量,包括字面值常量、符号引用,以及运行时常量池。
- 静态变量:存储类的静态变量,这些变量在类初始化的时候被赋值。
- 方法字节码:存储类的方法字节码,即编译后的代码。
- 符号引用:存储类和方法的符号引用,是一种不同于直接引用的引用类型。
- 运行时常量池:存储着在类文件中的常量池数据,在类加载后在方法区生成该运行时常量池。
- 常量池缓存:用于提升类加载的效率,将常用的常量缓存起来方便使用。
String保存在哪里呢?
String 保存在字符串常量池中,不同于其他对象,它的值是不可变的,且可以被多个引用共享。
String s = new String(“abc”)执行过程中分别对应哪些内存区域?
如果abc这个字符串常量不存在,则创建两个对象,分别是abc这个字符串常量,以及new String这个实例对象。如果abc这字符串常量存在,则只会创建一个对象。
引用类型有哪些?有什么区别?
强引用:最常见,只要引用存在就不会被回收;
软引用:内存不足时才回收,适合做缓存; SoftReference
弱引用:只要发生GC就被回收,常用于避免内存泄漏; WeakReference
虚引用:最弱,必须配合队列使用,主要用于跟踪对象是否被回收,比如管理堆外内存。 PhantomReference + ReferenceQueue
弱引用了解吗?举例说明在哪里可以用?
弱引用通过 WeakReference 实现,只要发生 GC,弱引用对象都会被回收。
常用于缓存或避免内存泄漏,比如 ThreadLocal 的 Key 就使用弱引用,防止线程长时间运行时 Key 无法释放。
内存泄漏和内存溢出的理解?
内存泄漏是指无用对象被引用,无法被GC回收,导致可用内存逐渐减少,比如给静态集合一直添加对象等。
内存溢出是 JVM 申请内存时,无法找到足够的内存,触发 OutOfMemoryError,比如创建大量对象或递归太深。
内存泄漏不一定会立刻报错,但长期积累会引发内存溢出。
jvm 内存结构有哪几种内存溢出的情况?
JVM 常见的内存溢出有四种:
- 堆溢出:
Java heap space,存在大对象分配或内存泄漏; - 栈溢出:
StackOverflowError,递归太深;或线程太多导致无法创建新栈; - 元空间溢出:
Metaspace,加载类过多,如动态生成类; - 直接内存溢出:
Direct buffer memory,NIO 使用堆外内存过多。
遇到过堆溢出的情况吗?如何解决?
- 捕获内存快照:通过 JVM 参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof,让程序在发生 OOM 时自动生成堆快照文件。 - 分析快照文件:使用 MAT(Memory Analyzer Tool)或 JProfiler 等工具分析快照,重点看哪些对象占用了大量内存、是否存在内存泄漏(如对象长期被无用引用持有,无法回收)。
栈溢出的情况呢?
- 排查递归逻辑:检查是否存在无限递归或递归层级过深的问题,添加正确的终止条件,或减少递归深度。必要时可将递归改写为迭代(如用循环替代),因为迭代不会持续创建新栈帧。
- 调整栈内存大小:通过 JVM 参数
-Xss(如-Xss256k)增大栈内存容量。但这种方式要谨慎,栈内存过大会导致线程可创建数量减少(总内存固定时,单个线程栈越大,能创建的线程数越少)。 - 优化方法栈帧:减少方法内局部变量的数量,避免在方法中创建过大的对象或数组,将大对象的创建移到堆中(通过 new 关键字),降低单个栈帧的内存占用。
有具体的内存泄漏和内存溢出的例子么请举例及解决方案?
不会
创建对象的过程?
- 遇到
new指令时,先检查类是否在常量池中定位到一个类的符号引用,并且这个引用所代表的类是否已被加载,否则先执行类加载过程; - JVM 在 Java 堆中为对象分配内存;
- 将分配的内存空间都初始化为零值;
- 设置对象头,如类的元数据信息在哪,是哪个类的实例、哈希码、GC 分代年龄等;
- 最后执行构造函数
<init>,真正初始化对象。
对象的生命周期
- 创建:对象通过关键字new在堆内存中被实例化,构造函数被调用,对象的内存空间被分配。
- 使用:对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
- 销毁:当对象不再被引用时,通过垃圾回收机制自动回收对象所占用的内存空间,完成对象的销毁过程。
类加载器有哪些?
JVM 有四种类加载器:
启动类加载器:负责加载 java的核心库,C++ 实现,是 JVM 的一部分,不能 Java 程序直接使用;
扩展类加载器:加载 Java 扩展目录下的 jar 包和类库,Java 语言实现,继承自 ClassLoader 类;
系统类加载器:加载用户类 Classpath 下的类库,是我们写代码时默认使用的类加载器,父类是扩展类加载器;
自定义类加载器:开发者可继承 ClassLoader 实现,用于从网络、数据库等加载类。
Java 中 双亲委派 是什么?有啥用?
双亲委派是指:当类加载器收到加载请求时,先委派给父加载器去加载,只有父加载器无法完成时,自己才尝试加载。
它保证了像 String 这样的核心类不会被自定义类替换,确保了系统的安全性和类的唯一性。
加载顺序是:自定义 → 应用 → 扩展 → 启动类加载器。这是JVM类加载的基石。
双亲委派模型的作用
双亲委派的核心作用有三点:
第一,保证类的唯一性,确保了所有加载请求都会传递到启动类加载器,避免重复加载;
第二,防止核心类被篡改,启动类加载器只加载信任的类路径中的类,这样可以防止不可信的类假冒核心类;
第三,清晰层次划分,不同类型的类都被不同的加载器加载,确保各个层级类加载器的职责清晰。
讲一下类加载过程?
类加载过程分为五个阶段:
-
加载:找到
.class文件,生成Class对象,放到方法区; -
链接:
- 验证:进行文件格式校验、元数据验证、字节码验证、符号引用验证,确保字节码安全合法
- 准备:给静态变量分配内存,设默认值;
- 解析:虚拟机将常量池的「符号引用」直接替换为「直接引用」;
-
初始化:执行类的构造器方法,赋真实初始值,运行静态代码块。
类的卸载需要什么条件?
- 该类所有的实例都已经被回收。
- 加载该类的 ClassLoader 已经被回收。
- 类对应的 Java.lang.Class 对象没有任何地方被引用。
什么是 Java 里的垃圾回收?如何触发垃圾回收?
垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。
触发方式:
- 内存不足。
- 手动请求。
- 对象数量或内存使用达到阈值。
判断垃圾的方法有哪些?
判断垃圾主要有两种方法:
第一种是引用计数法,为对象分配一个引用计数器,对象被引用就+1,引用失效就 -1,为0 就可以被回收。
第二种是可达性分析算法,从一组垃圾收集根 GC Roots(如栈变量、静态变量)出发,向下追溯它们引用的对象,以及它们引用的其他对象,能访问到的对象是存活的,也就是可达的,访问不到的就是不可达的,就是垃圾。这是目前 JVM 使用的算法。
垃圾回收机制是什么,是为了解决了什么问题?
垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。
它主要是为了解决传统语言中内存泄漏和内存溢出的问题,避免程序员手动释放内存带来的错误。
垃圾回收算法有哪些?
常见的垃圾回收算法有四种:
标记-清除:通过可达性分析,标记需要回收的对象,然后统一清除,效率不高,且会产生碎片;
复制算法:将内存分为两块,每次申请内存就使用其中一块,内存不够时,将存活对象复制到另一块区域,然后清理已使用的内存,无碎片,但是内存利用率不足;
标记-整理:通过可达性分析,标记需要回收的对象,标记后把存活对象移动到内存的一端,移动结束后清理剩余部分,用于存活对象较多的情况;
分代回收:分代收集是将内存划分成了新生代和老年代,在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值后,如果对象还存活,那么该对象会进入老年代。新生代用复制,老年代用整理或清除。
垃圾回收器有哪些?
- Serial收集器,新生代单线程收集器,复制算法
- ParNew收集器,新生代收并行集器,复制算法
- Parallel Scavenge收集器,新生代并行收集器,复制算法
- Serial Old收集器,老年代单线程收集器,标记-整理算法
- Parallel Old收集器,老年代并行收集器,标记-清除算法
- CMS(Concurrent Mark Sweep)收集器,老年代并行收集器,标记-清除算法
- G1(Garbage First)收集器, Java堆并行收集器,标记-整理算法
标记清除算法的缺点是什么?
- 标记和清除过程的效率都不高
- 标记清除之后会产生大量不连续的内存碎片,碎片太多在分配大对象时,可能会由于无法找到最够的连续内存而不得不提前一次 GC 动作
垃圾回收算法哪些阶段会stop the world?
先不记。
minorGC、majorGC、fullGC的区别,什么场景触发full GC
Minor GC 回收新生代,Eden区满时触发,频繁但快;
Major GC 主要回收老年代,老年代空间不足时发生;
Full GC 回收整个堆和元空间,最耗时。
触发 Full GC 的常见场景有:
- Minor GC 后对象进不了老年代;
- 老年代或元空间满了;
- 手动调用
System.gc()。
应尽量避免 Full GC,防止系统卡顿。
垃圾回收器 CMS 和 G1的区别?
- 使用范围:CMS 是老年代收集器;G1新生代和老年代都收集。
- STW 时间:CMS 是以最小停顿时间为目标的收集器,G1 收集器可预测垃圾回收的停顿时间。
- 垃圾碎片:CMS 使用标记-清除算法,G1 是标记-整理算法。
什么情况下使用CMS,什么情况使用G1?
CMS:
- 低延迟需求。
- 老年代手记。
- 碎片化管理。
G1:
- 大堆内存。
- 对内存碎片敏感。
- 比较平衡的性能。
G1回收器的特色是什么?
G1回收器的核心特色有四点:
第一,采用分区设计,弱化分代概念;
第二,使用标记-整理算法,不会产生内存碎片;
第三,支持停顿时间可控,可设定最大暂停时间;
第四,并发并行,充分利用多核优势。
GC只会对堆进行GC吗?
堆和方法区。