当前位置:   article > 正文

面试题篇-03-Java-Jvm相关面试题_cms垃圾收集器的并发更新失败是怎么回事? 如何优化?

cms垃圾收集器的并发更新失败是怎么回事? 如何优化?

文章目录

1. JVM的GC是任何时刻都会发生吗?

不是,只有stw(stop the world)即用户线程停下来后才会执行,那么如果千万线程如何治理呢?
就是此处的安全点安全区域

1.1 什么是安全点Safe Point

程序执行时并非在所有地方都能停顿下来开始GC , 只有在特定的位置才能停顿下来开始GC , 这些位置称为“ 安全点(Safepoint)。

安全点的选择很重要, 如果太少可能导致GC 等待的时间太长, 如果太频繁可能导致运行时的性能问题。
大部分指令的执行时间都非常短暂,通常会根据“ 是否具有让程序长时间执行的特征” 为标准。
比如: 选择一些执行时间较长的指令作为safe Point ,如:

  • 循环的末尾
  • 方法临返回前
  • 调用方法之后
  • 抛异常的位置

1.2 什么是安全区域Safe Region

SafePoint 机制保证了程序执行时, 在不太长的时间内就会遇到可进入GC的safepoint 。
但是, 程序“ 不执行” 的时候呢? 例如线程处于Sleep 状态或Blocked 状态, 这时候线程无法响应JVM的中断请求, “ 走” 到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况, 就需要安全区域(Safe Region) 来解决。

安全区域是指在一段代码片段中, 对象的引用关系不会发生变化, 在这个区域中的任何位置开始GC 都是安全的。我们也可以把Safe Region 看做是被扩展了的Safepoint

实际执行时:

  • 1 、当线程运行到 Safe Region 的代码时, 首先标识己经进入了safe Region ,如果这段时间内发生GC ,JVM会忽略标识为 Safe Region 状态的线程(认为它是安全的):
  • 2 、当线程即将离开时, 会检查JVM是否己经完成GC , 如果完成 , 则继续运行, 否则线程必须等待直到收到可以安全离开Safe Region 的信号为止。

举个生活列子:
一句话,睡觉可以,请先进入酒店再睡觉,并且进去是在屏幕上说一声我进入安全区域了,在睡觉。这样做的目的就是当要GC的时候不至于找不到你,如果看到屏幕上有你这个线程的名字,就知道你是安全的,就会忽略你;
你(线程)睡醒了要出门了。抬头看看大屏幕是不是安全在出去(true),如果不安全(false)就在酒店待着,别出门,等到gc完成后,状态变为false在出门。

1.3 GC时线程的中断策略 ?

如何在GC生时, 检查所有线程都跑到最近的安全点停顿下来呢?

  • 抢先式中断: ( 目前没有虚拟机采用了)
    首先中断所有线程。如果还有线程不在安全点, 就恢复线程, 让线程跑到安全点。
  • 主动式中断:
    设置一个中断标志, 各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真, 则将自己进行中断挂起。(客栈门上安装了一个显示器,上面会显示ture,或者false。如果系统需要垃圾回收,就会更新这个状态为true,线程到了客栈后看到为true,就进店别别出来了。)

2. CMS垃圾回收器的并发更新失败是怎么回事,如何优化?

在这里插入图片描述

  • 1.初始标记(CMS initial mark) 停
  • 2.并发标记(CMS concurrent mark)和用户线程一起 并发
  • 3.重新标记(CMS remark) 停
  • 4.并发清除(CMS concurrent sweep)和用户线程一起 并发

在这里插入图片描述

2.1 CMS并发更新失败问题

然而, CMS并不是完美的,在使用CMS的过程中会产生2个最让人头痛的问题:

  • promotion failed(晋升失败)
    promotion failed是在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年带有足够的空闲空间,但是由于碎片较多,这时如果新生代要转移到老年带的对象比较大,所以,必须尽可能提早触发老年带的CMS回收来避免这个问题(promotion failed时老年带CMS还没有机会进行回收,又放不下转移到老年带的对象,因此会出现下一个问题concurrent mode failure,需要stop-the-wold GC- Serail Old)。

  • 优化办法:
    让CMS在进行一定次数的Full GC(标记清除)的时候进行一次标记整理算法,CMS提供了以下参数来控制:
    -XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
    也就是CMS在进行5此Full GC(标记清除)之后进行一次标记整理算法,从而可以控制老年带的碎片在一定的数量以内,甚至可以配置CMS在每次Full GC的时候都进行内存的整理。

  • concurrent mode failure(并发更新失败)
    concurrent mode failure是在执行CMS GC的过程中同时业务线程将对象放入老年代,而此时老年代空间不足,这时CMS还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年带,而老年带也放不下而产生的。

  • concurrent mode failure出现的频率,这可以通过-XX:+PrintGCDetails来观察,当出现concurrent mode failure的现象时,就意味着此时JVM将继续采用Stop-The-World的方式来进行Full GC,这种情况下,CMS就没什么意义了,造成concurrent mode failure的原因是当minor GC进行时,旧生代所剩下的空间小于Eden区域+From区域的空间,或者在CMS执行老年带的回收时有业务线程试图将大的对象放入老年带,导致CMS在老年带的回收慢于业务对象对老年带内存的分配。

  • 优化办法:
    解决这个问题的通用方法是调低触发CMS GC执行的阀值,CMS GC触发主要由CMSInitiatingOccupancyFraction值决定,默认情况是当老年代已用空间为68%时,即触发CMS GC,在出现concurrent mode failure的情况下,可考虑调小这个值,提前CMS GC的触发,以保证旧生代有足够的空间。另外,扩大老年代空间和调小CMSMaxAbortablePrecleanTime的值也有助于避免这个问题。

      1. 扩大老年代空间
        调小新生代占用的空间或增大整个JVM Heap的空间可扩大旧生代空间,这对于避免concurrent mode failure现象可以提供很大的帮助。
      1. 调小CMSMaxAbortablePrecleanTime的值

CMS GC需要经过较多步骤才能完成一次GC的动作,在minor GC较为频繁的情况下,很有可能造成CMS GC尚未完成,从而造成concurrent mode failure,这种情况下,减少minor GC触发的频率是一种方法,另外一种方法则是加快CMS GC执行时间,在CMS的整个步骤中,JDK 5.0+、6.0+的有些版本在CMS-concurrent-abortable-preclean-start和CMS-concurrent-abortable-preclean这两步间有可能会耗费很长的时间,导致可回收的旧生代的对象很长时间后才被回收,这是Sun JDK CMS GC的一个bug[1],如通过PrintGCDetails观察到这两步之间耗费了较长的时间,可以通过-XX: CMSMaxAbortablePrecleanTime设置较小的值,以保证CMS GC尽快完成对象的回收,避免concurrent mode failure的现象。

2.2 总结

Minor GC后, 救助空间容纳不了剩余对象,将要放入老年带,老年带有碎片或者不能容纳这些对象,就产生了concurrent mode failure, 然后进行stop-the-world的Serial Old收集器。

解决办法:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 或者 调大新生代或者救助空间

CMS是和业务线程并发运行的,在执行CMS的过程中有业务对象需要在老年带直接分配,例如大对象,但是老年带没有足够的空间来分配,所以导致concurrent mode failure, 然后需要进行stop-the-world的Serial Old收集器。

解决办法:+XX:CMSInitiatingOccupancyFraction,调大老年带的空间,+XX:CMSMaxAbortablePrecleanTime

总结一句话:使用标记整理清除碎片和提早进行CMS操作。

3. Jvm内存模型–Java内存模型–Java对象模型

  • Jvm内存结构(jvm运行时内存结构)
    在这里插入图片描述
  • Java内存模型
    在这里插入图片描述
  • Java对象模型
    在这里插入图片描述

4. Java的热点代码与即时编译

  • 目前主流的两款商用Java虚拟机(HotSpot、OpenJ9)里,Java程序最初都是通过解释(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(HotSpot Code)
  • 为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器

在这里插入图片描述
主流的Java虚拟机都有解释器与编译器,他们各有优势:

  • 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译时间,立即执行。
  • 当程序运行后,随着时间推移,编译器逐渐发挥作用,把代码编译成本地代码之后,可以获取更高的执行效率。

HotSpot中内置了两个即时编译器,分别为Client Compiler(简称C1)Server Compiler(简称C2)

  • 默认采用一个解释器和一个编译器配合的方式工作,叫做混合模式(Mixed Mode),用户可以使用“-client”或“-server”参数去强制指定编译器。

  • 只使用解释器的方式叫“解释模式”(Interpreted Mode),只使用编译器的方式叫“编译模式”(Compiled Mode)。可以通过虚拟机的 -version 命令查看相应信息。

在运行过程中会被即时编译器编译的“热点代码”有两类:

  • 被多次调用的方法。一个方法被调用、执行的次数较多。
    • 编译器会以整个方法作为编译对象,这也是JVM中的标准JIT编译方式。
  • 被多次执行的循环体。方法体内存在循环多次的循环体,虽然该方法可能只被调用一次或几次,但是循环体却是重复执行多次的。
    • 依然会以整个方法作为编译对象,但是编译发生在方法执行的过程中(循环方法体内),因此形象的称为栈上替换(On Stack Replacement,OSR编译,即方法栈帧还在栈上,但是方法被替换了)。

次数是判断为热点代码的判定条件,判断一段代码是不是热点代码的行为就叫“热点探测”(Hot Spot Detection)
目前主要的热点探测方式有两种:基于采样的热点探测,基于计数器的热点探测。

  • 基于采样的热点探测
    基于采样的热点探测(Sample Based Hot Spot Detection):采用这种方式虚拟机会周期性地检查各个线程的栈顶,如果某个方法经常出现在栈顶,即为“热点方法”。

    • 这种方式的好处是实现简单、高效,容易获取方法调用关系。
    • 缺点是很难精确确认热度,也可能是因为线程阻塞等外界因素导致堆积在栈顶。
  • 基于计数器的热点探测
    基于计数器的热点探测(Counter Based Hot Spot Detection)

    • 采用这种方式的虚拟机会为每个方法(甚至代码块)建立计数器,统计执行次数,超过一定的次数即为“热点方法”。

    • HotSpt使用的是这种方法。

    • 这种方法实现起来比较麻烦一些,需要为每个方法建立并维护计数器,且不能直接获取方法的调用关系,但是它的统计结果更加精确和严谨。

    • 基于计数器的热点探测方式中有两种计数器:

      • 方法调用计数器(Invocation Counter):用于统计方法的调用次数。
      • 回边计数器(Back Edge Counter):用于统计循环代码块调用次数。

5. 三色标记

三色标记法是Java 虚拟机(JVM)中垃圾回收算法的一种,主要用来标记内存中存活和需要回收的对象。

它的好处是,可以让 JVM 不发生或仅短时间发生STW(Stop The World),从而达到清除JVM 内存垃圾的目的
JVM 中的「 CMSG1 垃圾回收器」都用到了三色标记法。

在三色标记法中,Java 虚拟机将内存中的对象分为三个颜色:

  • 白色:表示还没有被垃圾回收器扫描的对象;
  • 灰色:表示已经被垃圾回收器扫描过,但对象引用的其他对象尚未被扫描。
  • 黑色:表示已经被垃圾回收器扫描过,且对象及其引用的其他对象都是存活的;

在GC 开始时(如图)
在这里插入图片描述

  • 先将所有对象都标记为白色,然后从根对象开始遍历内存中的对象,接着把直接引用的对象标记为灰色。
  • 再判断灰色集合中的对象是否存在子引用,不存在咋放入黑色集合,如果存在,就把子引用对象放入到灰色集合。

按照这样一个步骤不断推导,直到灰色集合中所有的对象变黑后,本轮标记完成。最后,还处于白色标记的对象就是不可达对象,可以直接被回收。

6. CPU 飙高系统反应慢怎么排查?

1.CPU 是整个电脑的核心计算资源,对于一个应用进程来说,CPU 的最小执行单元是线程。
1.导致CPU 飙高的原因有几个方面

  • CPU 上下文切换过多,对于 CPU 来说,同一时刻下每个 CPU 核心只能运行一个线程,如果有多个线程要执行,CPU 只能通过上下文切换的方式来执行不同的线程。
    • 上下文切换需要做两个事情

      • i.保存运行线程的执行状态
      • ii.让处于等待中的线程执行
    • 这两个过程需要 CPU 执行内核相关指令实现状态保存,如果较多的上下文切换会占据大量CPU 资源,从而使得 cpu 无法去执行用户进程中的指令,导致响应速度下降。在Java 中,文件IO、网络IO、锁等待、线程阻塞等操作都会造成线程阻塞从而触发上下文切换

  • CPU 资源过度消耗,也就是在程序中创建了大量的线程,或者有线程一直占用 CPU 资源无法被释放,比如死循环!
    CPU 利用率过高之后,导致应用中的线程无法获得CPU 的调度,从而影响程序的执行效率!

既然是这两个问题导致的CPU 利用率较高,于是我们可以通过 top 命令,找到 CPU利用率较高的进程,在通过种情况。
找到进程中CPU 消耗过高的线程,这里有两种:

  • a.CPU 利用率过高的线程一直是同一个,说明程序中存在线程长期占用 CPU 没有释放的情况,这种情况直接通过 jstack 获得线程的Dump 日志,定位到线程日志后就可以找到问题的代码。
  • b.CPU 利用率过高的线程id 不断变化,说明线程创建过多,需要挑选几个线程 id,通过jstack 去线程dump 日志中排查。

最后有可能定位的结果是程序正常,只是在 CPU 飙高的那一刻,用户访问量较大,导致系统资源不够

7. 关于什么是 JVM?

在这里插入图片描述
从这张图中可以看出 JVM 所处的位置,同时也能看出它两个作用:

  • 运⾏并管理Java 源码⽂件所⽣成的Class⽂件,
  • 在不同的操作系统上安装不同的JVM,从⽽实现了跨平台的保证。

⼀般情况下,对于开发者⽽⾔,即使不熟悉 JVM 的运⾏机制并不影响业务代码的开发,因为在安装完JDK 或者JRE 之后,其中就已经内置了JVM,所以只需要将Class⽂件 交给JVM 运⾏即可;

但当程序运⾏的过程中出现了问题,⽽这个问题发生在 JVM 层⾯的,那我们就需要熟悉JVM 的运⾏机制,才能迅速排查并解决 JVM 的性能问题。
我们先看下目前主流的 JVM HotSpot 的架构图,通过这张架构图,我们可以看出 JVM的大致流程是把一个 class 文件通过类加载器加载进系统,然后放到不同的区域,通过编译器编译。

在这里插入图片描述

7.1 Class Files

在Java 中,Class⽂件是由源码⽂件⽣成的,⾄于源码⽂件的内容,是每个Java 开发者在JavaSE 阶段的必备知识,这⾥就不再赘述了,我们可以关注⼀下 Class⽂件的格式,⽐如其中的常量池、成员变量、⽅法等,这样就能知道Java 源码内容在Class⽂件中的表示⽅式

7.2 Class Loader Subsystem

即类加载机制

Class⽂件加载到内存中,需要借助Java 中的类加载机制。类加载机制分为装载、链接和初始化,其主要就是对类进⾏查找、验证以及分配相关的内存空间和赋值

7.3 Runtime Data Areas

第三个部分Runtime Data Areas 也就是通常所说的运⾏时数据区

其解决的问题就是Class⽂件进入内存之后,该如何进⾏存储不同的数据以及数据该如何进⾏扭转。比如:Method Area 通常会储存由Class⽂件常量池所对应的运⾏时常量池、字段和⽅法的元数据信息、类的模板信息等;Heap 是存储各种Java 中的对象实例;Java Threads 通过线程以栈的⽅式运⾏加载各个⽅法;Native Internal Thread可以理解为是加载运⾏native 类型的⽅法;PC Register 则是保存每个线程执⾏⽅法的实时地址。

7.4 Garbage Collector

第四个部分Garbage Collector 也就是通常所说的垃圾回收
就是对运⾏时数据区中的数据进⾏管理和回收。回收机制可以基于不同的垃圾收集器,

⽐如Serial、Parallel、CMS、G1、ZGC 等,可以针对不同的业务场景选择不同的收集器,只需要通过JVM 参数设置 即可。如果我们打开hotspot 的源码,可以发现这些收集器其实就是对于不同垃圾收集算法的实现,核⼼的算法有 3 个:标记-清除、标记-整理、复制

7.5 JIT Compiler 和Interpreter

通俗理解就是翻译器,Class 的字节码指令通过JIT Compiler 和Interpreter 翻译成对应操作系统的CPU 指令,只不过可以选择解释执⾏或者编译执⾏,在HotSpot JVM默认采用的是这两种⽅式的混合。
在这里插入图片描述

7.6 JNI 的技术

如果我们想要找 Java 中的某个native⽅法是如何通过C 或者C++实现的,那么可以通过Native Method Interface 来进⾏查找,也就是所谓的 JNI 技术。

8. 有没有排查过线上 oom 的问题,如何排查的

OOM 是out of memory 的简称,表示程序需要的内存空间大于 JVM 分配的内存空间。 OOM 后果就是导致程序崩溃;可以通俗理解:程序申请内存过大,虚拟机无法满足。

导致OOM 错误的情况一般是:

  • 给JVM 虚拟机分配的内存太小,实际业务需求对内存的消耗比较多
  • Java 应用里面存在内存泄漏的问题,或者应用中有大量占用内存的对象,并且没办法及时释放。

内存泄漏和内存溢出是两个完全不一样的情况

  • 内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。

  • 内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出。常见的OOM 异常情况有两种

    • java.lang.OutOfMemoryError: Java heap space >java 堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数 -Xms,-Xmx 来修改。
    • java.lang.OutOfMemoryError: PermGen space 或 java.lang.OutOfMemoryError:MetaSpace >java 方法区,溢出了,一般出现在大量Class、或者采用 cglib 等反射机制的情况,因为这些情况会产生大量的Class 信息存储于方法区。
      这种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m
      -XX:MaxPermSize=256m 的形式修改。
      另外,过多的常量尤其是字符串也会导致方法区溢出。
      遇到这类问题,通常的排查方式是,先获取内存的 Dump 文件。 Dump 文件有两种方式来生成,
  • 第一种是配置JVM 启动参数,当触发了OOM 异常的时候自动生成

  • 第二种是使用jmap 工具来生成。然后使用MAT 工具来分析Dump 文件。

  • 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。掌握了泄漏对象的类信息和 GC Roots 引用链的信息,就可以比较准确地定位泄漏代码的位置。

  • 如果是普通的内存溢出,确实有很多占用内存的对象,那就只需要提升堆内存空间即可。

9. 说一下你对双亲委派的理解

首先,我简单说一下类的加载机制(如图),就是我们自己写的 java 源文件到最终运行,必须要经过编译和类加载两个阶段。

  • 编译的过程就是把.java 文件编译成.class 文件。
  • 类加载的过程,就是把 class 文件装载到JVM 内存中,装载完成以后就会得到一个 Class对象,我们就可以使用 new 关键字来实例化这个对象。

在这里插入图片描述
(如图)而类的加载过程,需要涉及到类加载器。
JVM 在运行的时候,会产生 3 个类加载器,这三个类加载器组成了一个层级关系每个类加载器分别去加载不同作用范围的 jar 包,比如

  • Bootstrap ClassLoader,主要是负责Java 核心类库的加载,也就是 %{JDK_HOME}\lib 下的rt.jar、resources.jar 等
  • Extension ClassLoader,主要负责%{JDK_HOME}\lib\ext 目录下的jar 包和class文件
  • Application ClassLoader,主要负责当前应用里面的 classpath 下的所有jar 包和类文件

除了系统自己提供的类加载器以外,还可以通过 ClassLoader 类实现自定义加载器,去满足一些特殊场景的需求。
在这里插入图片描述
(如图)所谓的父委托模型,就是按照类加载器的层级关系,逐层进行委派。
比如当需要加载一个 class 文件的时候,首先会把这个 class 的查询和加载委派给父加载器去执行,如果父加载器都无法加载,再尝试自己来加载这个 class。

这样设计的好处,我认为有几个。

  • 安全性,因为这种层级关系实际上代表的是一种优先级,也就是所有的类的加载,优先给Bootstrap ClassLoader。那对于核心类库中的类,就没办法去破坏,比如自己写一个java.lang.String,最终还是会交给启动类加载器。再加上每个类加载器的作用范围,那么自己写的 java.lang.String 就没办法去覆盖类库中类。
  • 我认为这种层级关系的设计,可以避免重复加载导致程序混乱的问题,因为如果父加载器已经加载过了,那么子类就没必要去加载了。

10. JVM 如何判断一个对象可以被回收

在JVM 里面,要判断一个对象是否可以被回收,最重要的是判断这个对象是否还在被使用,只有没被使用的对象才能回收。

10.1 引用计数器

也就是为每一个对象添加一个引用计数器,用来统计指向当前对象的引用次数,如果当前对象存在应用的更新,那么就对这个引用计数器进行增加,一旦这个引用计数器变成 0,就意味着它可以被回收了。
这种方法需要额外的空间来存储引用计数器,但是它的实现很简单,而且效率也比较高;
在这里插入图片描述
不过主流的JVM 都没有采用这种方式,因为引用计数器在处理一些复杂的循环引用或者相互依赖的情况时,
可能会出现一些不再使用但是又无法回收的内存,造成内存泄露的问题。
在这里插入图片描述

10.2 可达性分析

它的主要思想是,首先确定一系列肯定不能回收的对象作为 GC Root,比如虚拟机栈里面的引用对象、本地方法栈引用的对象等,然后以 GC ROOT 作为起始节点,
从这些节点开始向下搜索,去寻找它的直接和间接引用的对象,当遍历完之后如果发现有一些对象不可到达,
那么就认为这些对象已经没有用了,需要被回收。

在垃圾回收的时候,JVM 会首先找到所有的GC root,这个过程会暂停所有用户线程,也就是stop the world,然后再从GC Roots 这些根节点向下搜索,可达的对象保留,不可达的就会回收掉。
可达性分析是目前主流 JVM 使用的算法。
在这里插入图片描述

11. JVM 分代年龄为什么是 15 次?可以 25 次吗?

首先,在 JVM 的heap 内存里面,分为 Eden Space、Survivor Space、Old Generation(如图)。
在这里插入图片描述

当我们在Java 里面使用new 关键字创建一个对象的时候,JVM 会在Eden Space 分配一块内存空间来存储这个对象。
当Eden Space 的内存空间不足的时候,会触发 Young GC 进行对象回收。
那些因为存在引用关系而无法回收的对象,JVM 会把它们转移到 Survivor Space。

Survivor Space 内部又分为From 区和To 区,刚从Eden 区转移过来的对象会分配到From 区,
每经历一次Young GC,这些没有办法被回收的对象就会在 From 区和To 区来回移动,每移动一次,这个对象的
GC 年龄就加 1。默认情况下 GC 年龄达到 15 的时候,JVM 就会把这个对象移动到 Old Generation。

其次呢,一个对象的 GC 年龄,是存储在对象头里面的(如图),一个 Java 对象在JVM内存中的布局由三个部分组成,分别是对象头、实例数据、对齐填充。而对象头里面有 4 个bit 位来存储GC 年龄。
在这里插入图片描述
而 4 个bit 位能够存储的最大数值是 15,所以从这个角度来说,JVM 分代年龄之所以设置成 15 次是因为它最大能够存储的数值就是 15。

虽然JVM 提供了参数来设置分代年龄的大小,但是这个大小不能超过 15。
而从设计角度来看,当一个对象触发了最大值 15 次gc,还没有办法被回收,就只能移动到old generation 了。

另外,设计者还引入了动态对象年龄判断的方式来决定把对象转移到 old generation,也就是说
不管这个对象的 gc 年龄是否达到了 15 次,只要满足动态年龄判断的依据,也同样会转移到old generation。

12. 一个空 Object 对象的占多大空间?

  • 在开启了压缩指针的情况下,Object 默认会占用 12 个字节,但是为了避免伪共享问题,JVM 会按照 8 个字节的倍数进行填充,所以会填充 4 个字节变成 16 个字节长度。
  • 在关闭压缩指针的情况下,Object 默认会占用 16 个字节,16 个字节正好是 8 的整数倍,因此不需要填充。

在HotSpot 虚拟机里面,(如图)一个对象在堆内存里面的内存布局是使用OOP 结构来表示的,
它主要分为三个部分。
在这里插入图片描述

  • 对象头,包括 Markword、类元指针、数组长度
    其中Markword 用来存储对象运行时的相关数据,比如hashCode、gc 分代年龄等。在 64 位操作系统中占 8 个字节,32 位操作系统中占 4 个字节,类元指针指向当前实例对象所属哪个类,开启指针压缩的情况下占 4 个字节,未开启则占 8 个字节,数组长度只有对象数组才会存在,占 4 个字节
  • 实例数据,存储对象中的字段信息;
  • 对齐填充,Java 对象的大小需要按照 8 个字节或者 8 个字节的倍数对齐,避免伪共享问题。
    在这里插入图片描述
    因此,一个空的对象,在开启压缩指针的情况下,占 16 个字节,其中Markword 占 8 个字节、类元指针占 4 个字节, 对齐填充占 4 个字节。

13. 什么是 Java 虚拟机,为什么要使用?

Java 虚拟机是Java 语言的运行环境。
之所以需要Java 虚拟机,主要是为 Java 语言提供Write Once,Run Anywhere 能力。实际上,一次编写,到处运行这个能力本身是不可能实现的。因为不同的操作系统和硬件。
最终执行的指令会有较大的差异。
而Java 虚拟机就是解决这个问题的,它能根据不同的操作系统和硬件差异,生成符合这个平台机器指令。

简单理解,它就相当于一个翻译工具,在window 下,翻译成window 可执行的指令,在linux 下,
翻译成linux 下可执行的指令。除了这个因素以为,我认为自动回收垃圾这个功能也是原因之一,它让开发者省去了垃圾回收这个工作。减少了程序开发的复杂性。

14. JVM 为什么使用元空间替换了永久代?

14.1 jdk7

在Hotspot 虚拟机中,方法区的实现是在永久代里面,它里面主要存储运行时常量池、 Klass 类元信息等。
永久代属于JVM 运行时内存中的一块存储空间,我们可以通过-XX:PermSize 来设置永久代的大小。
当内存不够的时候,会触发垃圾回收。
在这里插入图片描述

14.2 jdk8

在JDK1.8 里面,JVM 运行时数据区是这样的(如图)
在Hotspot 虚拟机中,取消了永久代,由元空间来实现方法区的数据存储。 元空间不属于JVM 内存,而是直接使用本地内存,因此不需要考虑GC 问题。
默认情况下元空间是可以无限制的使用本地内存的,但是我们也可以使用JVM 参数来限制内存使用大小。

在这里插入图片描述

14.3 为什么要使用元空间来替换永久代

  • 在 1.7 版本里面,永久代内存是有上限的,虽然我们可以通过参数来设置,但是 JVM加载的class 总数、大小是很难确定的。但是元空间是存储在本地内存里面,内存上限比较大,可以很好的避免这个问题。
  • 永久代的对象是通过 FullGC 进行垃圾收集,也就是和老年代同时实现垃圾收集。替换成元空间以后,简化了 Full GC。可以在不进行暂停的情况下并发地释放类数据,同时也提升了GC 的性能
  • Oracle 要合并Hotspot 和JRockit 的代码,而JRockit 没有永久代。以上就是我对这个问题的理解。

15. Java 常见的垃圾收集器有哪些?

实际上,垃圾收集器(GC,Garbage Collector)是和具体 JVM 实现紧密相关的,不同厂商(IBM、Oracle),不同版本的 JVM,提供的选择也不同。接下来,我来谈谈最主流的Oracle JDK。

15.1 Serial GC

Serial GC,它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。
当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是Client 模式下 JVM 的默认选项。从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(MarkCompact)算法,区别于新生代的复制算法。 Serial GC 的对应 JVM 参数是-XX:+UseSerialGC

15.2 ParNew GC

很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作,下面是对应参数-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

15.3 Parrallel GC

在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC选择,也被称作是吞吐量优先的 GC
它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。开启选项是:-XX:+UseParallelGC 另外,Parallel GC 引入了开发者友好的配置项,我们可以直接设置暂停时间或吞吐量等目标,JVM 会自动进行适应性调整,例如下面参数:
-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC 时间和用户时间比例= 1 / (N+1)

15.4 CMS(Concurrent Mark Sweep) GC

基于标记 - 清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间

这一点对于 Web 等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC。
但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。

15.5 G1 GC

这是一种兼顾吞吐量和停顿时间的 GC 实现,是 Oracle JDK 9 以后的默认 GC 选项。

G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。

G1 GC 仍然存在着年代的 概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region。 Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。

G1 吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时 CMS 已经在 JDK9 中被标记为废弃(deprecated),所以 G1 GC 值得你深入掌握。

16. 如何破坏双亲委派模型

我们自己写的java 源文件到最终运行,必须要经过编译和类加载两个阶段(如图)。

  • 编译的过程就是把.java 文件编译成.class 文件。
  • 类加载的过程,就是把 class 文件装载到JVM 内存中,装载完成以后就会得到一个 Class对象,我们就可以使用 new 关键字来实例化这个对象。
    在这里插入图片描述
    (如图)而类的加载过程,需要涉及到类加载器。

JVM 在运行的时候,会产生 3 个类加载器,这三个类加载器组成了一个层级关系每个类加载器分别去加载不同作用范围的 jar 包,比如

  • Bootstrap ClassLoader,主要是负责Java 核心类库的加载,也就是 %{JDK_HOME}\lib 下的rt.jar、resources.jar 等
  • Extension ClassLoader,主要负责%{JDK_HOME}\lib\ext 目录下的jar 包和class文件
  • Application ClassLoader,主要负责当前应用里面的 classpath 下的所有jar 包和类文件

除了系统自己提供的类加载器以外,还可以通过 ClassLoader 类实现自定义加载器,去满足一些特殊场景的需求。
(如图)而双亲模型,就是按照类加载器的层级关系,逐层进行委派。比如当需要加载一个 class 文件的时候,首先会把这个 class 的查询和加载委派给父加载器去执行,如果父加载器都无法加载,再尝试自己来加载这个 class。
在这里插入图片描述
不过,双亲委派并不是一个强制性的约束模型,我们可以通过一些方式去打破双亲委派模型。
这个打破的意思,就是类加载器可以加载不属于当前作用范围的类,实际上,JVM 本身就存在双亲委派被破坏的情况。

  • 第一种情况,双亲委派是在 JDK1.2 版本发布的,而类加载器和抽象类 ClassLoader 在 JDK1.0 就已经存在了,用户可以通过重写 ClassLoader 里面的loadClass()方法实现自定义类加载,JDK1.2 为了向前兼容,所以在设计的时候需要兼容 loadClass()重写的实现,导致双亲委派被破坏的情况。同时,为了避免后续再出现这样的问题,不在提倡重写 loadClass()方法,而是使用 JDK1.2 中ClassLoader 中提供了findClass 方法来实现符合双亲委派规则的类加载逻辑。
  • 第二种情况,在这个类加载模型中,有可能存在顶层类加载器加载的类,需要调用用户类加载器实现的代码的情况。比如java.jdbc.Driver 接口,它只是一个数据库驱动接口,这个接口是由启动类加载器加载的,但是java.jdbc.Driver 接口的实现是由各大数据库厂商来完成的,既然是自己实现的代码,就应该由应用类加载器来加载。于是就出现了启动类加载器加载的类要调用应用类加载器加载的实现。
    为了解决这个问题,在 JVM 中引入了线程上下文类加载器,它可以把原本需要启动类加载器加载的类,由应用类加载器进行加载。除此之外,像 Tomcat 容器,也存在破坏双亲委派的情况,来实现不同应用之间的资源隔离。

小总结:
有两种方式来破坏双亲委派模型

  • 第一种,集成 ClassLoader 抽象类,重写 loadClass 方法,在这个方法可以自定义要加载的类使用的类加载器。
  • 第二种,使用线程上下文加载器,可以通过 java.lang.Thread 类的 setContextClassLoader()方法来设置当前类使用的类加载器类型。

17. CMS 垃圾回器的工作原理

CMS (Concurrent Mark and Sweep) 是一种低停顿垃圾回收器
它主要通过并发标记阶段并发清除阶段两个并发的阶段来实现垃圾回收

它的整体流程可以分成四个步骤(如图):

  • 初始标记(CMS initial mark):这个阶段需要Stop The Word,来标记哪些对象是需要回收的,这个过程只需要标记GC Roots 能够直接关联的对象,所以速度很快,对性能影响比较小。
  • 并发标记(CMS concurrent mark):扫描整个堆中的对象,标记所有不需要回收的对象。这个阶段不需要 Stop The Word,在应用程序运行过程中进行标记。
  • 重新标记(CMS remark):为了修正并发标记期间,应用程序同步运行导致标记产生变动的那一部分对象。这个阶段需要 Stop The Word。
  • 并发清除(CMS concurrent sweep),CMS 会并发执行清除操作,同时应用程序继续运行,最大力度减少对性能的影响。
    在这里插入图片描述

18. 聊一下G1垃圾回收器

G1是JDK9默认的垃圾收集器,代替了CMS收集器。它的目标是达到更高的吞吐量和更短的GC停顿时间。

18.1 G1优点

18.1.1 并发和增量式回收

不像CMS要全部STW,G1可以渐进式回收,不停顿太久。

// G1CollectedHeap.java
void collectGarbage(G1ConcurrentMark mark) {
    initial-mark; // STW
    remark();     // Concurrent 
    cleanup();    // STW
    concurrent-cleanup();  // Concurrent
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

18.1.2 分代回收

不需要一次全堆回收,可以分代增量回收,选择性回收新生代和老年代。

void collectGarbage(boolean collectOnlyYongGen) {
    if (collectOnlyYongGen) {
        collectYoungGenGarbage();  // only YongGen
    } else {
        collectGarbage(); // YongGen and Old Gen
    }
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

18.1.3 空间整合

通过Remembered Sets实现空间整合,解决碎片问题。

// G1RemSet.java
void addToRememberedSets(HeapRegion from, HeapRegion to) { 
    from.addRememberedSetEntry(to); 
}
  • 1
  • 2
  • 3
  • 4
  • 预测分析:通过限定垃圾产生速率,动态调整回收频率与时间,实现高吞吐量。

18.2 “Remembered Sets”和“卡片表”

Remembered Sets和Card Tables都是G1用来管理堆和处理垃圾回收的重要数据结构。

  • Remembered Sets:记录不同Region之间的引用关系,用于判定垃圾。由于G1采用分代和分片回收,需要记录新生代和老年代以及各个Region之间的引用链,这就是Remembered Sets要做的工作。
  • Card Tables:由Remembered Sets维护的引用链过于精细,代价太大。所以,G1引入Card Tables,按照内存块做了分段,如果一个分段里至少有一个对象被老年代引用,则标记整个分段为”脏“。在回收时只处理”脏“的分段,提高效率。

它们的工作可以简述为:

  • Remembered Sets记录精细的引用信息
  • Card Tables进行概括性标记,在GC时结合使用,达到高精度且高性能的铁子回收效果。

可以看到,Remembered Sets和Card Tables是G1高效率回收的关键,它们让G1不需要像CMS那样全堆回收,可以有选择性地、增量式地进行分代、分片的回收,极大的提高了工作效率。

18.3 G1的垃圾回收过程

G1的垃圾回收过程可以分为以下几个主要阶段:

18.3.1 初始标记

标记GC Roots能直接关联的对象,需要Stop The World。

private void initialMark() {
    for (Object obj : strongRefs) {
        G1CollectedHeap.mark(obj);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5

18.3.2 并发标记

从GC Roots开始对堆中对象进行并发标记,需要部分STW。

18.3.3 重新标记

修正并发标记期间的错误标记,需要STW。

18.3.4 筛选回收

根据标记和Card Table结果筛选回收区域,回收垃圾,需要STW。

// 筛选待回收区域
void selectGarbageCollectionCandidates() {
    Region[] filtered = filterRegions(); 
    garbageCollect(filtered);
}
  • 1
  • 2
  • 3
  • 4
  • 5

18.3.4 并发清理

与用户线程一起工作,对标记和筛选阶段误差产生的垃圾链进行清理。

18.3.4 并发重置

与用户线程一起工作,为下次GC做准备。

这一过程实际上和CMS非常相似,同为“标记-清除”算法。但G1在并发标记的基础上,通过Remembered Sets和Card Tables实现了分代回收和空间整合,这也是它能达到高性能的关键。

18.4 小总结:

  • G1继承了“标记-清除”算法的思想,但已远非传统意义上的“标记-清除”。
  • G1引入了Remembered Sets和Card Tables,实现了细致且高效的分代、分片增量回收,这是它的重要创新点。
    所以,G1是在“标记-清除”思想上做出重大改进、发展和优化而成的一种高性能垃圾收集器,将它简单归类为“标记-清除”算法已忽略其最关键的优点。
  • G1与CMS一脉相承,但已大大超越,其性能和效率甚至与“复制”算法接近,堪称一代新高。 所以,更准确的说法应是:G1继承了标记-清除模型,但在算法和实现上都已经有了重大创新,超越了传统标记-清除算法,达到一种混合模型与新高度,是一款高性能、高效率的收集器。

19. 元空间有什么?以及什么情况下触发oom?

元空间是Java 8及以后版本中的概念,它用来替代永久代,存放加载的类信息、常量、静态变量、JIT编译后的代码等,使用的是本地内存。

元空间中存储的类信息、常量、静态变量等不再受到限制,可以随时地加载、卸载,因此,元空间出现内存溢出(OOM)的情况较多,例如:

  • 在Java 7之前,频繁错误使用String.intern方法。
  • 生成了大量的代理类,导致方法区被撑爆,无法卸载。
  • 应用长时间运行,没有重启。
  • JVM的元空间设置过小等。

解决元空间内存溢出的问题有以下几个方法:

  • 获取dump文件,通过MAT工具查看是否由于反射而生成的代理类,如果有的话需要找到对应代码中的可能生成大量反射操作的地方。
  • 检查JVM的参数设置中是否存在元空间设置过小的情况。
  • 如果以上都没有找到的话尝试重启JVM(非必要不要用)。

19.1 cglib为啥会撑爆方法区

cglib是一个第三方库,用于在运行时生成和修改Java字节码。它能够在运行时生成代理类,这是一种很强大的技术,但也意味着它会创建很多新的类和对象。因此,如果使用不当,它可能会消耗大量的方法区内存,甚至导致内存溢出错误。

"撑爆方法区"这个说法,其实是因为Java虚拟机(JVM)在运行时需要为每个加载的类保留一些元数据,这些元数据存储在方法区中。如果方法区的内存不足以存储这些元数据,就会抛出内存溢出错误。

当使用cglib动态生成大量类时,这些新生成的类也会在方法区中占用空间。如果生成的类过多,方法区的内存可能会被迅速消耗完,导致内存溢出错误。

要避免这种情况,可以尝试以下方法:

减少动态生成的类的数量。
尽量复用对象,避免创建过多的新对象。
调整JVM的参数,如增大方法区的内存大小(-XX:MaxPermSize)。
如果可能,使用其他库或技术替代cglib,如ByteBuddy、Javassist等。
请注意,具体的解决方案可能会根据你的应用场景和具体的问题而有所不同。

19.2 应用长时间运行,没有重启, 这种情况下为啥会导致元空间oom?

应用长时间运行,没有重启的情况下,会导致元空间OOM,是因为应用在运行过程中,随着时间的推移,会创建大量的对象,这些对象会占据内存空间。如果这些对象没有被及时回收,就会导致内存中的空间被逐渐消耗殆尽,最终触发元空间OOM错误。

20. oom场景情况

20.1 堆内存溢出

  • 对象生命周期过长:如果某个对象的生命周期过长,而且该对象占用的内存很大,那么在不断创建新对象的过程中,堆内存会被耗尽,从而导致内存溢出。这种情况一般出现在用集合当缓存,却忽略了缓存的淘汰机制。
  • 无限递归:递归调用中缺少退出条件或递归深度过大,会导致空间耗尽,引发溢出错误。往往在测试环境就会发现该问题,不会暴露在生产环境;
  • 大数据集合:在处理大量数据时,如果没有正确管理内存,例如加载过大的文件、查询结果集过大等,会导致内存溢出。
  • JVM配置不当:如果JVM的内存参数配置不合理,例如堆内存设置过小,无法满足应用程序的内存需求,也会导致内存溢出。

20.2 内存泄漏

  • 对象的引用未被正确释放:如果在使用完一个对象后,忘记将其引用置为 null 或者从数据结构中移除,那么该对象将无法被垃圾回收,导致内存泄漏。比如 ThreadLocal。
  • 长生命周期的对象持有短生命周期对象的引用:如果一个长生命周期的对象持有了一个短生命周期对象的引用,即使短生命周期对象不再使用,由于长生命周期对象的引用仍然存在,短生命周期对象也无法被垃圾回收,从而造成内存泄漏。
  • 过度使用第三方库:某些第三方库可能存在内存泄漏或者资源未正确释放的问题,如果使用不当或者没有适当地管理这些库,可能会导致内存溢出。
  • 集合类使用不当:在使用集合类时,如果没有正确地清理元素,当集合不再需要时,集合中的对象也不会被释放,导致内存泄漏;
  • 资源未正确释放:如果程序使用了诸如文件、数据库连接、网络连接等资源,在不再需要这些资源时没有正确释放,会导致资源泄漏,最终导致内存泄漏。

20.3 栈溢出

  • 当递归调用的层级过深,栈空间无法容纳更多的方法调用信息时,会引发 StackOverflowError 异常,这也是一种 OOM 异常

20.4 元空间(Metaspace)耗尽

元空间是 Java 8 及以后版本中用来存储类元数据的区域。它取代了早期版本中的永久代(PermGen)。元空间主要用于存储类的结构信息、方法信息、静态变量以及编译后的代码等。

当程序加载和定义大量类、动态生成类、使用反射频繁操作类等情况下,可能会导致元空间耗尽。
常见导致元空间耗尽的情况包括:

  • 类加载过多:如果应用程序动态加载大量的类或者使用动态生成类的方式,会导致元空间的使用量增加。如果无法及时卸载这些类,元空间可能会耗尽。
  • 字符串常量过多:Java中的字符串常量会被存储在元空间中。如果应用程序中使用了大量的字符串常量,尤其是较长的字符串,可能会导致元空间的耗尽。
  • 频繁使用反射:反射操作需要大量的元数据信息,会占用较多的元空间。如果应用程序频繁使用反射进行类的操作,可能会导致元空间耗尽。
  • 大量动态代理:动态代理是一种使用反射创建代理对象的技术。如果应用程序大量使用动态代理,将会生成大量的代理类,占用较多的元空间。
  • 未正确限制元空间大小:默认情况下,元空间的大小是不受限制的,它会根据需要动态扩展。如果没有正确设置元空间的大小限制,或者限制过小,可能会导致元空间耗尽。

21.当Java线程在处理请求时,抛出OOM异常,整个进程是否仍能处理请求。

结论:

  • 内存溢出情况下,若GC速度跟不上内存分配速度,导致OOM并杀死线程,通常整个进程仍能继续处理请求。
  • 内存泄漏情况下,未能回收的内存可能引发OOM,继而杀死线程以防止无法回收对象继续产生。此时,那些不占用大量内存的线程可能会继续执行,但那些耗费大量内存的线程可能会无法执行。极端情况下,进程可能会崩溃。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/930952
推荐阅读
相关标签
  

闽ICP备14008679号