JVM面试题整理

JVM面试题


备注:个人总结,仅供参考,如有错误,烦指出

1. 垃圾回收过程

垃圾回收大的流程分为2个阶段

  1. 查找可被回收的对象

    • 引用计数法
    • 可达性分析算法
  2. 执行回收

    • 对于新生代采用复制算法

    • 对于老年代使用标记整理/标记清除

      总的来说就是采用分代收集的思想,对于不同的代采用不同的收集算法

2. new 对象的过程
  1. 触发类加载机制:当jvm碰到new指令的时候会去检查对应的类有没有被加载过,如果已经加载过那么直接进入第二步,如果没有被加载过的便会触发类加载机制,至于类加载机制这里暂时先不展开讲了,总之第一步如果是初次new一个类的对象的话,便会触发类加载机制

  2. 分配内存空间:类加载机制完成之后,这个对象所需要的空间大小就已经确定下来,接下来便会在堆上分配内存空间,分配内存空间又有两种方式

    • 指针碰撞
    • 空闲列表

    至于选择哪种方式去分配内存,那得看你的堆是否规整,也就是堆内存是否处于连续的空间,而堆是否规整又取决于使用的是哪种垃圾收集器,至于垃圾收集器这里暂时不展开讲了,总之第二步就是分配内存空间

  3. 初始化零值:所谓初始化零值,就是对刚才分配好内存空间的对象属性去赋一个默认值,比如说int默认为0,boolean默认为false

  4. 设置对象头:比如说设置你这个对象是哪个类的实例,你的hashCode是多少,你的gc分代年龄等等信息,这一步做完,在虚拟机层面一个对象已经new出来了,但是对于应用程序来说,还不算一个完整的对象,因为还没有执行init方法

  5. 执行init:当init方法执行之后,这个对象已经完全生产出来,但是对于我们程序员来说的话还没有办法使用,因为我们没有拿到它的引用

  6. 引用指向实例:接下来把栈中的引用指向刚才创建的对象实例,至此,new对象彻底完成。

3. 对象的构成
  1. 对象头

    对象头的话又分为两部分

    • Mark Work

      hashCode、gc分代年龄、锁状态标识、线程持有的锁、偏向线程id、偏向时间戳等信息

    • 类型指针

      指的是你这个对象是属于哪个类的实例

    • 如果是数组的话还有一个数组长度

  2. 实例数据

    这个就比较好理解了,它存放的是你这个类的所有字段内容,包括自身的以及父类继承下来的

  3. 对齐填充

    事实上对齐填充就相当于是个凑数的,有时候有有时候没有,因为对象的大小必修是8字节的整数倍,如果对象头和实例数据加起来刚好是8字节的整数倍,那么就不需要对齐填充了。

4. 描述jvm内存结构

先画一张图

image-20200331142432534

上图是JVM运行时数据区

  1. 程序计数器

    程序计数器,它是属于线程私有的,用来记录当前线程执行字节码文件的行号的指示器,当然这是对于java方法来说,如果说当前线程执行的是native方法,那么程序计数器就是Undefined,用白话讲就是说这东西用来记录当前线程执行代码执行到了哪里了

  2. 栈(虚拟机栈)

    栈又称做虚拟机栈,栈它是用来描述方法执行过程的内存结构的,栈中存放的是栈帧,每个方法的调用到结束的过程,都对应着一个栈帧在栈中入栈到出栈的过程,也叫作压栈和弹栈,而栈里面的每个栈帧呢会由局部变量表、操作数栈、动态链接、方法出入口等信息

  3. 本地方法栈

    本地方法栈和虚拟机栈差不多,只不过他针对的是native方法,事实上虚拟机栈和本地方法栈都是JVM规范中规定的概念,而落地的实现是由具体的虚拟机来决定的,你想我们常用的HotSpot虚拟机,它是不区分虚拟机栈和本地方法栈的,都是同一个栈。

  4. 方法区

    存放了类信息,静态变量、常量等信息

  5. :默认情况下

    • 年轻代 1/3
      • Eden区 8/10
      • Survivor区
        • from(s0) 1/10
        • to(s1) 1/10
    • 老年代 2/3
5. 解释一下方法区、永久代、元空间
  1. 方法区: 在JVM规范中规定的这么一个概念,存放了类信息,静态变量、常量等信息,他只是这样的一个规定,就像我们写的抽象类里面没有实现的抽象方法一样,具体怎么实现还得看他实现类,而不同的虚拟机对于方法区的实现都略有差异,就拿我们最常用的HotSpot虚拟机来说,不同的jdk版本对于方法区的实现也不一样,1.7往前是永久代(PermGen space),1.8的时候是元空间(Meta Space)
  2. 永久代和方法区:1.8的元空间取代了1.7至往前的永久代,事实上这两者还是有些许差异的
6. 如何判断一个对象是否可以被收回
  1. 引用计数法(Java):这是一个简单高效的计算方法,但是他有缺陷,目前的JVM中并没有使用;
  2. 可达性分析算法又叫做根搜索算法也叫做引用链法:可达性分析算法提出GC Roots对象的概念,从GC Roots作为起始点,从这些点上往下搜索与之有引用关系的对象,那么他走过的这一条路径成为引用链,这个引用连所关联的对象都是可达对象,而与这个引用连没有关联的对象叫做不可达对象,那么这些不可达对象就是可回收对象,注意这里说的是可回收,并不是说一定会被回收
7. 什么对象可作为GC Roots对象
  1. 栈中栈帧里面局部变量表里的引用的对象
  2. 方法区中静态变量引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中引用的对象
8. 常用的垃圾收集算法
  1. Mark-Sweep 标记清除法

    介绍:标记清除法分为两个阶段,一个是标记另一个是清除,标记的话他会使用到可达性分析法(根搜索算法),这里就不展开讲了。

    缺点:标记清除法有两个问题,一是效率问题另外一个是空间问题,效率问题的话是因为标记和清除两个过程效率都不是很高,空间问题是因为清除过后会产生内存碎片,无法存放稍大对象,可能会提前触发GC

  2. 复制算法

    内存按照容量分为等大的两块区域,每次只使用其中一块区域,当这块区域内存用完了,就将存活的对象复制到另一块区域上去,然后将使用过的这块内存一次性清理掉。新生代的survivor区就是用的复制算法

    优点: 简单高效

    缺点: 可用内存缩短为原来的一半

  3. 标记整理法

    其标记过程与标记清除法中标记过程是一样的,但是接下来他会让存活对象都向一端移动,然后直接清理掉边界意外的区域内存

  4. 分代收集算法

    分代收集算法并不是新的垃圾收集算法,而是指对于不同的堆内存区域使用不同的垃圾收集算法

9. 为什么年轻代使用复制算法而老年代使用标记整理/标记清除
  1. 年轻代使用复制算法:

    第一因为复制算法效率高,第二因为年轻代对象的特点适合复制算法,为什么说它适合呢?因为年轻代对象98%都是朝生夕死的,经历一次youngGC之后,存活的对象很少,完全可以复制到另一块内存上去,就算万一存活的对象大于Young区的 10%,那也没关系,因为它有一个分配担保机制,通过分配担保机制把放不下的对象,存放到老年代去

  2. 老年代使用标记整理法:

    第一点,老年代的对象都比较稳定,也就是说存活率会比较高,那么这时候要用复制算法的时候,效率比较差,

    更关键的是第二点,没有额外的空间对老年代进行分配担保,所以老年代不能采用复制算法

10. 空间分配担保机制

Minor GC之前,JVM检测之前每次晋升到老年代对象的平均大小是否大于老年代剩余大小,如果大于的话直接改为Full GC,如果小于的话再去看JVM参数是否允许担保失败,如果允许的话就使用Minor GC,如果不允许的话还是改为Full GC

如果使用了Minor GC,又出现了担保失败,那么再重新发起一次Full GC

11. 对象分配规则
  1. 新生对象一般情况下会在Eden区分配内存,当Eden区没有足够的空间时,触发一次Minor GC
  2. 大对象直接进入老年代,至于多大的对象算大对象,JVM提供了参数区设置它的阈值,超过设置的阈值就是大对象
  3. 长期存活的对象进入老年代,对于长期存活的对象的定义是,默认情况下经历过15次MinorGC的的对象会进入老年代,而多少次这个阈值呢,也可以通过JVM参数指定
  4. 动态年龄的判断:如果Survivor区相同年龄的对象总和大于Survivor区的一半,那么该年龄的对象以及大于该年龄的对象,便会直接进入老年代
  5. 空间分配担保:指的是发生了MinorGC后,如果年轻代的对象如果放不下,那么久把这部分对象放到老年代。
12. 垃圾收集器分类

就说我们常用的HotSpot虚拟机的垃圾收集器来说

Serial新生代—Serial old老年代、

Parallel Sacvenge 新生代— Parallel old老年代

ParNew 新生代

CMS(Concurrent Mark Sweep)老年代

G1(Garbage First)通吃

  1. Serial垃圾收集器

    Serial收集器是最早的一个垃圾收集器,它是一个单线程收集器,至今还使用在Client模式下的新生代,它工作的时候需要STW,当然他的优点是并不需要多线程切换的开销

  2. Parallel Scavenge垃圾收集器

    Parallel Scavenge是多线程的收集器,跟ParNew有点类似,但是他的关注点在于达到一个可控制的吞吐量,所谓吞吐量是CUP运行用户代码的时间和CUP花费的总时间

  3. Parnew 垃圾收集器

    ParNew其实在设计上就是一个多线程的Serial,它采用多线程进行垃圾收集,但是依旧STW

  4. Serial Old垃圾收集器

    Serial的老年代版本,同样也是单线程,他可以作为CMS失败后的默认备用方案 采用Mark-Compact

  5. Parallel Old 垃圾收集器

    Parallel Scavenge的老年代版本,使用的是标记整理法,侧重于吞吐量。采用Mark-Compact

  6. CMS(Concurrent Mark Sweep)垃圾收集器

    cms的目标是获取最短的停顿时间,它总共分为4个步骤

    • 初始标记
    • 并发标记
    • 重新标记
    • 并发清除

    首先我说一下初始标记,初始标记它标记的是GC Roots对象直接关联的对象,至于什么叫做GC Roots对象待会再说,比如说A对象是GC Roots对象,A持有B,B持有C,那么初始标记只标记出B对象这一层,至于标记C对象,那是并发标记要干的事儿,还有一点初始标记是需要STW的。初始标记完成后便会进行并发标记,并发标记可以看成是GC线程与用户线程同时执行的,而不需要去STW,并发标记完成之后会进行重新标记,之所以有重新标记是因为并发标记的时候用户线程还在继续运行,所以需要重新标记对于这些标记的对象进行微调校正,重新标记之后便会进行并发清除,因为并发标记和并发清除耗时最长,所以我们可以不准确的说,CMS在工作的时候是与用户线程同时执行的,这也就是CMS名字的由来

    CMS的缺点

    • 对于CUP资源非常敏感:因为需要并发去工作,所以在多CUP下有优势很明显,但是CPU数量偏少的情况下,对程序性能影响还是特别大的,因为他会和用户线程抢占CPU时间片
    • 无法处理浮动垃圾:所谓浮动垃圾就是在CMS进行并发清除的时候,程序中产生的垃圾,因为没有被标记所以无法被清除,所以默认情况下CMS在老年代内存空间使用68%/92%的时候开始工作,预留空间是给浮动垃圾留的,但是如果预留空间不足存放浮动垃圾,就会产生“Concurrent mode failure”,这个时候就会启动预备方案使用Serial old来进行垃圾清理,这样的话STW会很长。所以说这个值设置的太高的话会产生大量的“Concurrent mode failure”,影响性能
    • 产生空间碎片:因为他使用标记清除的方式,所以会产生内存碎片,后果就是明明老年代内存还有很多,却无法找到一块连续的内存去存放大对象,就会造成full gc
  7. G1(Garbage First)

    G1讲究的是可控时间,在指定的时间内达到一个最大的回售效率,G1将堆内存分为若干个等大的Region,取消了新生代老年代物理上的内存划分,但是保留了新生代老年代的概念,这些Region分别存放Eden、Servivor、Old、Humongous的各种对象。

    G1的执行流程类似于CMS,也分为4个阶段

    • 初始标记:同CMS
    • 并发标记:同CMS
    • 最终标记:同CMS
    • 筛选回收:这也是G1可预测停顿的原因,首先对各个Region的回收价值和成本进行排序,然后根据用户期望的停顿时间来定制回收计划,事实上G1在内部维护了一个优先列表,优先选择回收价值最大的Region,这也就是Garbage-First的由来

    G1的GC分类:

    • Young GC:当Eden区满了的时候,引发YoungGC,存活的对象会被复制到Servivor或者Old区
    • Mixed GC: MixedGC是G1独有的概念,他会回收所有Young和部分Old,因为他会根据用户期望的停顿时间来确定回收性价比比较高的old垃圾
    • Full GC:巨型对象无法分配内存,YoungGC时没有足够的空间去复制,都会触发Full GC,这时候便会采用Serial Old单线程来处理,对性能影响非常大
13.G1和CMS的区别
  1. 堆空间划分:CMS把堆划分为年轻代老年代,年轻代又分为Eden和Servivor区,而G1在物理内存上取消了年轻代老年代的划分,取而代之的是Region,但是保留了这种分代的思想。
  2. 内存碎片:CMS采用标记清除法,必然会产生内存碎片,而G1从细节上来说的话采用的是复制算法
  3. 可预测停顿事件模型:这也是G1很大的一个特点,可以让使用者去指定垃圾收集时间
14. 方法区会被回收吗?

会被回收,方法区中主要是回收一些废弃的常量和无用的类,废弃的常量很好理解,但是所谓无用的类定义还是比较苛刻的

  • 该类 的所有实例已经被回收
  • 加载该类的ClassLoader已经被回收
  • 该类对应的Class对象没有在任何地方被引用,也就是说无法通过反射访问该类

在这种条件下,这个类是可以被回收的,但是只是可以,需要回收的话还得设置JVM参数

15. Minor GC、Major GC、Full GC、Mixed GC

首先介绍堆分区(参考4-5)

  1. Minor GC:新new处理的对象一般情况下会进入Eden区,当Eden满了的时候,会触发一次Minor GC,这个时候会把Eden区存活的对象复制到from Survivor区巴拉巴拉…(讲一波复制算法参考8-2),这时就是Minor GC大致流程,事实上在Minor GC的时候还有可能触发空间分配担保机制从而引发Full GC 这个待会再说

  2. Major GC:关于MajorGC,网上很多技术文档对这个描述的都很含糊,甚至在周志明的那本深入理解JVM中是这样表示的老年代GC(Major GC/Full GC),这样的感觉给人Major和Full GC是等价的,然后我再Oracle官方文档找到了答案,Major GC的工作范围只是老年代

  3. Full GC:说Full GC之前我得先介绍FullGC触发条件,大分为两类第一是Old区不够用了,第二是Metaspace不够用了,那么我们得明白老年代的对象是从哪里来的巴拉巴拉…(然后讲一波对象分配策略参考10,重点突出Old区对象是怎么来的)

    当Full GC被触发以后,老年代便会采用Mark-Sweep/Mark-Compact,也就标记-清除/标记-整理

    那至于到底是哪一种,那的看你老年代使用的是哪种垃圾收集器,像Serial old/Parallel old的话那就

    是标记整理,而CMS的话它使用的是标记清除(有时间的话给讲一波垃圾收集器参考11)

  4. Mixed GC:混合式垃圾回收,只针对G1 垃圾收集器,它是回年轻代跟部分老年代

15.内存溢出

在Java里面内存溢出是可能是堆、栈、方法区、常量池等溢出,但是不管你是哪里发生的内存溢出,造成的原因都是因为申请的内存大于可用的内存造成的

  1. 堆溢出:只要不断的创建对象,并保证对象到GC Roots可达,就会产生堆溢出

  2. 栈溢出:对方法进行递归调用最容易产生栈内存溢出

    • 如果线程请求的栈深度大于虚拟机所允许的栈深度,那么会抛出StackOverflowError
    • 如果虚拟机动态拓展栈时,无法申请到足够的内存,那么会抛出OOM

    不管是SOF还是OOM,都是因为方法递归调用次数太多而导致

  3. 运行时常量池溢出

  4. 方法区溢出

16. 内存泄漏

本该释放的内存却没有得到释放,这就叫内存泄漏。造成的原因就是‘’某些无用的对象被有用的对象引用‘’,首先对象是可达的,其次对象是无用的,这类对象就会造成内存泄漏。

17.四种引用类型
  1. 强引用:

    Object obj = new Object();
    //强引用,当内存不足时虚拟机哪怕抛出OOM也不活回收强引用的对象
    
  2. 软引用

    SoftReference<String> softName = new  SoftReference<>("张三");
    //软引用,当内存不足时,软引用会被垃圾收集器回收
    
  3. 弱引用

    WeakReference<String> weakName = new WeakReference<String>("hello");
    //弱引用,无论内存是否足够,一旦发现弱引用对象存在,GC的时候它就会被回收
    
  4. 虚引用

    ReferenceQueue<String> queue = new ReferenceQueue<String>();
    PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
    //虚引用,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。
    
18.对象的定位方式有哪些

就软件开发整体而言,对于对象的访问有两种形式

  1. 使用句柄访问

    栈上的引用保存的是句柄池中的句柄的地址,而句柄又指向对象的地址信息和对象类型数据

  2. 直接指针访问

    栈上的引用保存的是对象的地址信息

19. 什么是STW,为什么要STW

STW 全程是Stop The World,垃圾收集器工作的时候会暂停其他的用户线程,因为如果不这样做的话,那么垃圾收集器在标记的时候,对象的状态还在发生不断的改变,那么标记可达对象的意义何在?

20. 什么是类加载机制

把class文件中的数据通过类加载器加载到内存中,校验、解析、初始化等过程,最终形成jvm可操作的数据类型

21. 类加载过程

加载->连接->初始化,连接又可以分为 验证->准备->解析

22.说说你了解的类加载器

image-20200331142432534

从虚拟机的角度来讲,只存在两种类加载器

  1. 启动类加载器:这个类加载器由C++实现,是属于虚拟机本身的一部分
  2. 其他类加载器:由Java语言实现,不属于虚拟机的一部分,并且全部继承了java.lang.ClassLoader

从开发者角度的话还可以细分为四种

  1. 启动类加载器:负责加载JRE的核心类库,如jre/lib下的rt.jar,charsets.jar等
  2. 拓展类加载器:负责加载JRE的扩展目录jre/lib/ext下的jar
  3. 应用程序类加载器:负责加载ClassPath上指定的类包,如果说我们没有自定义自己的类加载器,一般情况下使用的就是这个类加载器
  4. 自定义类加载器:一般情况下我们自己做常规项目的话不会使用到自定义类加载器,但是你像Tomcat就实现了自定义类加载器
23.什么是双亲委派模型?

类加载器收到类加载请求的时候,他不会自己直接去加载这个类,而是委托给他的父类加载器去加载这个类,因此所有的类加载请求最终都会请求到启动类加载器中,如果父类加载器无法完成这个类加载请求的话,那么子类加载器才会尝试自己去加载

24.为什么使用双亲委派模型?

双亲委派模型的好处:

  • 沙箱安全机制:避免核心类被篡改,自定义的java.lang.String类不会被加载,比如说你自己写的java.lang.String委托到启动类加载器时,启动类加载器会去加载jre/lib/rt.jar里面的String而不是你自定义的String
  • 避免类的重复加载:父类加载器加载过的类,子类加载器就没有必要再去加载了
25.打破双亲委派模型

这个问题可以从两方面去回答,第一为什么要打破双亲委派模型,第二如何打破双亲委派模型

  1. 为什么打破双亲委派模型:Java中SPI机制的实现,基本上都需要打破双亲委派模型,首先我说一下SPI机制,spi相关代码都会由启动类加载器去加载,但是他具体的实现是由各个厂商来完成的,启动类加载器肯定加载不了具体的实现,这个时候就需要打破双亲委派机制
  2. 如何打破双亲委派机制:为了解决上述问题,jdk提供了一个线程上下文类加载器,可以通过线程的setContextClassLoader去设置,如果没设置的话,他会从父线程去继承,如果整个应用程序都没有设置过,那么它默认使用应用程序类加载器,这样在一些厂商实现spi机制相关代码的时候,就可以通过线程上下文类加载器去加载对于的类,然后启动类加载起就拿到了应用类加载器加载的类,实现了打破双亲委派机制
26.什么是SPI机制

SPI全称Service Provider Interface,是Java提供给第三方实现或者扩展的API,比如jdbc、jndi

27. 等待补充…

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值