校招面试之Java虚拟机面试题总结

面试总结

Java虚拟机也是面试过程中的常客,但问的问题也相对而言比较固定,主要就是分为下面几个部分

问题汇总与答案整理(仅供参考)

1. Java内存区域

Java运行时内存区域的划分,每个区域的作用和功能

Java运行时内存区域主要划分为以下五个部分:

图片

线程私有:

  • 程序计数器(Program Counter Register)
  • 虚拟机栈(JVM Stack)
  • 本地方法栈(Native Method Stack)

线程共享:

  • 方法区(Method Area)
  • 堆(Heap)

各个区域具体介绍

程序计数器

程序计数器是线程私有的,每个线程启动的时候都会创建一个较小的内存区域作为线程的程序计数器,其是当前线程所执行的字节码的行号指示器,其具备如下两个功能:

  • 字节码解释器工作时会通过改变这个计数器的值来选取下一条需要执行的字节码指令, 分支、 循环、 跳转、 异常处理、 线程恢复等基础功能都需要依赖这个计数器来完成。
  • 在多线程程序运行时,线程的私有程序计数器可以保证线程在上下文切换之后能恢复到正确的执行位置。

需要注意的是如果线程正在执行的是一个 Java 方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址 ;如果正在执行的是 Native 方法, 这个计数器值则为空(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

Native方法:简单地讲,一个Native Method就是一个java调用非java代码的接口。该方法的实现由非java语言实现,比如C,所以也没有字节码文件一说。

虚拟机栈

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

局部变量表:存放了编译期可知的各种基本数据类型(变量名和值),对象引用( 它不等同于对象本身) 和retumAddress 类型(指向了一条字节码指令的地址,只存在于字节码层面,其值就是指向特定指令内存地址的指针。例如程序计数器中的值就是当前指令所在的内存地址,即 returnAddress 类型的数据)。其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间 (Slot), 其余的数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配, 当进人一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的, 在方法运行期间不会改变局部变量表的大小。

操作数栈:虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的。不过局部变量表用的是索引,操作数栈是弹栈/压栈来访问。操作数栈可理解为Java虚拟机栈中的一个用于计算的临时数据存储区。

在 Java 虚拟机规范中, 对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度, 将拋出 StackOverflowError 异常 ;如果虚拟机栈可以动态扩展( 当前大部分的 Java 虚拟机都可动态扩展, 只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存, 就会拋出 OutOfMemoryError 异常。

本地方法栈

**本地方法栈(Native Method Stack) 与虚拟机栈所发挥的作用是非常相似的, 它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法( 也就是字节码) 服务, 而本地方法栈则为虚拟机使用到的 Native 方法服务。**在虚拟机规范中对本地方法栈中方法使用的语言、 使用方式与数据结构并没有强制规定, 因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机) 直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样, 本地方法栈区域也会拋出 StackOverflowError 和 OutOfMemoryError 异常。

**堆是Java内存管理区域中最大的一块,在虚拟机启动的时候创建。**几乎所有的对象实例都在这分配内存(因为随着JIT编译器的发展,有的在栈上分配了,需要详细了解的可以搜索逃逸分析技术)。Java堆是垃圾收集器管理的主要区域,因此也叫GC堆(Garbage Collected Heap)。Java 堆可以处于物理上不连续的内存空间中, 只要逻辑上是连续的即可, 在实现时, 既可以实现成固定大小的, 也可以是可扩展的, 不过当前主流的虚拟机都是按照可扩展来实现的( 通过-Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配, 并且堆也无法再扩展时, 将会拋出 OutOfMemoryError异常。

Java为了垃圾回收的方便将堆分为新生代和老年代。

  • 新生代:新生代程序新创建的对象都在新生代分配的,新生代由 Eden Space 和两块大小相同的 Survivor Space(通常又称 S0 和 S1或 FROM 和 To )构成。
  • 老年代:老年代用户存放经过多次新生代垃圾回收仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代。主要有两种情况:一种是 大对象,可通过启动参数设置 -XX:PretenureSizeThreshold=1024(单位为字节,默认为 0)来代表超过多大时就不再在新生代分配,而是直接在老年代分配。另一种是大的数组对象,且数组中无引用外部对象。

JDK 1.8 及之后堆的内存空间分配为三分之二的老年代和三分之一的新生代(其中Eden和Survivor的大小配比为8:1:1)

方法区

**方法区是用于存放已经被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。**与堆区一样不需要连续的物理内存,但要求逻辑连续,同时也可以动态的扩展方法区大小,但是当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。

对于HotSpot虚拟机来说,JDK1.8之前,方法区是用永久代实现的,这样可以对方法区来像堆一样进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。

**注意:**在JDK1.7之前的版本,运行时常量池是方法区的一部分,class文件中有个常量池,运行时常量池就是class文件中常量池(字面量和符号引用)经过类加载后存放的内存区域。字面量指字符串,声明为final的常量值等;而符号引用是Java编译后生成的各种常量,其包括:类和接口的全限定名,成员变量的名称和描述符,方法的名称和描述符。除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。在JDK1.7及之后的版本,运行时常量池中的字符串常量池已经不在方法区,而是在Java堆中开辟了一块区域作为字符串常量池

2. 垃圾收集

2.1 Java的垃圾回收算法和垃圾回收器

四种垃圾回收算法

  • 标记-清除

    首先遍历内存区域,对需要回收的对象打上标记。接着再次遍历内存,对已经标记过的内存进行回收

    但会存在如下两个问题:

    效率问题:遍历了两次内存空间(第一次标记,第二次清除)

    空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的

  • 标记-整理

    首先对需要回收的进行标记,接着让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。

    这种回收算法的优点在于不会产生内存碎片,缺点在于需要移动对象,效率低下

  • 复制

    将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然
    后再把使用过的内存空间进行一次清理

    其缺点主要在于内存利用率低,目前很多虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间(8:1:1),每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor

  • 分代收集

    新生代每次都有大量对象死亡,有老年代作为内存担保,采取复制算法。老年代的对象存活时间长,采用标记整理,或者标记清理算法

7种垃圾回收器(Jvm垃圾回收器(终结篇))

下图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。每个垃圾回收器的特点和应用场景详情可参照博客Jvm垃圾回收器(终结篇)

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

img

2.2 CMS垃圾回收器和G1垃圾回收器的垃圾回收过程

CMS垃圾回收器

一种以获取最短回收停顿时间为目标的收集器。

特点:基于标记-清除算法实现。并发收集、低停顿。

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。

CMS收集器的运行过程分为下列4步:

初始标记:标记GC Roots能直接到的对象。速度很快但是存在Stop The World问题。

并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。

重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。存在Stop The World问题。

并发清除:对标记的对象进行清除回收,不需要停顿。

CMS收集器的内存回收过程是与用户线程一起并发执行的,在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

具有以下缺点:

  • 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
  • 无法处理浮动垃圾:可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS
  • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC

G1垃圾回收器

G1垃圾回收器的特点如下:

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。

分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。

可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒

G1垃圾回收器的运行过程同样分为以下四步:

初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)

并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)

最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)

筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)

2.3 Minor GC,Major GC,Full GC各自什么时候发生

Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快

Major GC:回收老年代,一般由Minor GC触发,当Minor GC回收的空间还不够时就会触发

Full GC:回收所有空间的垃圾,其触发条件为:

  • 调用System.gc()
  • 老年代空间不足
  • 空间分配担保失败

2.4 如何判断一个对象是否可以被回收

  1. 引用计数算法

    为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被
    回收。在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存
    在,因此 Java 虚拟机不使用引用计数算法

  2. 可达性分析算法

    以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下对象:

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

3. 类加载

3.1 类加载过程

类的加载过程主要包括:加载、验证、准备、解析、初始化这五个阶段。

加载

在加载阶段,虚拟机需要完成三件事:

  • 通过一个类的全限定名来获取定义此类的二进制流
  • 将这个字节流所代表的的静态存储结构转化为运行时数据结构
  • 在内存中生成一个代表一个这个类的java.lang.Class对象,作为方法区这个类的各种访问数据接口。

关于第一点并没有指明从哪来获取定义该类的二进制流,所以其不仅仅局限于Class文件,可以从Zip包中读取,网络中获取,运行时计算生成等。第二点在前面的文章中讲过就不再阐述了,关于第三点需要注意的是Class是个实实在在的类,和我们自己写的类一样,Class对象就是这个类的实例,它也是继承自Object的一个特殊的类,它内部可以记录类的成员、接口等信息,也就是在Java里,Class是一个用来表示类的类。这个存放在内存中的Class对象并没有明确规定是在 Java 堆中,对于 HotSpot 虚拟机而言, Class 对象比较特殊, 它虽然是对象, 但是存放在方法区里面。

验证

这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求, 并且不会危害虚拟机自身的安全。验证阶段大致上会完成下面 4 个阶段的检验动作(详情参考深入理解虚拟机一书第七章):

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段, 这些变量所使用的内存都将在方法区中进行分配。这里需要注意的是进行内存分配的仅包括类变量( 被 static 修饰的变量), 而不包括实例变量, 实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。

举例来说:

public static int val = 123;

在准备阶段,val的值为0而不是123。因为这时候尚未开始执行任何 Java方法, 而把 value 赋值为 123 的 putstatic 指令是程序被编译后, 存放于类构造器 clinit()方法之中, 所以把 value 赋值为 123 的动作将在初始化阶段才会执行

解析

解析就是虚拟机将常量池的符号引用换成直接引用的过程。

符号引用:符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能无歧义地定位到目标即可。

直接引用:直接引用可以是直接指向目标的指针、 相对偏移量或是一个能间接定位到目标的句柄。

初始化

类初始化阶段是类加载的最后一步,在准备阶段, 变量已经赋过一次系统要求的初始值, 而在初始化阶段, 则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

3.2 类加载器分类

1.启动类加载器:这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。

2.扩展类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。

3.应用程序类加载器:这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。

4.自定义加载器:用户自己定义的类加载器。

img

3.3 什么是双亲委派模型,怎么打破双亲委派模型

上图中的类加载器关系就是双亲委派模型,其工作原理的是,如果一个类加载器收到了类加载请求,不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在父类加载器,则依次向上传递,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,如果父类加载器无法完成,子加载器才会尝试自己去加载。

双亲委派模型的好处:

Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载。

例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通
过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar
中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优
先级更高,那么程序中所有的 Object 都是这个 Object

怎么打破双亲委派模型:

想打破双亲委派模型需要重写ClassLoader类的loadClass()方法

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

miraclewk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值