JVM面试题学习笔记1:
Posted Vincent9847
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM面试题学习笔记1:相关的知识,希望对你有一定的参考价值。
1.谈谈你对JVM的理解?
(我们写的java文件到通过编译器编译成java字节码文件(.class文件),这个过程是java编译过程;而我们的java虚拟机执行的就是字节码文件。(即一堆16进制的字节))
答:
- Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。
- Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
2.Java8虚拟机和之前的变化更新?
答:撤销了永久带,引入了元空间:
在HotSpot虚拟机中,jdk1.6时,设计团队把方法区设计为永久带,这样GC工作区域就可以扩展至方法区。这种策略可以可以避免为方法区单独设计垃圾回收机制,但是坏处就是,方法区的回收条件十分苛刻,而且回收效果也不好。
在jdk1.7版本,设计团队也意识到这个问题,但是只将方法区中的字符串常量池移除永久带。
到了最新的jdk1.8版本,就不再有永久带这个概念,并且用元空间来代替原来的永久代
元空间内的规则:元空间中类及其相关的元数据和类加载器生命周期一致,每个类加载器有专门的存储空间,不会单独回收某个类,位置也是固定的,但是当类加载器不再存活时会把它相关的空间全部移除。
3.请简单描述一下JVM分区有哪些?
答:Java内存通常被划分为5个区域:程序计数器(Program Count Register)、本地方法栈(Native Stack)、方法区(Method Area)、栈(Stack)、堆(Heap)。
4.请简单描述一下JVM加载class文件的原理是什么?
答:
- JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。
- Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
- 类的装载方式有两种:
- (1)隐式装载,程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
- (2)显式装载,通过class.forname()等方法,显式加载需要的类 ,隐式加载与显式加载的区别:两者本质是一样的。
- Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
5.请列举一下,在JAVA虚拟机中,哪些对象可作为ROOT对象?
(常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。)
答:
- 虚拟机栈中的引用对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用对象;
- 本地方法栈中JNI引用对象。
6.请简单描述一下类的加载过程?
答:
- JVM类加载机制分为五个部分:加载、连接(验证、准备、解析)、初始化、使用、卸载。
- JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
- 当一个类加载器收到类加载任务时,会先交到其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成任务时,才会尝试执行加载任务。采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
(1.加载
我们编写一个java类的代码,经过编译之后生成一个后缀名为.class的文件,java虚拟机就能识别这种文件。
java的生命周期就是class文件从加载到消亡的过程。
关于加载,其实,就是将源文件的class文件找到类的信息将其加载到方法区中,
然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。
但是这一功能是在JVM之外实现的,主要的原因是方便让应用程序自己决定如何获取这个类,在不同的虚拟机实现的方式不一定相同,
hotspot虚拟机是采用需要时在加载的方式,也有其他是先预先加载的。 (可以参考深入理解JVM这本书)
2.连接
连接一般是加载阶段和初始化阶段交叉进行,过程由以下三部分组成:
(1)验证:确定该类是否符合java语言的规范,有没有属性和行为的重复,继承是否合理,总之,就是保证jvm能够执行
(2)准备:主要做的就是为由static修饰的成员变量分配内存,并设置默认的初始值
默认初始值如下:
1.八种基本数据类型默认的初始值是0
2.引用类型默认的初始值是null
3.有static final修饰的会直接赋值,例如:static final int x=10;则默认就是10.
(3)解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用,说白了就是jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。
3.初始化
初始化这个阶段就是将静态变量(类变量)赋值的过程,即只有static修饰的才能被初始化,执行的顺序就是:
父类静态域或着静态代码块,然后是子类静态域或者子类静态代码块(静态代码块先被加载,然后再是静态属性)
4.使用
在类的使用过程中依然存在以下三步:
(1)对象实例化:就是执行类中构造函数的内容,
如果该类存在父类JVM会通过显示或者隐示的方式先执行父类的构造函数,
在堆内存中为父类的实例变量开辟空间,并赋予默认的初始值,
然后在根据构造函数的代码内容将真正的值赋予实例变量本身,
然后,引用变量获取对象的首地址,通过操作对象来调用实例变量和方法
(2)垃圾收集:当对象不再被引用的时候,就会被虚拟机标上特别的垃圾记号,在堆中等待GC回收
(3)对象的终结:对象被GC回收后,对象就不再存在,对象的生命也就走到了尽头
5.类卸载
类卸载即类的生命周期走到了最后一步,程序中不再有该类的引用,该类也就会被JVM执行垃圾回收,从此生命结束…
)
7.请说明一下Eden区和survial区的含义以及工作原理?
答:
- 目前主流的虚拟机实现都采用了分代收集的思想,把整个堆区划分为新生代和老年代;新生代又被划分成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;(GC复制算法)
- 2) 清空 Eden 和 From Survivor ;
- 3) 颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To , To 变 From 。可以看出,只有在 Eden 空间快满的时候才会触发 Minor GC 。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以降低。当然,使用两个 Survivor 这种方式我们也付出了一定的代价,如 10% 的空间浪费、复制对象的开销等。
8.GC中如何判断对象是否需要被回收?
答:
如何确定垃圾?
想要回收垃圾,必须得先知道,哪些对象可以被认定为垃圾。关于垃圾确定方式,主要有两种,分别是引用计数法与可访问性分析法,其原理分别如下:
引用计数法:在 Java 中,引用与对象相关联,如果要操作对象,则必须使用引用。因此,可以通过引用计数来确定对象是否可以回收。实现原则是,如果一个对象被引用一次,计数器 +1,反之亦然。当计数器为 0 时,该对象不被引用,则该对象被视为垃圾,并且可以被 GC 回收利用。
可达性分析:为了解决引用计数法的循环引用问题,Java 采用了可达性分析的方法。其实现原理是,将一系列"GCroot"对象作为搜索起点。如果在"GCroot"和一个对象之间没有可达的路径,则该对象被认为是不可访问的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
9.Young GC和 Full GC 分别在什么时候发生?
答:
Young GC的触发时机:Young GC其实一般就是在新生代的Eden区域满了之后就会触发,采用复制算法来回收新生代的垃圾。
Full GC的触发时机如下:
(1)发生Young GC之前进行检查,如果“老年代可用的连续内存空间” < “新生代历次Young GC后升入老年代的对象总和的平均大小”,说明本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间
此时必须先触发一次Old GC给老年代腾出更多的空间,然后再执行Young GC。
(2)执行Young GC之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必须立即触发一次Old GC。
(3)老年代内存使用率超过了92%,也要直接触发Old GC,当然这个比例是可以通过参数调整的。
概括成一句话,就是老年代空间也不够了,没法放入更多对象了,这个时候务必执行Old GC对老年代进行垃圾回收。
10.掌握不同版本JDK的垃圾回收器是如何工作的。
答:
java垃圾收集器的历史
第一阶段,Serial(串行)收集器
在jdk1.3.1之前,java虚拟机仅仅能使用Serial收集器。 Serial收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
PS:开启Serial收集器的方式
-XX:+UseSerialGC
第二阶段,Parallel(并行)收集器
Parallel收集器也称吞吐量收集器,相比Serial收集器,Parallel最主要的优势在于使用多线程去完成垃圾清理工作,这样可以充分利用多核的特性,大幅降低gc时间。
PS:开启Parallel收集器的方式
-XX:+UseParallelGC -XX:+UseParallelOldGC
第三阶段,CMS(并发)收集器
CMS收集器在Minor GC时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。在Full GC时不再暂停应用线程,而是使用若干个后台线程定期的对老年代空间进行扫描,及时回收其中不再使用的对象。
PS:开启CMS收集器的方式
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
第四阶段,G1(并发)收集器
G1收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于4GB)时产生的停顿。相对于CMS的优势而言是内存碎片的产生率大大降低。
PS:开启G1收集器的方式
-XX:+UseG1GC
11.几代垃圾回收算法及垃圾回收器原理解读及特性对比详解;
资源:https://blog.csdn.net/Vincent9847/article/details/117822832
垃圾回收算法:
在Java中主要有四中垃圾回收算法,分别是标记清除算法、复制算法、标记整理算法 和 分代收集算法。
垃圾回收器:
在jvm中,实现了多种垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器、CMS(并发)垃圾收集器、G1垃圾收集器。
GC的执行机制:
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC:
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC:
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
1.年老代(Tenured)被写满;
2.持久代(Perm)被写满 ;
3.System.gc()被显示调用 ;
4.上一次GC之后Heap的各域分配策略动态变化。
12.请简单说明一下JVM的回收算法以及它的回收器是什么?还有CMS采用哪种回收算法?使用CMS怎样解决内存碎片的问题呢?
11.请问什么是JVM内存模型?
答:
- Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
- 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。其关系模型图如下图所示:
13.JVM最大内存限制是多少?
答:
- 堆内存分配:JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小 于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、 -Xmx相等以避免在每次GC后调整堆的大小。
- 非堆内存分配:JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。
- 最大内存VM:首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽 然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系 统下为2G-3G),而64bit以上的处理器就不会有限制了。
- 下面是当前比较流行的几个不同公司不同版本JVM最大堆内存:
14.OOM分析,内存故障、CPU100%等问题分析和解决
资源:
OOM(Out Of Memory)分析:(无非“本身资源不够”“申请资源太多”“资源耗尽”几个原因。)
确认是不是内存本身就分配过小;
找到最耗内存的对象;
确认是否资源耗尽。
CPU100%:
找到最耗CPU的进程;
找到最耗CPU的线程;
查看堆栈,定位线程在干嘛,定位对应代码。
15.JVM是如何实现线程的?
答:
- 线程是比进程更轻量级的调度执行单位。线程可以把一个进程的资源分配和执行调度分开。一个进程里可以启动多条线程,各个线程可共享该进程的资源(内存地址,文件IO等),又可以独立调度。线程是CPU调度的基本单位。
- 主流OS都提供线程实现。Java语言提供对线程操作的同一API,每个已经执行start(),且还未结束的java.lang.Thread类的实例,代表了一个线程。
- Thread类的关键方法,都声明为Native。这意味着这个方法无法或没有使用平台无关的手段来实现,也可能是为了执行效率。
实现线程的方式:
- 内核来完成线程切换:使用内核线程实现内核线程(Kernel-Level Thread, KLT)就是直接由操作系统内核支持的线程。内核通过调度器Scheduler调度线程,并将线程的任务映射到各个CPU上,程序使用内核线程的高级接口,轻量级进程(Light Weight Process,LWP),用户态和内核态切换消耗内核资源。
- 使用用户线程实现:系统内核不能感知线程存在的实现 ,用户线程的建立、同步、销毁和调度完全在用户态中完成,所有线程操作需要用户程序自己处理,复杂度高。
- 用户线程加轻量级进程混合实现:轻量级进程作为用户线程和内核线程之间的桥梁。
16.Tomcat 性能调优及性能工具使用
资源:https://blog.csdn.net/wj1314250/article/details/112596084
JDK内存优化;
tomcat线程优化;
使用Visualvm性能监控。
17.Java JDK内置工具的使用说明
资源:https://blog.csdn.net/spark_guo/article/details/102497521
以上是关于JVM面试题学习笔记1:的主要内容,如果未能解决你的问题,请参考以下文章