八股整理之JVM篇
根据 JDK 8 规范,JVM运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。程序计数器:可以看作是当前线程所执行的字节码的行号指示器,用于存储当前线程正在执行的 Java 方法的JVM 指令地址。如果线程执行的是 Native 方法,计数器值为 undefined(未定义)一一因为 native 方法由本
JVM的内存模型介绍一下
根据 JDK 8 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。

JVM的内存结构主要分为以下几个部分:
程序计数器:可以看作是当前线程所执行的字节码的行号指示器,用于存储当前线程正在执行的 Java 方法的JVM 指令地址。如果线程执行的是 Native 方法,计数器值为 undefined(未定义)一一因为 native 方法由本地代码实现,不再对应字节码指令。它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,生命周期与线程相同。
Java虚拟机栈:每个线程都有自己独立的Java虚拟机栈,生命周期与线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。可能会拋出 StackOverflowError 和 OutOfMemoryError 异常。
本地方法栈:与Java 虚拟机栈类似,主要为虚拟机使用到的Native方法服务,在HotSpot虚拟机中和Java 虚拟机栈合二为一。本地方法执行时也会创建栈帧,同样可能出现StackOverflowError 和OutOfMemoryError两种错误。
Java 堆:是VM中最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,用于存放对象实例。从内存回收角度,堆被划分为新生代和老年代,新生代又分为Eden 区和两个Survivor区(From Survivor和 To Survivor)。如果在堆中没有内存完成实例分配,并且堆也无法扩展时会拋出OutOfMemoryError异常。
方法区(元空间):在JDK1.8及以后的版本中,方法区被元空间取代,使用本地内存。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。虽然方法区被描述为堆的逻辑部分,但有“非堆”的别名。方法区可以选择不实现垃圾收集,内存不足时会抛出OutOfMemoryError 异常。
运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,具有动态性,运行时也可将新的常量放入池中。当无法申请到足够内存时,会抛出OutOfMemoryError 异常。
直接内存:不属于JVM 运行时数据区的一部分,通过NIO 类引入,是一种堆外内存,可以显著提高I/O性能。直接内存的使用受到本机总内存的限制,若分配不当,可能导致OutOfMemoryError 异常。
JVM内存模型里的堆和栈有什么区别?
用途:栈主要用于存储局部变量、方法调用的参数、方法返回地址以及一些临时数据。每当一个方法被调用,一个栈帧(stack frame)就会在栈中创建,用于存储该方法的信息,当方法执行完毕,栈帧也会被移除。
堆用于存储对象的实例(包括类的实例和数组)。当你使用new关键字创建一个对象时,对象的实例就会在堆上分配空间。
生命周期:栈中的数据具有确定的生命周期,当一个方法调用结束时,其对应的栈帧就会被销毁,栈中存储的局部变量也会随之消失。堆中的对象生命周期不确定,其对应的栈帧就会被销毁,栈中存储的局部变量也会随之消失。堆中的对象生命周期不确定,
存取速度:栈的存取速度通常比堆快,因为栈遵循先进后出的原则,操作简单快速。堆的存取速度相对较慢,因为对象在堆上的分配和回收需要更多的时间,而且垃圾回收机制的运行也会影响性能。
存储空间:栈的空间相对较小(每个线程一个,单线程栈大小可由 -Xss 参数配置),由 JVM 管理。当栈溢出时,通常是因为递归过深或局部变量过大。堆的空间较大,动态扩展,也由 JVM 管理。堆溢出通常是由于创建了太多的大对象或未能及时回收不再使用的对象。
可见性:栈中的数据对线程是私有的,每个线程有自己的栈空间。堆中的数据对线程是共享的,所有线程都可以访问堆上的对象。
栈中存的到底是指针还是对象?
栈中存储的不是对象,而是对象的引用。
堆分为哪几部分呢?

新生代:新生代分为 Eden Space 和 Survivor Space。Eden 区是新生代中最大的区域(默认 Eden:S0:S1 = 8:1:1),大多数新创建的对象首先存放在这里。当 Eden 区满时,会触发一次 Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分为两个相等大小的区域,称为S0(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下来的对象会被移动到其中一个Survivor空间,以继续它们的生命周期。这两个区域轮流充当对象的中转站,帮助区分短暂存活的对象和长期存活的对象。
老年代:存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代中的对象生命周期较长,因此Major GC(也称为Full GC,涉及老年代的垃圾回收)发生的频率相对较低,但其执行时间通常比Minor GC长。老年代的空间通常比新生代大,以存储更多的长期存活对象。
元空间:从Java 8开始,永久代被元空间取代,用于存储类的元数据信息,如类的结构信息(如字段、方法信息等)。元空间并不在Java堆中,而是使用本地内存,这解决了永久代容易出现的内存溢出问题。
大对象:在 G1 垃圾收集器中,任何超过 Region 一半大小的对象都会被认定为 Humongous Object,直接分配在一组连续的 Humongous Region 中;这些 Region 在 G1 的逻辑上属于老年代的一部分(但有独立的分配策略),避免大对象在年轻代频繁被复制移动而带来的开销。传统的分代 GC(如 Parallel / CMS)中,超过 -XX:PretenureSizeThreshold 的大对象也会直接分配到老年代,原因同样是避免在 Eden 和 Survivor 之间反复复制。
如果有个大对象一般是在哪个区域?
大对象通常会直接分配到老年代。
新生代主要用于存放生命周期较短的对象,并且其内存空间相对较小。如果将大对象分配到新生代,可能会很快导致新生代空间不足,从而频繁触发 Minor GC。而每次 Minor GC 都需要进行对象的复制和移动操作,这会带来一定的性能开销。将大对象直接分配到老年代,可以减少新生代的内存压力,降低 Minor GC 的频率。
大对象通常需要连续的内存空间,如果在新生代中频繁分配和回收大对象,容易产生内存碎片,导致后续分配大对象时可能因为内存不连续而失败。老年代的空间相对较大,更适合存储大对象,有助于减少内存碎片的产生。
程序计数器的作用,为什么是私有的?
Java程序是支持多线程一起运行的,多个线程一起运行的时候cpu会有一个调动器组件给它们分配时间片,比如说会给线程1分给一个时间片,它在时间片内如果它的代码没有执行完,它就会把线程1的状态执行一个暂存,切换到线程2去,执行线程2的代码,等线程2的代码执行到了一定程度,线程2的时间片用完了,再切换回来,再继续执行线程1剩余部分的代码。
我们考虑一下,如果在线程切换的过程中,下一条指令执行到哪里了,是不是还是会用到我们的程序计数器啊。每个线程都有自己的程序计数器,因为它们各自执行的代码的指令地址是不一样的呀,所以每个线程都应该有自己的程序计数器。
方法区中的方法的执行过程?
- 解析方法调用:JVM会根据方法的符号引用找到实际的方法地址(如果之前没有解析过的话)。
- 栈帧创建:在调用一个方法前,JVM会在当前线程的Java虚拟机栈中为该方法分配一个新的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 执行方法:执行方法内的字节码指令,涉及的操作可能包括局部变量的读写、操作数栈的操作、跳转控制、对象创建、方法调用等。
- 返回处理:方法执行完毕后,可能会返回一个结果给调用者,并清理当前栈帧,恢复调用者的执行环境。
方法区中还有哪些东西?
用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
String保存在哪里呢?
字符串字面量保存在字符串常量池中,字符串常量池在 JDK 6 及之前位于方法区(永久代)中,自 JDK 7 起已经移到了堆中。
String s = new String("abc") 执行过程中分别对应哪些内存区域?
如果 "abc" 这个字符串常量之前不存在,则创建两个对象(常量池里的 "abc" + new 出来的实例);如果 "abc" 这个字符串常量已经存在,则只会创建一个对象(new 出来的实例)。
引用类型有哪些?有什么区别?
- 强引用指的就是代码中普遍存在的赋值方式,比如
A a = new A()这种。只要强引用还存在(变量未离开作用域、也没有被显式置 null),GC 就不会回收该对象。 - 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
- 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
- 虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。
内存泄漏和内存溢出的理解?
内存泄露:内存泄漏是指程序在运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致可用内存逐渐减少。
内存泄露常见原因:
- 静态集合:使用静态数据结构(如
HashMap或ArrayList)存储对象,且未清理。 - 事件监听:未取消对事件源的监听,导致对象持续被引用。
- 线程:未停止的线程可能持有对象引用,无法被回收。
内存溢出:内存溢出是指Java虚拟机(JVM)在申请内存时,无法找到足够的内存,最终引发OutOfMemoryError。这通常发生在堆内存不足以存放新创建的对象时。
内存溢出常见原因:
- 大量对象创建:程序中不断创建大量对象,超出JVM堆的限制。
- 持久引用:大型数据结构(如缓存、集合等)长时间持有对象引用,导致内存累积。
- 线程过多:每个线程都需要独立的栈空间,线程数过多时申请栈内存失败可能抛出
OutOfMemoryError: unable to create new native thread(注意:深度递归触发的是StackOverflowError,并不属于 OOM,二者是不同的 Error)。
遇到过堆溢出的情况吗?如何解决?
堆溢出通常发生在程序持续创建对象且无法被 GC 及时回收的场景下。
首先需要定位原因,一般分两步:
- 捕获内存快照:通过 JVM 参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof,让程序在发生 OOM 时自动生成堆快照文件。 - 分析快照文件:使用 MAT(Memory Analyzer Tool)或 JProfiler 等工具分析快照,重点看哪些对象占用了大量内存、是否存在内存泄漏(如对象长期被无用引用持有,无法回收)。
常见的解决思路根据原因不同而不同:
- 如果是内存泄漏:比如静态集合无意识地缓存了大量对象、长生命周期对象持有短生命周期对象的引用(如单例类持有业务对象)等。这时候需要梳理对象引用链,找到未释放的根源,比如清理静态集合中不再使用的元素、解除不必要的对象关联。
- 如果是内存不足:即程序确实需要大量内存(如处理大文件、加载大量数据到内存),但当前堆配置太小。这种情况下可以通过调整 JVM 参数扩大堆内存,比如
-Xms2g -Xmx4g(初始堆 2G,最大堆 4G),但需注意不能超过物理内存限制,避免频繁 swap。
另外,从代码层面优化也很重要:比如避免一次性加载全部数据(改用分批处理)、使用缓存时设置合理的过期策略、及时释放资源(如 IO 流、数据库连接)等,从源头减少内存占用。
栈溢出的情况呢?
栈溢出主要发生在 Java 虚拟机栈(或本地方法栈)的内存空间被耗尽时,通常与方法调用的深度直接相关。
从触发原因来看,最常见的场景是无限递归调用。因为 Java 方法调用时会在栈中创建栈帧(存储局部变量、操作数栈、方法返回地址等),每递归一次就会新增一个栈帧。如果递归没有正确的终止条件,栈帧会不断累积,最终超过虚拟机栈的最大容量,导致栈溢出。
另一种情况是单个方法的栈帧过大。如果一个方法定义了大量局部变量,或者局部变量占用内存过大(比如大数组),单个栈帧就会占用较多栈空间,可能在调用层级不深时就耗尽栈内存。
解决栈溢出的思路主要有:
- 排查递归逻辑:检查是否存在无限递归或递归层级过深的问题,添加正确的终止条件,或减少递归深度。必要时可将递归改写为迭代(如用循环替代),因为迭代不会持续创建新栈帧。
- 调整栈内存大小:通过 JVM 参数
-Xss(如-Xss256k)增大栈内存容量。但这种方式要谨慎,栈内存过大会导致线程可创建数量减少(总内存固定时,单个线程栈越大,能创建的线程数越少)。 - 优化方法栈帧:减少方法内局部变量的数量,避免在方法中创建过大的对象或数组,将大对象的创建移到堆中(通过 new 关键字),降低单个栈帧的内存占用。
有具体的内存泄漏和内存溢出的例子么请举例及解决方案?
1、静态属性导致内存泄露:如何优化?第一,进来减少静态变量;第二,如果使用单例,尽量采用懒加载。
2、 未关闭的资源:如何优化?第一,始终记得在finally中进行资源的关闭;第二,关闭连接的自身代码不能发生异常;第三,Java7以上版本可使用try-with-resources代码方式进行资源关闭。
3、 使用ThreadLocal
如何解决此问题?
- 第一,使用ThreadLocal提供的remove方法,可对当前线程中的value值进行移除;
- 第二,不要使用ThreadLocal.set(null) 的方式清除value,它实际上并没有清除值,而是查找与当前线程关联的Map并将键值对分别设置为当前线程和null。
- 第三,最好将ThreadLocal视为需要在finally块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。
对象的生命周期
对象的生命周期包括创建、使用和销毁三个阶段:
- 创建:对象通过关键字new在堆内存中被实例化,构造函数被调用,对象的内存空间被分配。
- 使用:对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
- 销毁:当对象不再被引用时,通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象,释放对象占用的内存空间,完成对象的销毁过程。
类加载器有哪些?

双亲委派模型的作用
- 保证类的唯一性:通过委托机制,确保了所有加载请求都会传递到启动类加载器,避免了不同类加载器重复加载相同类的情况,保证了Java核心类库的统一性,也防止了用户自定义类覆盖核心类库的可能。
- 保证安全性:由于Java核心库被启动类加载器加载,而启动类加载器只加载信任的类路径中的类,这样可以防止不可信的类假冒核心类,增强了系统的安全性。例如,恶意代码无法自定义一个
java.lang.System类并加载到 JVM 中,因为这个请求会被委托给启动类加载器,而启动类加载器只会加载标准的 Java 库中的类。 - 支持隔离和层次划分:双亲委派模型支持不同层次的类加载器服务于不同的类加载需求,如应用程序类加载器加载用户代码,扩展类加载器加载扩展框架,启动类加载器加载核心库。这种层次化的划分有助于实现沙箱安全机制,保证了各个层级类加载器的职责清晰,也便于维护和扩展。
- 简化了加载流程:通过委派,大部分类能够被正确的类加载器加载,减少了每个加载器需要处理的类的数量,简化了类的加载过程,提高了加载效率。
讲一下类的加载和双亲委派原则
我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化。
首先是加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。
加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
第二阶段是链接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:
- 验证(Verification),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
- 准备(Preparation),创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
- 解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。
最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
再来谈谈双亲委派模型,简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。
标记清除算法的缺点是什么?
主要缺点有两个:
- 一个是效率问题,标记和清除过程的效率都不高;
- 另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
GC只会对堆进行GC吗?
JVM 的垃圾回收器不仅仅会对堆进行垃圾回收,它还会对方法区进行垃圾回收。
- 堆(Heap): 堆是用于存储对象实例的内存区域。大部分的垃圾回收工作都发生在堆上,因为大多数对象都会被分配在堆上,而垃圾回收的重点通常也是回收堆中不再被引用的对象,以释放内存空间。
- 方法区(Method Area): 方法区是用于存储类信息、常量、静态变量等数据的区域。虽然方法区中的垃圾回收与堆有所不同,但是同样存在对不再需要的常量、无用的类信息等进行清理的过程。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)