2.1 什么情况下会发生栈内存溢出。

  • 递归调用过深:超过了JVM为线程(多个栈帧(局部变量、操作数栈、方法返回地址))分配的栈空间,导致栈内存溢出。确保递归有合理退出条件
  • 大量本地变量占用大内存:方法中声明过多大对象(几百K或几M的对象)作为局部变量如数组、大型对象占用大量栈空间,导致栈内存溢出;尽量将大对象放入堆中
  • 栈线程设置过小JVM通过-Xss设置线程大小,因栈限制导致栈内存溢出;适当增加-Xss参数,增大线程大小。
  • 死锁或循环等待引起栈内存溢出

2.2 JVM的内存结构,Eden和Survivor比例。

  在JVM内存结构中,Eden和Survivor都是堆中的新生代内存模块用于存储新创建的或生命周期较短的对象,其中Survivor有Survivor0区和Survivor1区,Eden与其余两Survivor内存占比为8:1:1

2.3 JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。

  JVM内存被划分为不同区域,是为了更好管理和优化内存使用提高垃圾回收效率。 

新生代

新生代主要是存储新创建的或生命周期较短的对象,大多数对象在刚创建时很快变得不可达,成为垃圾对象。新生代通过复制算法进行垃圾回收,效率高,大多数对象在第一个GC后死亡。新生代栈堆内存较小部分。

老年代

老年代用于存储经过多次垃圾回收仍存活的对象,这些对象生命周期较长,或大对象(占用内存达到内存阈值的队形)。由于这些对象生命周期较长且数量相较稳定,因此采用标记-清除或标记压缩算法进行垃圾回收,频率较低。

持久代

  在JDK1.7及之前,方法区的一部分被称为持久代,用于存储类信息、常量池、静态变量、即时编译后的代码。持久代内存受限制,容易出现内存溢出问题。在JDK1.8之后,元空间取代了持久代,主要存储类元数据;它位于本地内存而不是堆内存,其大小受物理内存限制,JVM能动态调整元数据大小,有效管理内存。

Eden区和Survivor区的区别

  新创建的对象首先被分配到Eden区,Eden区满,会触发Minor GC(年轻代垃圾回收);Minor GC时,会检查Eden区和一个Survivor区(假设S0)中的对象,仍然存活的对象会被复制到另一个Survivor区(S1),这样可通过复制算法快速释放无用对象占用的内存,达到清理内存带来的碎片化问题经过15次检查仍存活的对象会进入Old区

2.4 JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代,说说你知道的几种主要的JVM参数。

  一次完整GC包含标记阶段、复制阶段、清理阶段、晋升检查与对象晋升、混合GC或Full GC。

标记阶段

  垃圾收集器首先通过可达性分析算法标记所有从根对象集合(全局变量、栈中引用)能直接或间接访问到的对象

复制阶段

  在新生代内存区域,Eden区和Survivor区都是采用复制算法进行垃圾回收,当Eden区空间不足,出发Minor GCMinor GC会将Eden区和一个Survivor区的对象复制到另一个Survivor区,在该过程中,对象年龄+1

清理阶段

  清理没有被复制对象占用的空间

晋升检查与对象晋升

  每次Minor GC多会检查对象的年龄当一个对象在Survivor区经历一定次数(默认为15次,可通过-XX MaxTenuringThreshold设置)的Minor GC依然存活,则晋升为老年代

混合GC或Full GC

  如果在进行Minor GC的过程中,发现要晋升的对象过多使得Survivor区无法容纳或者老年代也无法提供足够的空间,那么可能会触发对整个年轻代和部分老年代的混合式GC(如果使用CMS垃圾收集器),或者触发Major GC/Full GC(如果是G1或者Parallel Old垃圾收集器)来清理老年代和年轻代的所有空间

晋升为老年代的场景

  • 多次Minor GC仍存活的对象
  • 大对象(即需要分配大量连续内存大于特定阈值的对象,默认由-XX:PretenureSizeThreshold参数设定),它们在分配时可能直接进入老年代

主要的JVM垃圾收集相关参数

  • -Xms 和 -Xmx:设置堆内存初始大小和最大大小
  • -XX:NewRatio:设置年轻代和老年代的大小比例(默认1:2)
  • -XX:SurvivorRatio:设置年轻代中Eden区和Survivor区的比例(默认8:1:1)
  • -XX:+UseConcMarkSweepGC 或 -XX:+UseG1GC 等:选择垃圾收集器策略
  • -XX:MaxTenuringThreshold:设置对象晋升到老年代前在年轻代中经历的GC次数上限(默认15)
  • -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize(Java 8及之后版本):元空间大小的初始值和最大值
  • -XX:+PrintGCDetails:输出详细的垃圾收集日志信息

2.5 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。

  垃圾回收器是JVM中负责回收内存的组件。Serial GC、Parallel GC/Parallel Old GC、CMS、G1等垃圾收集器

Serial-GC

原理流程:Serial GC是单线程的垃圾回收器,它在进行垃圾回收时会暂停所有用户线程(Stop the world)。整个过程包括年轻代的Minor GC和老年代的Major GC,采用标记-复制或标记-压缩算法

优缺点:简单高效,适合客户端应用或单核CPU服务器;在多核处理器系统中,由于只有一个GC线程可能导致较长时间的STW

Parallel GC/Parallel Old GC

原理流程:Parallel GC是对Serial GC的并行版本,在多核环境下使用多个线程进行垃圾回收,同样在回收过程中会暂停所有用户线程

优缺点:在多核CPU环境中性能较好,显著缩短STW时间;在高并发场景下,并行处理可能导致系统负载较大

CMS

原理流程

  • 初始标记(Initial Mark)STW阶段仅标记从根对象直接可达的对象
  • 并发标记(Concurrent Mark)遍历对象图,并标记所有可达对象,此时用户线程可继续运行
  • 重新标记(Remark)STW阶段修正并发标记期间因用户程序运行而发生变化的引用关系
  • 并发清除(Concurrent Sweep)清理未被标记的对象,此阶段用户线程依然可以运行

优缺点

  • 优点:大部分工作都并发执行,减少STW时间适用于对响应时间有较高要求的应用场景
  • 缺点:并发标记和并发清除阶段会导致系统资源竞争,可能影响应用程序性能;碎片化问题严重需要定期进行并发压缩(CMS的并发压缩功能在Java 9之后已经废弃)。

G1

原理流程

  • 区域化管理:将堆划分为多个大小相等的Region,每个Region都可以作为Eden、Survivor或Old区域
  • 并发标记:与CMS类似,先进行初始标记和根区域扫描,然后并发标记全堆
  • 空间回收:优先回收收益最大的Region(即垃圾最多),避免全堆整理,降低停顿时间
  • 完整GC(Full GC)在空间不足时进行,包含全局并发标记和压缩操作,但G1尽量避免触发这种类型的GC

优缺点

  • 优点:目标是 pauses are predictable and garbage collection work is done incrementally,提供了接近实时的垃圾回收预测模型通过并发标记和部分压缩来减少STW时间,并且能够自动进行分区平衡
  • 缺点:对于大量小对象分配的场景,G1的表现可能不如其他收集器;在大内存机器上,由于其复杂性,启动和初始化阶段可能会比较慢

2.6 垃圾回收算法的实现原理。

  垃圾回收算法主要是如何自动识别和释放不再使用的内存区域。垃圾回收算法分为垃圾标记算法和垃圾清除算法。垃圾标记算法包括、引用计数法和可达性分析;垃圾清除算法包括标记-清除算法、复制算法、标记-压缩算法、分代收集、增量式手机、并发收集、分区收集。

  1. 引用计数法 (Reference Counting): 每个对象都有一个引用计数器,当有新的引用指向该对象时,计数器加一;当引用失效时,计数器减一。当对象的引用计数为零时,表示没有其他对象引用它,可以被回收。然而,这种方法无法处理循环引用的问题。
  2. 可达性分析/根搜索算法 (Reachability Analysis): 通过从一系列称为“根”的对象集合(如栈帧中的本地变量表、方法区静态变量、JNI局部引用等)开始遍历整个对象图,如果某个对象不能从这些根对象通过引用链到达,则认为它是不可达的,即垃圾。Java虚拟机采用的就是这种算法,并在此基础上发展出了多种垃圾收集器。
  3. 标记-清除 (Mark-Sweep): 分为两个阶段:首先,对堆中所有对象进行可达性分析,标记出存活的对象;然后,清理未被标记的所有对象所占用的内存空间。缺点是会产生不连续的内存碎片
  4. 复制算法 (Copying): 把内存分为两部分,每次只使用其中一部分,当这部分内存用完时,就将存活的对象复制到另一部分,同时清理掉原来的那部分内存。这样可以避免内存碎片,但会浪费一半的空间新生代的Eden区和Survivor区就是采用复制算法进行垃圾回收的。
  5. 标记-压缩/整理 (Mark-Compact): 类似于标记-清除,但在清除后会对存活对象进行压缩整理,把它们移到内存的一端,从而消除内存碎片。这种方式在老年代通常更为适用
  6. 分代收集 (Generational Collection): 基于对象生命周期假设,将内存划分为新生代和老年代,不同年龄段的对象使用不同的垃圾回收策略。新生代大多采用复制算法,而老年代一般采用标记-压缩或标记-清除
  7. 增量式收集 (Incremental Collecting): 将垃圾回收过程分成多个小步骤执行,每次执行一小部分,使得应用程序可以在垃圾回收过程中继续运行,减少STW(Stop-The-World)的时间。
  8. 并发收集 (Concurrent Collecting): 在应用线程运行的同时进行垃圾回收,比如CMS(Concurrent Mark Sweep)和G1(Garbage First)收集器的部分阶段就是并发执行的。
  9. 分区收集 (Region-Based Collecting): G1垃圾收集器进一步改进了分代收集,将堆划分为多个大小相等的独立区域,并优先回收垃圾最多的区域,以此降低停顿时间

2.7 当出现了内存溢出,你怎么排错。

   当出现内存溢出,可以通过识别错误类型收集详细信息分析堆转储文件排查代码逻辑调整JVM参数

识别错误类型通过系统日志或错误堆栈信息确定哪种类型的内存溢出。如Java Heap Space(堆内存不足)、PermGen Space/MetaSpace(方法区内存不足)、dnative memory(本地内存不足)

收集详细信息

  • 使用JVM参数 -XX:+HeapDumpOnOutOfMemoryError 在发生OOM时生成堆转储文件(heap dump),便于后续分析
  • 使用 jstack 工具获取线程快照,以了解程序在发生溢出时的状态和活动线程情况
  • 调整日志级别,输出详细的GC日志,使用 -XX:+PrintGCDetails 和 -Xlog:gc* 参数

分析堆转储文件

  • 使用如VisualVM、MAT(Memory Analyzer Tool)、JProfiler等工具分析堆转储文件找出占用大量内存的对象和可能的内存泄漏点
  • 查看是否存在大对象、长生命周期的对象或者对象数量异常增长的情况。

排查代码逻辑

  • 根据堆转储分析结果,结合代码审查,寻找可能导致内存泄漏的地方,比如静态集合类中的对象没有及时清理、长时间持有的大对象引用等。
  • 检查是否存在循环依赖导致对象无法被垃圾回收。

调整JVM参数

  • 根据应用需求和实际情况适当增加堆内存大小,例如 -Xms 和 -Xmx 设置初始和最大堆内存。
  • 对于Metaspace,可以调整 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 参数

2.8 JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等。

  JVM内存模型(JMM)定义了如何访问和修改共享变量的规则。

  1. 主内存与工作内存
  • 主内存(Main Memory):在JMM中,所有变量都存储在主内存中,它是共享资源区域所有线程都可以直接或间接地访问
  • 工作内存(Working Memory/Local Memory):每个线程拥有自己的工作内存,里面保存了该线程使用到的变量副本。当一个线程对变量进行读取或写入时,会先操作工作内存中的副本然后按照一定规则与主内存进行同步
  1. 重排序 (Reordering)
  • 多核处理器或者编译器优化过程中,为了提高执行效率,可能会改变代码的实际执行顺序,这种现象称为指令重排序。但重排序必须遵循as-if-serial语义,即单线程环境下看起来像是按照程序顺序执行的,但在多线程环境下可能会影响可见性和一致性。
  1. 内存屏障 (Memory Barrier/Fence)
  • 内存屏障是一种CPU指令,用于确保某些内存操作的顺序性它可以防止重排序保证特定的内存访问顺序,并且强制更新工作内存和主内存之间的数据一致性。例如,Load Barrier可以确保加载操作完成后再执行后面的指令,Store Barrier则可以确保前面的写操作提交到主内存后才执行后续的写操作。
  1. Happens-Before原则
  • Happens-Before是JMM中定义的一种先行发生关系,用来保证多线程环境下的程序执行的一致性和正确性如果一个操作A happens-before 操作B,那么操作A的结果对于操作B来说是可见的,且A的执行顺序早于B
  • JVM通过以下几种规则保证happens-before关系:
  • 程序次序规则在一个线程内,按照代码顺序,前面的操作happens-before后面的操作
  • 锁定规则:解锁操作happens-before随后对同一锁的加锁操作。
  • volatile变量规则对一个volatile变量的写操作happens-before后面对该变量的读操作
  • 传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C。

2.9 简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。

  类加载器再JVM负责加载类和接口二进制数据,并将它们转化为JVM可以直接使用的运行时类对象。每个类由特定的类加载器加载到JVM中,确保类的唯一性和安全性。其分为三种,分别是引导加载器(Bootstrap ClassLoader)、扩展加载器(Extension ClassLoader)、应用(系统)加载器(Application ClassLoader/System ClassLoader)。

  可以,在Java的类加载机制中,有一种双亲委派机制。在该机制中,当一个类加载器收到加载类请求时,首先会将请求委派给父加载器加载,如果父加载器可以加载,则向上层层递进,由父类的父加载器加载,直至父类的加载器无法加载,再通过父类的子加载器加载。这种自底向上,层层委托的加载机制可以防止类的重复加载,保证Java核心API库的稳定性和安全性

  在某些场景下,确实需要打破双亲委派模型。例如:

  • 实现隔离性:在容器环境如Tomcat或OSGi框架中,为了支持不同应用之间的类隔离,可能会创建多个类加载器,每个加载器负责加载各自的应用程序类,即使这些类有相同的全限定名,也可以通过不同的类加载器加载并视为不同的类
  • 插件系统一些插件框架允许用户动态加载扩展功能,而这些扩展可能需要使用与宿主应用程序相同名称但不同版本的类库。为了实现这一点,需要定制类加载器来加载插件私有的类库。

  打破双亲委派模型通常涉及自定义类加载器,即继承java.lang.ClassLoader类并重写其loadClass()方法,在此方法内部添加自定义加载逻辑,比如先尝试自己加载然后再委派给父加载器

2.10 你们线上应用的JVM参数有哪些。

  线上应用的JVM参数配置非常丰富,用于优化和调整Java应用程序在生产环境中的性能内存管理垃圾回收线程栈大小堆大小等多个方面。以下是一些常用的JVM参数示例:

  1. 内存相关参数(堆大小、元空间大小)
  • -Xms:设置Java虚拟机初始堆内存大小。
  • -Xmx:设置Java虚拟机最大堆内存大小。
  • -Xmn(年轻代大小):设置新生代的大小,适用于使用Parallel GC或CMS等垃圾收集器。
  • -XX:MetaspaceSize:设置元空间(MetaSpace,在Java 8及更高版本中取代了PermGen)的初始大小。
  • -XX:MaxMetaspaceSize:设置元空间的最大大小。
  1. 垃圾回收相关参数
  • -XX:+UseG1GC 或 -XX:+UseConcMarkSweepGC 等:选择垃圾收集器策略。
  • -XX:SurvivorRatio:设置年轻代Eden区与Survivor区的比例。
  • -XX:MaxTenuringThreshold:设置对象晋升到老年代的年龄阈值。
  1. 并发和线程栈相关参数
  • -Xss:每个线程的栈大小
  • -XX:ThreadStackSize:另一个设置线程栈大小的选项,具体用法取决于JDK版本。
  • -XX:ParallelGCThreads:指定并行垃圾回收时使用的线程数(针对Parallel GC)。
  1. 性能监控与诊断参数
  • -XX:+PrintGC 或 -verbose:gc:开启垃圾回收日志输出
  • -XX:+PrintGCDetails:输出详细的垃圾回收信息
  • -XX:+HeapDumpOnOutOfMemoryError:当发生OOM错误时,生成堆转储文件(heap dump)
  1. 其他高级特性参数:
  • -XX:+UseStringDeduplication:启用字符串去重功能(减少重复字符串对内存的占用)。
  • -XX:NewRatio:设置年轻代与老年代的内存比例
  1. 服务端模式启动
  • -server:告知JVM以服务器模式运行,通常提供更好的性能

2.11 g1和cms区别,吞吐量优先和响应优先的垃圾收集器选择。

  G1和CMS都是Java Hotspot中的垃圾回收器,它们在设计目标和实际应用上有所不同。

G1垃圾收集器:

  • 设计目标:兼顾吞吐量与低延迟。G1被设计为在大内存服务器上提供可预测的停顿时间,并且能处理非常大的堆内存(数十GB甚至上百GB)。
  • 特点:
  • 区域化管理:将整个堆划分为多个大小相等的Region,每个Region可以是Eden、Survivor或老年代。
  • 并发标记:大部分垃圾回收阶段并发执行,减少STW(Stop-The-World)时间。
  • 优先处理垃圾最多的区域:根据Region中垃圾对象的比例选择优先回收的Region,从而实现更高效的内存回收。
  • 记忆集:用于跟踪跨Region引用,以避免全堆扫描。
  • Pause Prediction模型:允许用户指定最大暂停时间目标

CMS垃圾收集器:

  • 设计目标:响应时间优先,适用于对响应时间要求较高的应用环境
  • 特点:
  • 并发标记清除:同样采用了并发标记阶段来减少STW时间,但清理阶段不并发,可能导致碎片问题。
  • 两个线程并行工作:一个进行标记,另一个负责重新标记可能在并发标记过程中改变的对象引用关系。
  • 无法设置最大暂停时间目标:CMS的目标是尽可能缩短每次GC造成的应用暂停时间,但不支持像G1那样预设最大暂停时间。

吞吐量优先与响应优先的选择:

  • 如果应用对系统整体的吞吐量(单位时间内完成的工作量)有较高要求,且能够接受较长的偶尔停顿时间,可以选择Parallel GC或Parallel Old GC
  • 如果应用对响应时间敏感,需要尽量降低因垃圾回收导致的程序暂停,那么CMS或者G1通常是更好的选择
  • G1在大多数现代应用场景下是一个更为综合的选择,因为它既可以优化吞吐量,也能保持较低的暂停时间,同时还能处理大堆内存

2.12 怎么打出线程栈信息。

  1. 通过Thread.currentThread().getStackTrace()获取当前线程的堆栈信息:打印出当前线程调用栈的所有元素,包括类名、方法名、文件名和行号
  2. 使用Thread.getAllStackTraces()获取所有活动线程的堆栈信息
  3. Linux或Unix环境下使用jstack命令: 在终端窗口中,如果JDK已安装并配置好环境变量,可以直接执行 jstack 命令来获取指定Java进程的详细线程堆栈信息
  4. Windows环境下也可以使用类似的方法: 同样可以通过jstack命令来获取线程堆栈信息,只是命令行界面稍有不同。
  5. 触发JVM崩溃转储(Heap Dump)时附带线程堆栈: 可以向Java进程发送SIGQUIT信号(Linux/Unix环境下)或者在Windows上通过Ctrl+Break快捷键来触发一个核心转储文件,其中包含了所有的线程堆栈信息。然后可以使用像jhat(在较早版本的JDK中可用)或VisualVM等工具分析这个转储文件

2.13 请解释如下jvm参数的含义:

-server -Xms512m -Xmx512m -Xss1024K

服务器模式启动,内存管理、现成调度有更好性能;堆初始大小为512M,堆最大内存512M,每个线程栈最大内存1M。
-XX:PermSize=256m -XX:MaxPermSize=512m -

持久代初始内存256M,最大内存512M
XX:MaxTenuringThreshold=20 XX:CMSInitiatingOccupancyFraction=80 -

从新生代晋升为老年代的Survivor区复制的GC阈值为20次:该参数指定了触发CMS进行垃圾回收的阈值。当老年代空间使用率达到80%时,CMS将会启动垃圾回收
XX:+UseCMSInitiatingOccupancyOnly。

只根据 CMSInitiatingOccupancyFraction 参数来决定何时触发CMS垃圾回收,而不考虑其他因素如当前时间间隔等因素