JVM基础面试题

JDK、JRE、JVM的关系

JVM
Java虚拟机,它只识别.class类型文件,它能将class文件中的字节码指令进行识别并调用操作系统向上的API完成动作。
JRE
Java运行时环境。它主要包含两部分:Jvm的标准实现和Java的一些基本类库。相对于JVM来说,JRE多出来一部分Java类库。
JDK
Java开发工具包。包括整个Java开发的核心,它集成了JRE和一些好用的小工具,如:javac.exe、java.exe、jre.exe等。
三者的关系:一层层的嵌套关系,JDK>JRE>JVM。

JVM内存模型以及分区情况和作用

如图所示:

JVM基础面试题_第1张图片

黄色部分线程线程共享,蓝色部分线程私有。

方法区

存储类加载器的类信息,常量,静态变量等数据。

存放实例对象,所有的对象(不包括直接内存对象、符合逃逸分析的对象和)和数组都在堆内存分配,JVM所管理的内存中最大的一块区域。

Java方法执行的内存模型:存储局部变量表,操作数栈,动态链接,方法出口等信息。生命周期与线程相同

本地方法栈

作用与虚拟机栈类似。不同点本地方法栈为native方法执行服务,虚拟机栈为虚拟机执行的java方法服务。

程序计数器

当前线程所执行的行号指示器,是JVM内存区域最小的一块区域。执行字节码工作时就是利用程序计数器来选取下一条需要执行的字节码指令。

JVM对象创建流程

整体流程图如下

JVM基础面试题_第2张图片

  1. 虚拟机遇到new指令,首先去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并检查这个符号引用的类是否已被加载,解析,初始化。
  2. 如果类已经加载直接分配内存,如果未加载,则先进行类的加载。
  3. 类加载检查通过之后,对新对象进行内存分配。
  4. 对象生成需要的内存大小在加载完成后便可完全确定,为对象分配空间等同于从Java堆内存中划分出一块确定大小的内存
  5. 内存大小划分分为两种情况:JVM内存是规整的(使用和未使用的内存分别放一边,中间放一个指针作为分界点指示器,这样分配就很简单,只需要将指针向空闲空间那边挪动与对象大小相同的距离,这就是“指针碰撞”)。JVM内存不是规整的(使用内存和未使用内存相互交错,这时候我们需要维护一张表,用于记录哪些内存可用,在分配时从列表中找到一个足够大的空间划分给对象,并更新到这张表中)
  6. JVM将内存空间初始化为0值,如果使用TLAB,就可以在TLAB分配的时候进行工作。
  7. JVM对对象进行必要设置。
  8. 执行完以上步骤之后从JVM来看一个对象基本上就完成了,但从Java程序代码绝对来看,对象创建才刚刚开始,需要执行init方法,按照程序中设定的初始化操作初始化,这时候一个真正的程序对象才生成了。

垃圾回收算法有哪些,他们的优缺点又是什么?

常见的垃圾回收算法有:

标记清除算法,复制算法,标记整理算法和分代收集算法

标记清除算法

包含两个阶段,标记和清除

标记阶段:确认所有需要回收的对象并做好标记

清除阶段:将标记节点标记不可用的对象清除

缺点:

标记和清除的效率都不高,会产生大量的碎片,导致频繁的回收。

复制算法

内存分为相等大小的两块,每次使用其中一块。当垃圾回收的时候把存活对象复制到另一块上,然后把这块内存整个清理掉

缺点:

浪费额外的内存作为复制区,当对象存活率较高时,复制算法效率会下降。

标记整理算法

不是把存活的对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外内存。

缺点:

算法复杂度大,执行步骤较多。

分代收集算法

目前大多数JVM垃圾收集器采用的算法,根据对象存活的生命周期将内存划分为若干不同的区域,一般情况将堆区划分为新生代、老年代和永久代。

老年代的特点是每次垃圾收集时只有少量对象被回收,而新生代的特点是每次垃圾收集会有大量对象被回收,那么根据不同代的特点采取最合适的收集算法。

如图:

JVM基础面试题_第3张图片Young区:存放新创建的对象,对象生命周期非常短,几乎用完可以立即回收,也叫eden区

Tenured:Young区经过多次回收后存活下来的对象将被移到改区,也称old区

Permanent:永久代,主要存放加载类的信息,生命周期长,几乎不会被回收。

缺点:

算法复杂,步骤较多。

简单介绍一下什么是类加载机制

Class文件由类加载器装载后,在JVM中形成一份描述Class结构的元信息对象,通过该元信息对象可以获取Class结构信息,如构造函数,方法,属性等。

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java对象。

类加载过程是什么,简单描述下每个步骤?

包括:加载,验证,准备,解析,初始化

加载

查找并加载类的二进制数据

加载是类加载的第一个阶段,需要完成三件事情:

通过类的全限定名来获取其定义的二进制字节流

将字节流所代表的静态存储结构转换为方法区的运行时数据结构

在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区中这些数据的访问入口。

验证

确保被加载的类的正确性

确保class文件的字节流中包含的信息符合当前虚拟机的规范,并且不会损害虚拟机自身的安全,包含四个验证动作:文件格式验证,元数据验证,字节码验证,符号引用验证。

准备

为静态变量分配内存,并将其初始化为默认值

正式为类的静态变量分配内存并设置类变量初始值阶段,这些内存都将在方法区中分配。

解析

符号引用转为直接引用

虚拟机将常量池中的符号引用替换为直接引用的过程,解析动作主要针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化

类变量进行初始化

为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始。

jvm类加载器有几种?分别作用是什么?

启动类加载器,扩展类加载器,程序类加载器

启动类加载器

引导类装入器是本地代码实现的类装入器,他负责将java_runtime_home/lib下面的类库加载到内存中。由于引导类加载器设计到虚拟机本地实现细节,开发者无法知己获取到启动类加载器的引用。

标准扩展类加载器

负责将java_runtime_home/lib/ext或者由系统变量java.ext.dir指定位置中的类库加载到内存中,程序员可以直接使用标准扩展类加载器

程序类加载器

负责加载用户路径classpath上的类库

双亲委派模式,有什么作用?

当一个类加载器需要加载一个类时,并不会立即自己去记载,而是首先委派给父类加载器去加载,父类加载器加载不了再给父类的父类去加载,一层一层往上委托,直到顶层加载器(启动类加载器),如果父类加载器反馈无法加载那么类加器才会自己去加载。(如下图所示)

JVM基础面试题_第4张图片

作用:

1) 防止重复加载类。在JVM中,要唯一确定一个对象,是由类加载器和全类名两者共同确定的,考虑到各层级的类加载器之间仍然由重叠的类资源加载区域,通过向上抛的方式可以避免一个类被多个不同的类加载器加载,从而形成重复加载。

2) 安全。例如读者朋友定义了一个名为java.lang.Integer的类,而该类在核心库中也存在,借用双亲委派的机制,我们就能有效防止该自定义的同名类被加载,从而保护了平台的安全性。

怎么打破双亲委派模型?

打破双亲委派机制则不仅 要继承 ClassLoader 类,还要 重写 loadClass fifindClass 方法。

JVM垃圾收集器有哪些?

主要分为以下七种,如图:

JVM基础面试题_第5张图片

每种垃圾收集器之间有连线,表示他们可以搭配使用。

Serial收集器

Serial 是一款用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial 进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World)。

就比如妈妈在家打扫卫生的时候,肯定不会边打扫边让儿子往地上乱扔纸屑,否则一边制造垃圾,一遍清理垃圾,这活啥时候也干不完。

如下是 Serial 收集器和 Serial Old 收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,所有线程暂停执行,Serial 收集器以单线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行。

JVM基础面试题_第6张图片

ParNew 收集器

ParNew 就是一个 Serial 的多线程版本,其它与Serial并无区别。ParNew 在单核 CPU 环境并不会比 Serial 收集器达到更好的效果,它默认开启的收集线程数和 CPU 数量一致,可以通过 -XX:ParallelGCThreads 来设置垃圾收集的线程数。

如下是 ParNew 收集器和 Serial Old 收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,所有线程暂停执行,ParNew 收集器以多线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行。

JVM基础面试题_第7张图片

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃
圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码
的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),
高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而
不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个
重要区别。
JVM基础面试题_第8张图片

Serial Old 收集器

Serial Old 收集器是 Serial 的老年代版本,同样是一个单线程收集器,采用标记-整理算法。 这个收集器也主要是 运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器
在 Server 模式下,主要有两个用途:
1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
2. 作为年老代中使用 CMS 收集器的后备垃圾收集方案。

Parallel Old 收集器

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,是一个多线程收集器,采用标记-整理算法。可以与 Parallel Scavenge 收集器搭配,可以充分利用多核 CPU 的计算能力。

CMS收集器

Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最 主要目标是获取最短垃圾
回收停顿时间, 和其他年老代使用标记-整理算法不同,它使用 多线程的标记-清除算法
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
微信公众号:Java架构师进阶编程 13/04/2018
Page 34 of 283
并发标记
进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记
记录,仍然需要暂停所有的工作线程。
并发清除
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并
发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作, 所以总体上来看
CMS 收集器的内存回收和用户线程是一起并发地执行。
CMS 收集器工作过程

JVM基础面试题_第9张图片

G1 收集器

G1 收集器是 jdk1.7 才正式引用的商用收集器,现在已经成为 jdk9 默认的收集器。前面几款收集器收集的范围都是新生代或者老年代,G1 进行垃圾收集的范围是整个堆内存,它采用 “ 化整为零 ” 的思路,把整个堆内存划分为多个大小相等的独立区域(Region),在 G1 收集器中还保留着新生代和老年代的概念,它们分别都是一部分 Region,如下图:

JVM基础面试题_第10张图片

每一个方块就是一个区域,每个区域可能是 Eden、Survivor、老年代,每种区域的数量也不一定。JVM 启动时会自动设置每个区域的大小(1M ~ 32M,必须是 2 的次幂),最多可以设置 2048 个区域(即支持的最大堆内存为 32M*2048 = 64G),假如设置 -Xmx8g -Xms8g,则每个区域大小为 8g/2048=4M。

为了在 GC Roots Tracing 的时候避免扫描全堆,在每个 Region 中,都有一个 Remembered Set 来实时记录该区域内的引用类型数据与其他区域数据的引用关系(在前面的几款分代收集中,新生代、老年代中也有一个 Remembered Set 来实时记录与其他区域的引用关系),在标记时直接参考这些引用关系就可以知道这些对象是否应该被清除,而不用扫描全堆的数据。

G1 收集器可以 “ 建立可预测的停顿时间模型 ”,它维护了一个列表用于记录每个 Region 回收的价值大小(回收后获得的空间大小以及回收所需时间的经验值),这样可以保证 G1 收集器在有限的时间内可以获得最大的回收效率。

如下图所示,G1 收集器收集器收集过程有初始标记、并发标记、最终标记、筛选回收,和 CMS 收集器前几步的收集过程很相似:

JVM基础面试题_第11张图片

① 初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行。

② 并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。

③ 最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录。

④ 筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是 Garbage First 的由来——第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。

适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。可以用 -XX:+UseG1GC 使用 G1 收集器,jdk9 默认使用 G1 收集器。

对象已死是什么意思

对象不可能在被任何途径使用

判断对象已死的方法有:引用计数法和可达性分析算法

什么情况会栈溢出

  1. 方法创建一个很大的对象,如:List,Array
  2. 是否产生循环调用,死循环
  3. 是否引用了较大的全局变量

Java四种引用类型,强软弱虚

强引用:new出的对象之类的,只要强引用还在永远不会被回收,因此强引用是造成 Java 内存泄漏的主要原因之 一。

软引用:当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

弱引用:它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用: 它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

Eden和Survivor的分配比例是多少,为什么?

默认比例是8:1
大部分对象朝生夕死。

cpu占用过高排查

top命令查看cpu占用最高程序

top -H -p [java进程id],找到cpu占用较高的线程id

计算java线程id的16进制值,因为后续用jstack看到的线程快照中,线程id为小写十六进制值

(1)可百度在线进制转换

(2)可使用windows自带的计算器,程序员模式,可转换十六进制

(3)Linux可使用命令:printf "%x\n" [线程_id]

使用命令 jstack [java进程pid] | grep [线程id十六进制值] -A 30(-A 30表示向下打印30行)

怎么打出线程栈信息

输入 jps ,获得进程号。
top -Hp pid 获取本进程中所有线程的 CPU 耗时性能
jstack pid 命令查看当前 java 进程的堆栈状态
或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个 txt 文件。

你可能感兴趣的:(面试,jvm)