关于java:牛客网真实面经总结JVM篇

32次阅读

共计 9711 个字符,预计需要花费 25 分钟才能阅读完成。

最近在牛客网上看到很多程序员面试都被问到 JVM 相干的一些问题,为了让大家更好的参考,我把这些 JVM 相干面试题都整顿了一下,而后本人也做了一些解答,如果有什么不对的欢送大家在评论区通知我,大家一起交流学习!

作为浏览福利,我整顿了一些 Java 面试题(有脑图、手写 pdf、md 文档),须要的可【点击此处】获取!

请简略形容一下 JVM 加载 class 文件的原理是什么?

考察点:JVM

参考答复:

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

Java 中的所有类,都须要由类加载器装载到 JVM 中能力运行。类加载器自身也是一个类,而它的工作就是把 class 文件从硬盘读取到内存中。在写程序的时候,咱们简直不须要关怀类的加载,因为这些都是隐式装载的,除非咱们有非凡的用法,像是反射,就须要显式的加载所须要的类。

类装载形式,有两种
(1)隐式装载,程序在运行过程中当碰到通过 new 等形式生成对象时,隐式调用类装载器加载对应的类到 jvm 中,
(2)显式装载,通过 class.forname() 等办法,显式加载须要的类 , 隐式加载与显式加载的区别:两者实质是一样的。

Java 类的加载是动静的,它并不会一次性将所有类全副加载后再运行,而是保障程序运行的根底类 (像是基类) 齐全加载到 jvm 中,至于其余类,则在须要的时候才加载。这当然就是为了节俭内存开销。

● 什么是 Java 虚拟机?为什么 Java 被称作是“平台无关的编程语言”?

考察点:JVM

参考答复:

Java 虚拟机是一个能够执行 Java 字节码的虚拟机过程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。
Java 被设计成容许应用程序能够运行在任意的平台,而不须要程序员为每一个平台独自重写或者是从新编译。Java 虚拟机让这个变为可能,因为它晓得底层硬件平台的指令长度和其余个性。

● jvm 最大内存限度多少?

考察点:JVM

参考答复:

(1)堆内存调配

JVM 初始调配的内存由 -Xms 指定,默认是物理内存的 1 /64;JVM 最大调配的内存由 -Xmx 指定,默认是物理内存的 1 /4。默认空余堆内存小 于 40% 时,JVM 就会增大堆直到 -Xmx 的最大限度;空余堆内存大于 70% 时,JVM 会缩小堆直到 -Xms 的最小限度。因而服务器个别设置 -Xms、-Xmx 相等以防止在每次 GC 后调整堆的大小。

(2)非堆内存调配

JVM 应用 -XX:PermSize 设置非堆内存初始值,默认是物理内存的 1 /64;由 XX:MaxPermSize 设置最大非堆内存的大小,默认是物理内存的 1 /4。

(3)VM 最大内存

首先 JVM 内存限度于理论的最大物理内存,假如物理内存无限大的话,JVM 内存的最大值跟操作系统有很大的关系。简略的说就 32 位处理器虽 然可控内存空间有 4GB, 然而具体的操作系统会给一个限度,这个限度个别是 2GB-3GB(一般来说 Windows 零碎下为 1.5G-2G,Linux 系 统下为 2G-3G),而 64bit 以上的处理器就不会有限度了。

(3)上面是以后比拟风行的几个不同公司不同版本 JVM 最大堆内存:

● jvm 是如何实现线程的?

考察点:JVM

参考答复:

线程是比过程更轻量级的调度执行单位。线程能够把一个过程的资源分配和执行调度离开。一个过程里能够启动多条线程,各个线程可共享该过程的资源(内存地址,文件 IO 等),又能够独立调度。线程是 CPU 调度的根本单位。

支流 OS 都提供线程实现。Java 语言提供对线程操作的同一 API,每个曾经执行 start(),且还未完结的 java.lang.Thread 类的实例,代表了一个线程。

Thread 类的要害办法,都申明为 Native。这意味着这个办法无奈或没有应用平台无关的伎俩来实现,也可能是为了执行效率。

实现线程的形式

A. 应用内核线程实现内核线程 (Kernel-Level Thread, KLT) 就是间接由操作系统内核反对的线程。

内核来实现线程切换

内核通过调度器 Scheduler 调度线程,并将线程的工作映射到各个 CPU 上程序应用内核线程的高级接口,轻量级过程 (Light Weight Process,LWP) 用户态和内核态切换耗费内核资源

应用用户线程实现

零碎内核不能感知线程存在的实现

用户线程的建设、同步、销毁和调度齐全在用户态中实现,所有线程操作须要用户程序本人解决,复杂度高

用户线程加轻量级过程混合实现

轻量级过程作为用户线程和内核线程之间的桥梁

● 请问什么是 JVM 内存模型?

考察点:JVM 内存模型

参考答复:

Java 内存模型(简称 JMM),JMM 决定一个线程对共享变量的写入何时对另一个线程可见。从形象的角度来看,JMM 定义了线程和主内存之间的形象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个公有的本地内存(local memory),本地内存中存储了该线程以读 / 写共享变量的正本。

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

● 请列举一下,在 JAVA 虚拟机中,哪些对象可作为 ROOT 对象?

考察点:JAVA 虚拟机

参考答复:

  • 虚拟机栈中的援用对象
  • 办法区中类动态属性援用的对象
  • 办法区中常量援用对象
  • 本地办法栈中 JNI 援用对象

● GC 中如何判断对象是否须要被回收?

考察点:JAVA 虚拟机

参考答复:

即便在可达性剖析算法中不可达的对象, 也并非是“非回收不可”的, 这时候它们临时处于“期待”阶段, 要真正宣告一个对象回收, 至多要经验两次标记过程: 如果对象在进行可达性剖析后发现没有与 GC Roots 相连接的援用链, 那它将会被第一次标记并且进行一次筛选, 筛选的条件是此对象是否有必要执行 finalize()办法。当对象没有笼罩 finalize()办法, 或者 finalize()办法曾经被虚拟机调用过, 虚拟机将这两种状况都视为“没有必要执行”。(即意味着间接回收)

如果这个对象被断定为有必要执行 finalize()办法, 那么这个对象将会搁置在一个叫做 F -Queue 的队列之中, 并在稍后由一个由虚拟机主动建设的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚构机会触发这个办法, 但并不承诺会期待它运行完结, 这样做的起因是, 如果一个对象在 finalize()办法中执行迟缓, 或者产生了死循环(更极其的状况), 将很可能会导致 F -Queue 队列中其余对象永恒处于期待, 甚至导致整个内存回收零碎解体。

finalize()办法是对象逃脱回收的最初一次机会, 稍后 GC 将对 F -Queue 中的对象进行第二次小规模的标记, 如果对象要在 finalize()中跳出回收——只有从新与援用链上的任何一个对象建设关联即可, 譬如把本人 (this 关键字) 赋值给某个类变量或者对象的成员变量, 那在第二次标记时它将被移除出“行将回收”的汇合; 如果对象这时候还没有逃脱, 那基本上它就真的被回收了。

● 请阐明一下 JAVA 虚拟机的作用是什么?

考察点:java 虚拟机

参考答复:

解释运行字节码程序打消平台相关性。

jvm 将 java 字节码解释为具体平台的具体指令。个别的高级语言如要在不同的平台上运行,至多须要编译成不同的指标代码。而引入 JVM 后,Java 语言在不同平台上运行时不须要从新编译。Java 语言应用模式 Java 虚拟机屏蔽了与具体平台相干的信息,使得 Java 语言编译程序只需生成在 Java 虚拟机上运行的指标代码(字节码),就能够在多种平台上不加批改地运行。Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

假如一个场景,要求 stop the world 工夫十分短,你会怎么设计垃圾回收机制?

绝大多数新创建的对象调配在 Eden 区。在 Eden 区产生一次 GC 后,存活的对象移到其中一个 Survivor 区。在 Eden 区产生一次 GC 后,对象是寄存到 Survivor 区,这个 Survivor 区曾经存在其余存活的对象。

一旦一个 Survivor 区已满,存活的对象挪动到另外一个 Survivor 区。而后之前那个空间已满 Survivor 区将置为空,没有任何数据。通过反复屡次这样的步骤后仍旧存活的对象将被移到老年代。

● 请阐明一下 eden 区和 survial 区的含意以及工作原理?

考察点:JVM

参考答复:

目前支流的虚拟机实现都采纳了分代收集的思维,把整个堆区划分为新生代和老年代;新生代又被划分成 Eden 空间、From Survivor 和 To Survivor 三块区域。

咱们把 Eden : From Survivor : To Survivor 空间大小设成 8 : 1 : 1,对象总是在 Eden 区出世,From Survivor 保留以后的幸存对象,To Survivor 为空。一次 gc 产生后:

1)Eden 区活着的对象 + From Survivor 存储的对象被复制到 To Survivor

2) 清空 Eden 和 From Survivor;3) 颠倒 From Survivor 和 To Survivor 的逻辑关系:From 变 To,To 变 From。能够看出,只有在 Eden 空间快满的时候才会触发 Minor GC。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以升高。当然,应用两个 Survivor 这种形式咱们也付出了肯定的代价,如 10% 的空间节约、复制对象的开销等。

● 请简略形容一下 JVM 分区都有哪些?

考察点:JVM

参考答复:

java 内存通常被划分为 5 个区域:程序计数器(Program Count Register)、本地办法栈(Native Stack)、办法区(Methon Area)、栈(Stack)、堆(Heap)。

● 请简略形容一下类的加载过程

考察点:JVM

参考答复:

如下图所示,JVM 类加载机制分为五个局部:加载,验证,筹备,解析,初始化,上面咱们就别离来看一下这五个过程。

加载

加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为办法区这个类的各种数据的入口。留神这里不肯定非得要从一个 Class 文件获取,这里既能够从 ZIP 包中读取(比方从 jar 包和 war 包中读取),也能够在运行时计算生成(动静代理),也能够由其它文件生成(比方将 JSP 文件转换成对应的 Class 类)。

验证

这一阶段的次要目标是为了确保 Class 文件的字节流中蕴含的信息是否合乎以后虚拟机的要求,并且不会危害虚拟机本身的平安。

筹备

筹备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在办法区中调配这些变量所应用的内存空间。留神这里所说的初始值概念,比方一个类变量定义为:

public static int v = 8080;

实际上变量 v 在筹备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 putstatic 指令是程序被编译后,寄存于类结构器 <client> 办法之中,这里咱们前面会解释。

然而留神如果申明为:

public static final int v = 8080;

在编译阶段会为 v 生成 ConstantValue 属性,在筹备阶段虚构机会依据 ConstantValue 属性将 v 赋值为 8080。

解析

解析阶段是指虚拟机将常量池中的符号援用替换为间接援用的过程。符号援用就是 class 文件中的:

  • CONSTANT_Class_info
  • CONSTANT_Field_info
  • CONSTANT_Method_info

等类型的常量。

上面咱们解释一下符号援用和间接援用的概念:

符号援用与虚拟机实现的布局无关,援用的指标并不一定要曾经加载到内存中。各种虚拟机实现的内存布局能够各不相同,然而它们能承受的符号援用必须是统一的,因为符号援用的字面量模式明确定义在 Java 虚拟机标准的 Class 文件格式中。

间接援用能够是指向指标的指针,绝对偏移量或是一个能间接定位到指标的句柄。如果有了间接援用,那援用的指标必然曾经在内存中存在。

初始化

初始化阶段是类加载最初一个阶段,后面的类加载阶段之后,除了在加载阶段能够自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。

初始化阶段是执行类结构器 <client> 办法的过程。<client> 办法是由编译器主动收集类中的类变量的赋值操作和动态语句块中的语句合并而成的。虚构机会保障 <client> 办法执行之前,父类的 <client> 办法曾经执行结束。p.s: 如果一个类中没有对动态变量赋值也没有动态语句块,那么编译器能够不为这个类生成 <client>()办法。

留神以下几种状况不会执行类初始化:

  • 通过子类援用父类的动态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,实质上并没有间接援用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取 Class 对象,不会触发类的初始化。
  • 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是通知虚拟机,是否要对类进行初始化。
  • 通过 ClassLoader 默认的 loadClass 办法,也不会触发初始化动作。

类加载器

虚拟机设计团队把加载动作放到 JVM 内部实现,以便让应用程序决定如何获取所需的类,JVM 提供了 3 品种加载器:

启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过 -Xbootclasspath 参数指定门路中的,且被虚拟机认可(按文件名辨认,如 rt.jar)的类。

扩大类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 零碎变量指定门路中的类库。

应用程序类加载器(Application ClassLoader):负责加载用户门路(classpath)上的类库。

JVM 通过双亲委派模型进行类的加载,当然咱们也能够通过继承 java.lang.ClassLoader 实现自定义的类加载器。

当一个类加载器收到类加载工作,会先交给其父类加载器去实现,因而最终加载工作都会传递到顶层的启动类加载器,只有当父类加载器无奈实现加载工作时,才会尝试执行加载工作。采纳双亲委派的一个益处是比方加载位于 rt.jar 包中的类 java.lang.Object,不论是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保障了应用不同的类加载器最终失去的都是同样一个 Object 对象。

● 请简略阐明一下 JVM 的回收算法以及它的回收器是什么?还有 CMS 采纳哪种回收算法?应用 CMS 怎么解决内存碎片的问题呢?

考察点:JVM

参考答复:

垃圾回收算法

标记革除

标记 - 革除算法将垃圾回收分为两个阶段:标记阶段和革除阶段。在标记阶段首先通过根节点,标记所有从根节点开始的对象,未被标记的对象就是未被援用的垃圾对象。而后,在革除阶段,革除所有未被标记的对象。标记革除算法带来的一个问题是会存在大量的空间碎片,因为回收后的空间是不间断的,这样给大对象分配内存的时候可能会提前触发 full gc。

复制算法

将现有的内存空间分为两快,每次只应用其中一块,在垃圾回收时将正在应用的内存中的存活对象复制到未被应用的内存块中,之后,革除正在应用的内存块中的所有对象,替换两个内存的角色,实现垃圾回收。

当初的商业虚拟机都采纳这种收集算法来回收新生代,IBM 钻研表明新生代中的对象 98% 是朝夕生死的,所以并不需要依照 1:1 的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次应用 Eden 和其中的一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地拷贝到另外一个 Survivor 空间上,最初清理掉 Eden 和方才用过的 Survivor 的空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1(能够通过 -SurvivorRattio 来配置),也就是每次新生代中可用内存空间为整个新生代容量的 90%,只有 10% 的内存会被“节约”。当然,98% 的对象可回收只是个别场景下的数据,咱们没有方法保障回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,须要依赖其余内存(这里指老年代)进行调配担保。

标记整顿

复制算法的高效性是建设在存活对象少、垃圾对象多的前提下的。这种状况在新生代常常产生,然而在老年代更常见的状况是大部分对象都是存活对象。如果仍然应用复制算法,因为存活的对象较多,复制的老本也将很高。

标记 - 压缩算法是一种老年代的回收算法,它在标记 - 革除算法的根底上做了一些优化。首先也须要从根节点开始对所有可达对象做一次标记,但之后,它并不简略地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种办法既防止了碎片的产生,又不须要两块雷同的内存空间,因而,其性价比比拟高。

增量算法

增量算法的根本思维是,如果一次性将所有的垃圾进行解决,须要造成零碎长时间的进展,那么就能够让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。顺次重复,直到垃圾收集实现。应用这种形式,因为在垃圾回收过程中,间断性地还执行了利用程序代码,所以能缩小零碎的进展工夫。然而,因为线程切换和上下文转换的耗费,会使得垃圾回收的总体成本上升,造成零碎吞吐量的降落。

垃圾回收器

Serial 收集器

Serial 收集器是最古老的收集器,它的毛病是当 Serial 收集器想进行垃圾回收的时候,必须暂停用户的所有过程,即 stop the world。到当初为止,它仍然是虚拟机运行在 client 模式下的默认新生代收集器,与其余收集器相比,对于限定在单个 CPU 的运行环境来说,Serial 收集器因为没有线程交互的开销,分心做垃圾回收天然能够取得最高的单线程收集效率。

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,应用”标记-整顿“算法。这个收集器的次要意义也是被 Client 模式下的虚拟机应用。在 Server 模式下,它次要还有两大用处:一个是在 JDK1.5 及以前的版本中与 Parallel Scanvenge 收集器搭配应用,另外一个就是作为 CMS 收集器的后备预案,在并发收集产生 Concurrent Mode Failure 的时候应用。

通过指定 -UseSerialGC 参数,应用 Serial + Serial Old 的串行收集器组合进行内存回收。

ParNew 收集器

ParNew 收集器是 Serial 收集器新生代的多线程实现,留神在进行垃圾回收的时候仍然会 stop the world,只是相比拟 Serial 收集器而言它会运行多条过程进行垃圾回收。

ParNew 收集器在单 CPU 的环境中相对不会有比 Serial 收集器更好的成果,甚至因为存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百的保障能超过 Serial 收集器。当然,随着能够应用的 CPU 的数量减少,它对于 GC 时系统资源的利用还是很有益处的。它默认开启的收集线程数与 CPU 的数量雷同,在 CPU 十分多(譬如 32 个,当初 CPU 动辄 4 核加超线程,服务器超过 32 个逻辑 CPU 的状况越来越多了)的环境下,能够应用 -XX:ParallelGCThreads 参数来限度垃圾收集的线程数。

-UseParNewGC: 关上此开关后,应用 ParNew + Serial Old 的收集器组合进行内存回收,这样新生代应用并行收集器,老年代应用串行收集器。

Parallel Scavenge 收集器

Parallel 是采纳复制算法的多线程新生代垃圾回收器,仿佛和 ParNew 收集器有很多的类似的中央。然而 Parallel Scanvenge 收集器的一个特点是它所关注的指标是吞吐量(Throughput)。所谓吞吐量就是 CPU 用于运行用户代码的工夫与 CPU 总耗费工夫的比值,即吞吐量 = 运行用户代码工夫 / (运行用户代码工夫 + 垃圾收集工夫)。进展工夫越短就越适宜须要与用户交互的程序,良好的响应速度可能晋升用户的体验;而高吞吐量则能够最高效率地利用 CPU 工夫,尽快地实现程序的运算工作,次要适宜在后盾运算而不须要太多交互的工作。

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,采纳多线程和”标记-整顿”算法。这个收集器是在 jdk1.6 中才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器始终处于比拟难堪的状态。起因是如果新生代 Parallel Scavenge 收集器,那么老年代除了 Serial Old(PS MarkSweep)收集器外别无选择。因为单线程的老年代 Serial Old 收集器在服务端利用性能上的”连累“,即便应用了 Parallel Scavenge 收集器也未必能在整体利用上取得吞吐量最大化的成果,又因为老年代收集中无奈充分利用服务器多 CPU 的解决能力,在老年代很大而且硬件比拟高级的环境中,这种组合的吞吐量甚至还不肯定有 ParNew 加 CMS 的组合”给力“。直到 Parallel Old 收集器呈现后,”吞吐量优先“收集器终于有了比拟货真价实的利用恭喜,在重视吞吐量及 CPU 资源敏感的场合,都能够优先思考 Parallel Scavenge 加 Parallel Old 收集器。

-UseParallelGC: 虚拟机运行在 Server 模式下的默认值,关上此开关后,应用 Parallel Scavenge + Serial Old 的收集器组合进行内存回收。-UseParallelOldGC: 关上此开关后,应用 Parallel Scavenge + Parallel Old 的收集器组合进行垃圾回收

CMS 收集器

CMS(Concurrent Mark Swep)收集器是一个比拟重要的回收器,当初利用十分宽泛,咱们重点来看一下,CMS 一种获取最短回收进展工夫为指标的收集器,这使得它很适宜用于和用户交互的业务。从名字 (Mark Swep) 就能够看出,CMS 收集器是基于标记革除算法实现的。它的收集过程分为四个步骤:

  • 初始标记(initial mark)
  • 并发标记(concurrent mark)
  • 从新标记(remark)
  • 并发革除(concurrent sweep)

留神初始标记和从新标记还是会 stop the world,然而在消耗工夫更长的并发标记和并发革除两个阶段都能够和用户过程同时工作。

G1 收集器

G1 收集器是一款面向服务端利用的垃圾收集器。HotSpot 团队赋予它的使命是在将来替换掉 JDK1.5 中公布的 CMS 收集器。与其余 GC 收集器相比,G1 具备如下特点:

并行与并发:G1 能更充沛的利用 CPU,多核环境下的硬件劣势来缩短 stop the world 的进展工夫。

分代收集:和其余收集器一样,分代的概念在 G1 中仍然存在,不过 G1 不须要其余的垃圾回收器的配合就能够单独治理整个 GC 堆。

空间整合:G1 收集器有利于程序长时间运行,调配大对象时不会无奈失去间断的空间而提前触发一次 GC。

可预测的非进展:这是 G1 绝对于 CMS 的另一大劣势,升高进展工夫是 G1 和 CMS 独特的关注点,能让使用者明确指定在一个长度为 M 毫秒的工夫片段内,耗费在垃圾收集上的工夫不得超过 N 毫秒。

CMS:采纳标记革除算法

解决这个问题的方法就是能够让 CMS 在进行肯定次数的 Full GC(标记革除)的时候进行一次标记整顿算法,CMS 提供了以下参数来管制:

-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5

也就是 CMS 在进行 5 次 Full GC(标记革除)之后进行一次标记整顿算法,从而能够管制老年带的碎片在肯定的数量以内,甚至能够配置 CMS 在每次 Full GC 的时候都进行内存的整顿。

正文完
 0