乐趣区

Java虚拟机面试题总结

上次已经介绍了初识 Java 虚拟机)和浅谈 Java 垃圾回收),今天来总结一下面试中经常遇到的有关 Java 虚拟机的面试题。

  • 简述 java 垃圾回收机制
  • JVM 内存分哪几个区,每个区的作用是什么?
  • 如何判断一个对象是否存活?
  • Java 中垃圾收集的方法有哪些?
  • 谈谈类加载器双亲委派模型机制
  • 类加载器有哪些
  • 请解释 StackOverflowError 和 OutOfMemeryError 的区别?
  • GC 的回收流程是怎样的

简述 java 垃圾回收机制

在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是 低优先级 的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

JVM 内存分哪几个区,每个区的作用是什么?

Java 虚拟机主要分为以下几个区:

方法区

1. 有时候也成为 永久代,在该区内很少发生垃圾回收,但是并不代表不发生 GC,在这里进行的 GC 主要是对方法区里的常量池和对类型的卸载
2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
3. 该区域是被线程共享的。
4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。

虚拟机栈

1. 虚拟机栈也就是我们平常所称的 栈内存, 它为 Java 方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
2. 虚拟机栈是线程私有的,它的生命周期与线程相同。

本地方法栈

本地方法栈和虚拟机栈类似,只不过本地方法栈为 Native 方法服务。

Java 堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。

程序计数器

内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。

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

判断一个对象是否存活有两种方法:

引用计数法

所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”, 将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。

可达性算法

该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。

Java 中垃圾收集的方法有哪些?

  1. 标记 - 清除

    这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不高 ,标记和清除的效率都很低;2. 会产生大量不连续的内存碎片 ,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC 动作。

  2. 复制算法

    为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一半的内存。
    于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为 8:1:1 三部分,较大那份内存交 Eden 区,其余是两块较小的内存区叫 Survior 区。每次都会优先使用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,然后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对象通过分配担保机制复制到老年代中。(Java 堆又分为新生代和老年代)

  3. 标记 - 整理

    该算法主要是为了解决标记 - 清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现 将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。

  4. 分代收集 

    现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用 复制 算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用 标记 - 整理  或者  标记 - 清除

谈谈类加载器双亲委派模型机制

当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

类加载器有哪些

  • Bootstrap ClassLoader
  • Extensions ClassLoader
  • App ClassLoader
  • 用户自定义类加载器

请解释 StackOverflowError 和 OutOfMemeryError 的区别?

StackOverflowError 栈溢出,一般由于递归过多,调用方法过多导致

OutOfMemeryError 堆内存溢出,即 OOM,由于堆内存中没有被 GC 回收的对象过多导致。

出现 OOM 的原因:

  • Java 虚拟机的堆内存设置不够,可以通过参数 -Xms 和 -Xmx 来调优
  • 程序中创建了大量对象,并且长时间不能被被垃圾回收器回收(存在引用)

GC 的回收流程是怎样的

GC 回收流程如下:

对于整个的 GC 流程里面,那么最需要处理的就是新生代和老年代的内存清理操作,而元空间(永久代)都不在 GC 范围内
当现在有一个新的对象产生,那么对象一定需要内存空间。

1、首先会 判断 Eden 区是否有内存空间,如果此时有内存空间,则直接将新对象保存在 Eden 区。

2、但是如果此时在 Eden 区内存不足,那么会自动执行一个 Minor GC 操作,将 Eden 区的无用内存空间进行清理,Minor GC 的清理范围只在 Eden 区,清理之后会继续判断 Eden 区的内存空间是否充足?如果内存空间充足,则将新对象直接在 Eden 区进行空间分配。

3、如果执行Minor GC 之后发现 Eden 区的内存空间依然不足,那么这个时候会执行 Survivor 区的判断,如果 Survivor 区有剩余空间,则将 Eden 区部分活跃对象保存在 Survivor 区,那么随后继续判断 Eden 区的内存空间是否充足,如果充足怎则将新对象直接在 Eden 区进行空间分配。

4、此时如果 Survivor 区没有内存空间,则继续判断老年区。若老年区有剩余内存则将部分存活对象保存在老年代。

5、如果这个时候老年代也满了,那么这个时候将产生Major GC(Full GC), 那么这个时候将进行老年代的清理。

6、如果老年代执行 Full GC 之后,无法进行对象的保存,则会产生OOM 异常,OutOfMemoryError 异常

退出移动版