备战秋招冲击大厂Java面试题系列—JVM及垃圾回收机制

Posted Java-桃子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了备战秋招冲击大厂Java面试题系列—JVM及垃圾回收机制相关的知识,希望对你有一定的参考价值。

1. JVM

  • JVM的作用:解释运行字节码程序消除平台相关性。jvm将java字节码解释为具体平台的具体指令。一般的高级语言如要在不同的平台上运行,至少需要编译成不同的目标代码。而引入JVM后,Java语言在不同平台上运行时不需要重新编译。Java语言使用模式Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

  • JVM知识结构图

  • JVM内存结构图

  • 程序计数器:内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

  • 虚拟机栈:线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

  • 本地方法栈:Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

  • 堆:对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

  • 方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • 运行时常量池:属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

  • 直接内存:非虚拟机运行时数据区的部分

2. 虚拟机栈结构

3. Java内存模型(JMM)

Java内存模型(简称JMM),是一种规范。JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。线程对变量的操作只能在各自线程的工作内存操作。


JMM的三大特性:

  • 可见性
  • 原子性
  • 有序性

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。其关系模型图如下图所示:

 

4. JVM 调优,查看 JVM 参数默认值

  • jps -v 可以查看 jvm 进程显示指定的参数
  • 使用 -XX:+PrintFlagsFinal 可以看到 JVM 所有参数的值
  • jinfo 可以实时查看和调整虚拟机各项参数
  • jstat -gc 12538 5000即会每5秒一次显示进程号为12538的java进程的GC情况

5. OOM排查方法

1)先查看应用进程号pid:ps -ef | grep 应用名
2)查看pid垃圾回收情况:  jstat  -gc  pid  5000(时间间隔)
3)开启OOM快照: 
  -XX:+HeapDumpOnOutOfMemoryError(开启堆快照)
  -XX:HeapDumpPath=C:/m.hprof(保存文件到哪个目录)
4)dump 查看方法栈信息:jstack  -l  pid  >  /home/test/jstack.txt
5)dump 查看JVM内存分配以及使用情况:jmap  -heap  pid  >  /home/test/jmapHeap.txt
6)dump jvm二进制的内存详细使用情况

6. 类加载

  • 类加载器:

  • 类加载过程:加载,验证,准备,解析,初始化

  • 加载:加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。

  • 验证:确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 准备:准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。

  • 解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。

  • 初始化:初始化阶段是执行类构造器<client>方法的过程。到了初始阶段,才开始真正执行类中定义的Java程序代码。

  • 双亲委派模型

    • 定义:除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
    • 工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。
    • 优点:使用双亲委派模型来组织类加载器之间的关系,使得Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放再rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
    • 打破双亲委派机制的方法:重写loadclass()方法
    • 打破双亲委派机制的例子:
      • Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。打破的目的是为了完成应用间的类隔离。
      • JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。
    • 沙箱安全机制:防止恶意代码污染java源代码。

7. JVM加载class文件的原理

JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件的类。


Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。


类装载方式,有两种

  • 隐式装载,程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中
  • 显式装载,通过class.forname()等方法,显式加载需要的类,隐式加载与显式加载的区别:两者本质是一样的。
      Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

8. GC Roots

作为GCRoots的对象包括下面几种:


1)虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
2)方法区中的类静态属性引用的对象。
3)方法区中常量引用的对象。
4)本地方法栈中JNI(Native方法)引用的对象。

9. 对象可达性分析

  • 如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程去处理。
  • 对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。

10. 垃圾回收机制

堆分为新生代和老年代,新生代默认占总空间的 1/3,老年代默认占 2/3。新生代使用复制算法,有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。


当新生代中的 Eden 区内存不足时,就会触发 Minor GC,过程如下:


1)在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
2)Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
3)移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代
4)Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%
5)Survivor 区内存不足会发生担保分配
6)年龄超过指定大小的对象可以直接进入老年代
7)Major GC,指的是老年代的垃圾清理,但并未找到明确说明何时在进行Major GC
8)FullGC,整个堆的垃圾收集,触发条件:

  • 每次晋升到老年代的对象平均大小>老年代剩余空间
  • MinorGC后存活的对象超过了老年代剩余空间
  • 元空间不足
  • System.gc()
  • CMS GC异常,promotion failed:MinorGC时,survivor空间放不下,对象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC时,同时有对象要放入老年代,而老年代空间不足造成
  • 堆内存分配很大的对象

11. 垃圾回收算法

  • 引用计数法:给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任何时刻计数值为0的对象就是不可能再被使用的。
    • 缺点:(1)每次给对象赋值时都要维护引用计数器,且计数器本身也有一定的消耗;(2)较难处理循环引用
  • 复制算法:(年轻代)
    • 优点:(1)不产生内存碎片;(2)速度快
    • 缺点:浪费10%的内存空间
  • 标记清除算法(老年代):先标记出要回收的对象,然后统一回收这些对象
    • 优点:节省空间
    • 缺点:产生内存碎片;
  • 标记整理算法(老年代):先标记清除,再次扫描并往一端滑动存活对象。
    • 优点:不产生内存碎片;
    • 缺点:需要移动对象的成本,耗时严重

12. 垃圾收集器

1)Serial收集器:新生代收集器,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)

2)Serial Old收集器:老年代收集器,单线程收集器,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标 记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。


3)ParNew收集器:新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。

4)Parallel Scavenge 收集器:新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用 Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)


5)Parallel Old收集器:老年代收集器,多线程,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清 理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。

6)CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间,使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见后面),当用户线程内存不足时,采用备用方案Serial Old收集。解决内存碎片问题:让CMS在进行一定次数的Full GC(标记清除)的时候进行一次标记整理算法。

  • 初始标记:标记GC roots直接关联的对象,速度快
  • 并发标记:GC Roots Tracing过程
  • 重新标记:修正并发标记期间用户进程继续运行而产生变化的标记,耗时比初始标记长,但远小于并发标记
  • 并发清除:清除标记的对象

7)G1(Garbage First)收集器:区域化垃圾收集器,物理上不区分新生代和老年代。采用标记整理的算法,与CMS相比:(1)不会产生内存碎片;(2)用户可以指定垃圾回收时间。G1 收集器,相关配置如下:

-Xmx12g
-Xms12g
-XX:+UseG1GC
-XX:InitiatingHeapOccupancyPercent=45
-XX:MaxGCPauseMillis=200
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
  • G1将新生代,老年代的物理空间划分取消了。
  • G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。H区域可以是连续的用来分配比较大的对象

8)ZGC
ZGC给Hotspot Garbage Collectors增加了两种新技术:着色指针和读屏障。ZGC的标记分为三个阶段。

  • 第一阶段是STW,其中GC roots被标记为活对象。
  • 第二阶段,同时遍历对象图并标记所有可访问的对象。在此阶段期间,读屏障针使用掩码测试所有已加载的引用,该掩码确定它们是否已标记或尚未标记,如果尚未标记引用,则将其添加到队列以进行标记。
  • 在遍历完成之后,有一个最终的,时间很短的的Stop The World阶段,这个阶段处理一些边缘情况(我们现在将它忽略),该阶段完成之后标记阶段就完成了。

以上是关于备战秋招冲击大厂Java面试题系列—JVM及垃圾回收机制的主要内容,如果未能解决你的问题,请参考以下文章

备战秋招冲击大厂Java面试题系列—Java集合

备战秋招冲击大厂Java面试题系列—Redis

备战秋招冲击大厂Java面试题系列—并发编程

备战秋招冲击大厂Java面试题系列—数据库

备战秋招冲击大厂Java面试题系列—数据结构与算法

2021 备战秋招新鲜出炉的美团字节阿里腾讯等大厂综合 Java 岗面试题