JVM面试题

1. JVM的内存结构组成?

图片

  • 主内存(Main Memory):所有线程共享的主内存是一个共享的内存区域,用于存储所有线程创建的实例对象和数组。

  • 工作内存(Working Memory):每个线程有自己的工作内存,它保存了该线程使用的变量的主内存副本。线程对这些变量的所有操作首先在工作内存中进行,然后将结果刷新回主内存。

  • 程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的信号指示器,用于存储指向下一条指令的地址,即将要执行的指令代码。

  • 虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError

  • 本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接直接调用该方法;

  • :java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;

  • 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池。 加载的类信息被保存在元数据区中,运行时常量池保存在堆中。

2. 什么是JVM内存模型?

Java 内存模型(下文简称 JMM)就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性

这一组规则被称为 Happens-Before, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系

  • 程序顺序原则 :在一个线程内,按照程序代码的顺序执行,即前面的操作happens-before后面的操作。

  • 监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作。在synchronized

    中,也就是说线程要锁定就得上个线程先解锁。

  • volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作

  • 线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作

  • 线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前

  • 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C

怎么理解 happens-before 呢?如果按字面意思,比如第二个规则,线程(不管是不是同一个)的解锁动作发生在锁定之前?这明显不对。happens-before 也是为了保证可见性,比如那个解锁和加锁的动作,可以这样理解,线程1释放锁退出同步块,线程2加锁进入同步块,那么线程2就能看见线程1对共享对象修改的结果。

图片

Java 提供了几种语言结构,包括 volatile, finalsynchronized, 它们旨在帮助程序员向编译器描述程序的并发要求,其中:

  • volatile - 保证可见性有序性
  • synchronized - 保证可见性有序性; 通过管程(Monitor)保证一组动作的原子性
  • final - 通过禁止在构造函数初始化给 final 字段赋值这两个动作的重排序,保证可见性(如果 this 引用逃逸就不好说可见性了)

编译器在遇到这些关键字时,会插入相应的内存屏障,保证语义的正确性。

有一点需要注意的是,synchronized不保证同步块内的代码禁止重排序,因为它通过锁保证同一时刻只有一个线程访问同步块(或临界区),也就是说同步块的代码只需满足 as-if-serial 语义 - 只要单线程的执行结果不改变,可以进行重排序。

所以说,Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性,另外,还确保正确同步的 Java 代码可以在不同体系结构的处理器上正确运行。

3. heap 和stack 有什么区别?

Java中的堆(Heap)和栈(Stack)是两种不同的内存区域,它们在程序运行时扮演着不同的角色,并且具有不同的特性。以下是堆和栈的主要区别:

  1. 存储内容

    • :用于存储对象实例,包括类的实例和数组。当你使用关键字new创建对象时,对象会被分配到堆内存中。
    • :用于存储基本类型的局部变量(如int, short, long, float, double, char, byte, boolean以及对象的引用或地址)以及方法调用时的临时数据,包括方法调用的参数和局部变量。
  2. 内存管理

    • :由Java虚拟机的垃圾回收器(Garbage Collector, GC)管理,负责回收不再使用的对象。
    • :由操作系统自动管理,用于存储局部变量和方法调用时的临时数据。当一个方法被调用时,栈会分配一部分空间来存储这些数据,当方法执行完毕后,这部分空间会被自动释放
  3. 内存大小

    • :通常较大,其大小可以在启动JVM时通过启动参数进行配置。
    • :相对较小,为每个线程分配固定大小的内存空间,大小通常较小,由JVM实现和操作系统限制
  4. 内存分配

    • :内存分配不固定,对象的大小通常不一致,分配和回收是动态进行的。
    • :内存分配固定,每个线程分配的栈大小是固定的,局部变量的分配和回收是自动的,随着方法的调用和退出自动进行。
  5. 生命周期

    • :对象的生命周期不固定,由GC根据对象的可达性进行管理。
    • :生命周期与方法的调用相关,一个方法的调用开始时,栈帧被创建,方法执行结束时,栈帧被销毁
  6. 性能

    • :由于内存分配和回收的动态性,以及垃圾回收的开销,访问速度通常比栈慢。
    • :由于内存分配和回收的自动性,以及内存访问的局部性原理,访问速度通常比堆快。
  7. 垃圾回收

    • :需要垃圾回收,因为对象的生命周期不固定,需要GC来回收不再使用的对象以避免内存泄漏。
    • :不需要垃圾回收,因为栈内存随着方法的退出自动释放。
  8. 线程隔离性

    • :堆是线程共享的,所有线程都可访问堆内存中的对象。
    • :每个线程有自己的栈,线程间的栈内存是相互独立的。

理解堆和栈的区别对于编写高效的Java程序和理解Java内存管理机制非常重要。

4. 什么情况下会发生栈内存溢出?

1、栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个栈帧,它包含局部变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用

2、当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归调用肯可能会出现该问题;

3、调整参数**-xss**去调整jvm栈的大小

5. 谈谈对 OOM 的认识?如何排查 OOM 的问题?

除了程序计数器,其他内存区域都有 OOM 的风险。

  • 栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM
  • Java 8 常量池移到堆中,溢出会报异常:java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效;
  • 堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错;
  • 方法区 OOM,经常会遇到的是动态生成大量的类等;
  • 直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。

排查 OOM 的方法:

  • 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
  • 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
  • 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。

OOM(Out of Memory)错误是Java程序运行时常见的问题之一,表示Java虚拟机(JVM)的堆内存或者某个内存区域已耗尽。当JVM没有足够的内存来满足程序的需求时,就会抛出OutOfMemoryError异常。

OOM的类型

  1. Java heap space: 当JVM的堆内存耗尽时,通常是因为存活的对象太多,无法被垃圾回收器回收。

  2. Garbage collector overhead limit exceeded: 当垃圾回收占用了大量时间,超过了JVM启动参数-XX:+UseGCOverheadLimit设定的阈值时,会抛出此错误。

  3. Metaspace: JDK 8引入了Metaspace来替代永久代,用于存储类元数据。当Metaspace耗尽时,会抛出OOM。

  4. Unable to create new native thread: 当系统无法创建更多的本地线程时,会抛出此错误,这通常与操作系统的线程限制有关。

  5. Direct buffer memory: 使用NIO时,如果分配了太多的直接内存,也可能导致OOM。

如何排查OOM问题

  1. 监控和分析:

    • 使用JVM监控工具(如jconsole, VisualVM)监控内存使用情况。
    • 分析内存泄漏和内存溢出的常见迹象。
  2. 日志和异常信息:

    • 查看程序的日志和异常信息,确定OOM发生的上下文
  3. Heap Dump分析:

    • 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
    • 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
    • 使用分析工具(如MAT, JProfiler)分析堆转储,找出内存消耗最多的对象。
  4. 代码审查:

    • 检查代码中是否存在内存泄漏,如长时间持有对象引用、不必要的大对象创建等。
  5. 内存和垃圾回收参数调优:

    • 调整JVM参数,如增加堆大小-Xms和-Xmx调整Eden区与Survivor区的比例等。
    • 选择合适的垃圾回收器,并调整其参数。
  6. 优化代码和算法:

    • 优化内存使用,如使用合适的数据结构、避免创建不必要的对象等。
    • 优化算法复杂度,减少内存消耗。

排查OOM问题通常需要综合使用多种工具和方法,结合程序的具体情况进行分析。通过监控、分析和调优,可以有效地解决OOM问题,提高程序的稳定性和性能。

6. 谈谈 JVM 中的常量池?

JVM常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池

  • Class文件常量池。class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。

  • 运行时常量池:运行时常量池相对于class常量池一大特征就是具有动态性,java规范并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自class常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是String.intern()

  • 全局字符串常量池:字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。

  • 基本类型包装类对象常量池:java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。IntegerShortByte 和 Long 类型在一定范围内([-128127] )实现了常量池 ,这意味着在这个范围内的数值,每次调用 valueOf() 方法时,都会返回缓存中的对象,而不是每次调用都创建新对象。Character 类型有一个特殊的常量池,它缓存了从 \u0000\u007f(即0127)的 char 值。

    使用 valueOf() 方法可以获取到常量池中的对象,如果请求的值超出了常量池的范围,将会创建一个新的对象。

    Integer a = Integer.valueOf(127); // 从常量池获取
    Integer b = Integer.valueOf(127); // 从常量池获取,a和b指向同一对象
    Integer c = Integer.valueOf(128); // 创建新对象
    Integer d = Integer.valueOf(128); // 创建新对象,c和d指向不同对象
    

7. 如何判断一个对象是否存活?

判断一个对象是否存活,分为两种算法1:引用计数法;2:可达性分析算法;

引用计数法:给每一个对象设置一个引用计数器,当有一个地方引用该对象的时候,引用计数器就+1,引用失效时,引用计数器就-1;当引用计数器为0的时候,就说明这个对象没有被引用,也就是垃圾对象,等待回收;

缺点:无法解决循环引用的问题,当A引用B,B也引用A的时候,此时AB对象的引用都不为0,此时也就无法垃圾回收,所以一般主流虚拟机都不采用这个方法;

可达性分析法:从一个被称为GC Roots的对象向下搜索,如果一个对象到GC Roots没有任何引用链相连接时,说明此对象不可用,在java中可以作为GC Roots的对象有以下几种:

  • 虚拟机栈中引用的对象
  • 方法区类静态属性引用的变量
  • 方法区常量池引用的对象
  • 本地方法栈JNI引用的对象

但一个对象满足上述条件的时候,不会马上被回收,还需要进行两次标记

第一次标记:判断当前对象是否有finalize()方法并且该方法没有被执行过,若不存在则标记为垃圾对象,等待回收;

若有的话,则进行第二次标记;

第二次标记:将当前对象放入F-Queue队列,并生成一个finalize线程去执行该方法,虚拟机不保证该方法一定会被执行,这是因为如果线程执行缓慢或进入了死锁,会导致回收系统的崩溃;

如果执行了finalize方法之后仍然没有与GC Roots有直接或者间接的引用,则该对象会被回收

8. 强引用、软引用、弱引用、虚引用是什么,有什么区别?

  • 强引用,就是普通的对象引用关系,如 String s = new String(“ConstXiong”)
  • 软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
  • 弱引用,相比软引用来说,要更加无用一些,它拥有更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
  • 虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。

9. 被引用的对象就一定能存活吗?

不一定,看 Reference 类型,弱引用在 GC 时会被回收,软引用在内存不足的时候,即 OOM 前会被回收,但如果没有在 Reference Chain 中的对象就一定会被回收。

10. Java中的垃圾回收算法有哪些?

java中有四种垃圾回收算法,分别是标记清除法、标记整理法、复制算法、分代收集算法

标记清除法:第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;第二步:再遍历一遍,将所有标记的对象回收掉;

特点:效率低,标记和清除的效率都不高;标记和清除后会产生大量的不连续的内存碎片,可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC

图片

标记整理法:第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;第二步:将所有的存活的对象向一端移动,将端边界以外的对象都回收掉;

特点:适用于存活对象多,垃圾少的情况,要不然移动效率低;需要整理的过程,无空间碎片产生;

图片

复制算法:将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除;

特点:不会产生空间碎片;内存使用率极低;

分代收集算法:根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;

对比

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

11. 有哪几种垃圾回收器,各自的优缺点是什么?

垃圾回收器主要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;

  • Serial:单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。它的最大特点是在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,是client级别的默认GC方式。
  • ParNew:Serial收集器的多线程版本,也需要stop the world,复制算法
  • Parallel Scavenge:新生代收集器复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量;
  • Serial Old:Serial收集器的老年代版本,单线程收集器,使用标记整理算法
  • Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法
  • CMS:是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片
  • G1:标记整理算法+复制算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选回收不会产生空间碎片,可以精确地控制停顿;G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率;缺点:需要较多的内存作为记忆集,维护成本较高,可能影响效率。

垃圾回收器间的配合使用图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

各个垃圾回收器对比

图片

JDK8默认使用的垃圾收集器是?

Parallel GC(并行垃圾回收器)

  • 适用场景:适合于多核处理器环境,特别是后台处理任务,如批处理系统、计算密集型应用等。
  • 工作原理:使用多个线程并行地进行垃圾回收,缩短垃圾回收的时间,以此提高吞吐量。
  • 新生代收集:Parallel GC主要用于新生代的垃圾回收,它使用复制算法,将新生代分为一个Eden区和两个Survivor区。垃圾回收时,活动对象从Eden区和Survivor区复制到另一个空闲的Survivor区。
  • 老年代配合:与Parallel GC配合使用的老年代收集器通常是Parallel Old GC,它同样是一个并行的收集器,使用标记-整理算法进行垃圾回收。

12. 详细说一下CMS的回收过程?CMS的问题是什么?

CMS(Concurrent Mark Sweep,并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

从名字就可以知道,CMS是基于“标记-清除”算法实现的。CMS 回收过程分为以下四步:

  1. 初始标记 (CMS initial mark):主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
  2. 并发标记 (CMS concurrent mark):根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞没有 STW
  3. 重新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。这个过程会STW,但这个阶段的停顿时间通常比初始标记阶段要短,因为重新标记阶段主要处理的是并发标记阶段产生的少量误差
  4. 并发清除(CMS concurrent sweep):清除阶段是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发进行的。

CMS 的问题:

1. 并发回收导致CPU资源紧张:

在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。

2. 无法清理浮动垃圾:

在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。

3. 并发失败(Concurrent Mode Failure):

由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX**😗* CMSInitiatingOccupancyFraction 参数来设置。

这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。

4.内存碎片问题:

CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。

为了解决这个问题,CMS收集器提供了一个 -XX**:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS在执行过若干次**不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为0,表示每次进入 Full GC 时都进行碎片整理)。

13. 详细说一下G1的回收过程?

G1(Garbage First)回收器采用面向局部收集的设计思路和基于Region的内存布局形式,是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS,成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器,但从局部(两个Region之间)上看又是基于 标记-复制 算法实现的。

G1 回收过程,G1 回收器的运作过程大致可分为四个步骤:

  1. 初始标记(会STW):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  2. 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
  3. 最终标记(会STW):对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。
  4. 清理阶段(会STW):更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。

14. JVM中一次完整的GC是什么样子的?

先描述一下Java堆内存划分。

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代默认占总空间的 1/3,老年代默认占 2/3。新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。

新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。

老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。

图片

流转:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

15. Minor GC 和 Full GC 有什么不同呢?

Minor GC:只收集新生代的GC。

Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。

**Minor GC触发条件:**当Eden区满时,触发Minor GC。

Full GC触发条件

  • 大对象直接进入老年代,如果老年代有效空间不足,触发full GC。
  • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,并且没有开启JVM分配担保机制,则会进行Full GC。
  • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,开启JVM分配担保机制,老年代最大可用连续空间小于历次晋升到老年代对象的平均大小,则会进行Full GC。
  • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,开启JVM分配担保机制,老年代最大可用连续空间大于历次晋升到老年代对象的平均大小,进行了Minor GC,但是存活对象总大小还是大于S区,则会进入老年代,如果老年代有效空间不足,则会触发full GC。
  • 老年代空间不够分配新的内存(或永久代空间不足,但只是JDK1.7有的,这也是用元空间来取代永久代的原因,可以减少Full GC的频率,减少GC负担,提升其效率)。
  • 年龄阈值检查和对象动态年龄判断也会进入老年代,如果老年代有效空间不足,触发full GC。
  • 调用System.gc时,系统建议执行Full GC,但是不必然执行。

16. 介绍下空间分配担保原则?

如果YougGC时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么处理呢?其实JVM有一个老年代空间分配担保机制来保证对象能够进入老年代。

在执行每次 YoungGC 之前,JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,可能新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,那可能所有对象都要进入老年代了。

这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小的,那就可以放心进行 YoungGC。

但如果老年代的内存大小是小于新生代对象总大小的,那就有可能老年代空间不够放入新生代所有存活对象,这个时候JVM就会先检查 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次YoungGC,尽管这次YoungGC是有风险的。如果小于,或者 -XX:HandlePromotionFailure 参数不允许担保失败,这时就会进行一次 Full GC。

在允许担保失败并尝试进行YoungGC后,可能会出现三种情况:

  • ① YoungGC后,存活对象小于survivor大小,此时存活对象进入survivor区中
  • ② YoungGC后,存活对象大于survivor大小,但是小于老年大可用空间大小,此时直接进入老年代。
  • ③ YoungGC后,存活对象大于survivor大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会发生“Handle Promotion Failure”,就触发了 Full GC。如果 Full GC后,老年代还是没有足够的空间,此时就会发生OOM内存溢出了。

17. 什么是类加载?类加载的过程?

虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象;

类的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如图所示:

图片

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)

类加载过程如下:

  • 加载,加载分为三步:1、通过类的全限定性类名获取该类的二进制流;2、将该二进制流的静态存储结构转为方法区的运行时数据结构;3、在堆中为该类生成一个class对象
  • 验证:验证该class文件中的字节流信息复合虚拟机的要求,不会威胁到jvm的安全;
  • 准备:为class对象的静态变量分配内存,初始化其初始值
  • 解析:该阶段主要完成符号引用转化成直接引用
  • 初始化:到了初始化阶段,才开始执行类中定义的java代码;初始化阶段是调用类构造器的过程;

18. 什么是类加载器,常见的类加载器有哪些?

类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器;类加载器分为以下四种:

  • 启动类加载器(BootStrapClassLoader):用来加载java核心类库,无法被java程序直接引用;
  • 扩展类加载器(Extension ClassLoader):用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类;
  • 系统类加载器(AppClassLoader):它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的;
  • 自定义类加载器:由java语言实现,继承自ClassLoader;

图片

19. 什么是双亲委派模型?为什么需要双亲委派模型?

当一个类加载器收到一个类加载的请求,他首先不会尝试自己去加载,而是将这个请求委派给父类加载器去加载,只有父类加载器在自己的搜索范围类查找不到该类时,子加载器才会尝试自己去加载该类;

**作用:**为了防止内存中出现多个相同的字节码,保证被加载类的唯一性;沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。

补充:那怎么打破双亲委派模型

自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法

20. 列举一些你知道的打破双亲委派机制的例子,为什么要打破?

  • JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。

  • Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。为了隔离Web应用程序,Tomcat等容器为每个应用分配了独立的类加载器。

    tomcat之所以造了一堆自己的classloader,大致是出于下面三类目的:

    tomcat类加载器如下图:

    图片

    • 对于各个 webapp中的 classlib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的lib以便不浪费资源。
    • jvm一样的安全性问题。使用单独的 classloader去装载 tomcat自身的类库,以免其他恶意或无意的破坏;
    • 热部署。
  • 热部署:在某些服务器应用中,开发者可能需要在不重启服务器的情况下更新应用程序的类。这就需要自定义类加载器来加载新的类,而不是使用原来的类加载器。

  • 插件化开发:在一些插件化的应用程序中,主程序和插件可能需要相互隔离。每个插件可能有自己的类加载器,以避免不同插件间的类版本冲突。

  • 类重用:在某些情况下,可能需要在同一个JVM实例中多次加载同一个类的不同版本。这可以通过自定义类加载器实现,它不遵循双亲委派机制,而是加载特定的类版本。

  • 兼容性:当需要在同一个应用程序中同时运行不同版本的同一个库时,可能需要打破双亲委派机制,以便每个库组件都可以使用其特定版本的类。

  • 动态加载:在某些应用中,可能需要根据运行时条件动态地加载类。自定义类加载器可以实现这种需求,因为它可以独立于双亲委派机制工作。

  • JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。

21.说一下 JVM 调优?

避免不同插件间的类版本冲突。

  • 类重用:在某些情况下,可能需要在同一个JVM实例中多次加载同一个类的不同版本。这可以通过自定义类加载器实现,它不遵循双亲委派机制,而是加载特定的类版本。

  • 兼容性:当需要在同一个应用程序中同时运行不同版本的同一个库时,可能需要打破双亲委派机制,以便每个库组件都可以使用其特定版本的类。

  • 动态加载:在某些应用中,可能需要根据运行时条件动态地加载类。自定义类加载器可以实现这种需求,因为它可以独立于双亲委派机制工作。

  • JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。

21.说一下 JVM 调优?

JVM问题排查记录博客:https://blog.youkuaiyun.com/qq_42947952/article/details/138533543

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值