以下面试题为本人JVM虚拟机部分课程学习中老师针对该部分提出的问题;列出相关问题以及本人针对该问题的回答,仅供参考;如若有不同意见,请勿喷,留言私聊改进。

 

一、请你谈谈你对JVM的理解?JDK8虚拟机有什么更新?

JVM虚拟机可以说是JAVA语言【跨平台】特性的核心部分。只需要将编写的java文件转成.class文件,通过虚拟机便可以让.class文件在各种平台上运行;

JAVA语言是一种在操作系统之上的语言,其本身无法操作内存,而通过虚拟机,便可以将其与操作系统相关联;

同时JVM虚拟机还有一个很大的优点——自动垃圾回收机制;相较于一些其他的语言,可以自动的针对虚拟机现有内存大小进行垃圾回收;

即JVM虚拟机为JAVA语言提供了:可运行环境、内存分配、垃圾回收三大功能

我们都知道JVM虚拟机针对内存进行了一个划分:线程私有(虚拟机栈、本地方法栈、程序计数器),线程共享(堆、方法区)

在JDK1.7之前 ,字符串常量池存在于方法区中,而在JDK1.7之后,字符串常量池被移至堆内存中(字符串是不可变对象,任何改变都会导致在内存中重新生成一个对象,会导致方法区的内存会远远不足而导致OOM)

在JDK1.8之后,JAVA取消了方法区的永久代的实现,改为使用元空间的方式,当然这里仅仅只是说方法区改变了一种实现方式;然而,最大的变动在于,元空间使用的并非再说JAVA虚拟机的内存,而是使用本地内存,即物理存放地址变化了。

当然它还是可能发生OOM的。

 

二、什么是OOM,请你说说OOM产生的原因?如何分析?

OOM:全称Out Of Memory,即内存溢出;当程序运行过程中,可使用内存空间不足以满足需要创建对象所需的大小,同时GC之后还是无法满足,从而出现OOM异常;

OOM产生的原因就是上面所描述的内存空间不足以再支持程序运行,那么又是什么原因才会导致JAVA运行内存不足呢;

1、Out Of Memory Error:Java Heap space(堆空间溢出)

程序运行过程中创建的对象过多,并且这些对象无法被GC清除,那么这些对象便会一直存在于java堆中,直到堆空间溢出;

又或者程序一次性从数据库中或者本地读取的数据过多过大

可以通过-Xmx和-Xms调整最大堆内存、默认堆内存空间

2、Out Of Memory Error:GC Overhead Limit Exceeded(连续GC后发现效果不大)

3、Out Of Memory Error:Dircet buffer Memory(程序向本地写数据,本地内存不足。可通过-XX:MaxDirectMemorySize设置)

4、Out Of Memory Error:Metaspace(元空间溢出,加载过多的类)

5、Out Of Memory Error:Unable to create Native Thread(创建的线程过多)

6、Out Of Memory Error: Requested array size exceeds VM limit(JVM默认允许的最大数组长度是INTEGER.MAX_VALUE - 2 即 2的15次方 - 1 -2)

.....(还有一些原因,比较复杂就不管了)

我们可以通过-XX:+PrintDumpOnOutOfMemoryError 来打印OOM出现时的相关信息,通过文件先定位导致OOM出现的具体原因。从而调整代码结构,或者优化jvm参数配置

 

三、JVM的常用调优参数

一般会用到的是

查看GC相关信息  -XX:+PrintGCDetails   、 -Xloggc:log/gc.log .....

调整内存空间  -Xms(-XX:InitialHeapSize)、 -Xmx(-XX:MaxHeapSize)、 -Xmn(-XX:ThreadStackSize) 、-XX:MaxMetaspaceSize 、-XX:MetaspaceSize 、-XX:NewRatio 、-XX:SurvivorRatio......

具体用处就不多做解释了

 

四、解释堆里分区:Eden、Survivor、老年区

JVM虚拟机将堆内存分为了新生区和老年区两个区块,新生区是对象的诞生地,老年区存放的一般是存活时间较长的对象。我们的Hotspot 虚拟机又将新生区又划分为Eden区和两个Survivor区。而我们所说的对象就是在Eden区诞生的。两个Survivor区,又称from和to区,from区的对象在GC垃圾回收时判定对象是否为“可达对象”,如若是,再判断对象“存活年龄”是否到达设定,如若是,则移入老年区,否则复制一份进入to区,随后清理Eden区和s0区

 

五、GC垃圾回收算法?谈谈利弊?

GC垃圾回收算法有以下几种,

复制算法:将内存空间分成大小相同的两等份,随后将可达对象复制一份,移动到另一个区域,随后将原本的半个区域清除(效率高,内存浪费)

标记清除算法。首先是扫描整个堆,将GC Root是无法到达的对象进行标记;随后再次全局扫描将标记的对象进行清除(多次扫描,效率不高;容易产生内存碎片,导致大对象的进入会引发频繁GC)

标记清除-整理算法。在标记清除算法的基础上,对整个清除后的堆空间进行整理,将所有存活对象移动到一块(多次扫描,效率不高)

内存效率:复制算法>标记清除>标记整理

内存整齐度:标记整理=复制算法>标记清除

内存利用率:标记整理=标记清除>复制算法

分代收集算法:实际上是两种算法的汇总。新生代采用复制算法,老年代采用标记整理算法。由于新生代所诞生的对象存活的时间一般都是很短暂的,GC也比较频繁,故最后在这个区存活的对象一般情况下不会很多,所以采用复制算法的花可以极大的提高时间效率;而老年代由于对象的存活时间一般比较久,需要被回收的可能性不大,采用标记整理算法

 

六、内存快照抓取,如何分析,命令是什么

-XX:+PrintDumpOnOutOfMemoryError

-XX:HeapDumpPath=路径

IDEA使用JProfiler插件;JDK自带的JConsole

 

七、JVM垃圾回收时如何确定垃圾,GC Roots是什么?

JVM确定垃圾的方式存在两种。其中一种引用计数法,采用针对对象所拥有的引用数进行计数的方式,当引用数=0时,表示这个对象可被回收。但这种算法存在问题,如若对象之间出现循环引用,则会导致这两个对象用于存在对其的引用,但实际上这两个对象已经用于不可能再被获取到,导致内存泄漏。而这种确定垃圾的方式也已经不再使用了;另外一种方式便是可达性分析法。通过GC Roots,如若该对象可以通过GC Roots达到,则便认为是可达对象,不会被回收。

而GC Roots又是什么呢?GC Roots是一种可以用来作为判定对象是否是可达对象的对象的引用。那么哪些可以作为GC Roots呢?

包括:存活线程、方法中的参数或者局部变量、本地方法栈中的对象的引用、静态变量以及常量、由系统类加载器加载的对象class等等

换句话说,当要进行GC时,GC Roots作为一种判定标准,其自身一定是要确保是存活的。

 

八、强引用、软引用、弱引用、虚引用

强引用:JAVA中绝大部分的引用都是强引用。比如你通过new的方式创建了一个对象,那这个对象其所指向它的变量与这个对象之间就是强引用的关系;强引用对象不会被垃圾回收,无论内存是否已经不足。最终便会导致OOM

软引用:SoftReference,被作为软引用的对象,在内存充足的情况下不会被垃圾回收,只有当内存不足时,才会被GC回收(缓存)

弱引用:WeakReference,无论内存是否充足,都会被GC(缓存)

虚引用:可以参考

 

九、谈谈默认的垃圾回收器

通过-XX:+PrintCommandLineFlags查看默认的垃圾回收器;

JDK8采用的默认垃圾回收器是UseParallelGC,即Parallel Scavenge(新生代并发复制) + Parallel Old(老年代并行标记整理)的组合

而JDK9采用的是目前先进的G1垃圾收集器

 

十、G1垃圾回收器的特点

相较于G1,其他的一些垃圾收集器都有以下的一些特点:

1、新生代和老年代是物理上区分开的

2、新生代需要划分Eden和Survivor区进行复制算法

3、老年代收集需要扫描整个老年代

4、尽可能少而快的进行GC

而G1垃圾回收器相较于上述这些特点:

最大的特点在于其将堆内存划分成了大小相同的多个区域(Region),虽然还有新生代和老年代存在,不过却是逻辑上的存在。每一个区域会随着GC的进行而有着不同的作为;

G1以尽可能收集更多的垃圾作为回收目标,并不会等到内存耗尽时才会进行垃圾回收。在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;

并且它可以自定义垃圾回收的时间;

同时虽然G1都是STW,但是它也采用mixed gc的方式,每次回收可能只回收新生代,也可能会对部分老年代进行回收

 

十一、类加载整体过程了解

java文件在被编译称为.class文件之后,经由JVM加载转成可被机器识别的二进制文件执行。期间主要经过以下几个步骤:

类加载器加载:通过类的全限定名获取此类的二进制字节流;将文件中的静态存储结构转为运行时数据(方法区的运行时常量池--字面量、符号引用<类或接口的描述符、字段的类型和描述符、方法的参数和描述符);.class文件经由类加载器加载到内存中生成一个class对象;

链接:分为三步

校验:校验文件是否是合法的可被执行的class文件(魔数-cafe babe)、校验类是否存在父类,是否继承或实现类父类方法等等

准备:为静态变量分配内存,并赋予默认值(final类型的直接赋默认值)

解析:符号引用转为直接引用

类初始化:clinit(为静态变量赋初值)

 

十二、类加载的几种方式:

1、new一个对象、put static、get static、调用类的静态方法

2、对类进行反射调用

3、初始化一个类,其父类尚未初始化时,会首先加载其父类

4、main方法所在的类会进行初始化

 

十三、new 一个对象时发生了什么?

1、查找对应类对象,如若不存在,则通过类加载器去查找对应的class文件;如若不存在class文件,则跑出classnotfound异常;反之则将类加载进内存

2、为对象在堆中分配一块内存空间,并分配一个引用变量内存;如若是普通变量,则指向常量所在地址

3、为成员变量设置默认值

4、设置对象头(包括对象的哈希码等等)

5、执行init方法,成员变量初始化,执行实例化代码块,构造方法,将对象地址分配给引用变量

针对JVM虚拟机部分,本人认为脑海中有两幅图理解即可(一副是关于jvm内存模型、一副是类加载过程)