今天你准备了吗——JVM面试题

103 阅读13分钟

JVM面试题

1.JVM的内存模型和分区情况?

方法区:线程共享区域,存放类的信息/静态变量/常量等信息。方法区有一个运行的常量池,用于存放静态编译产生的字面量,该常量池中不一定存放编译时期的常量,在运行时期也可以放入到常量池中。

堆:线程共享区域,存放对象实例,所有的对象和数组都要存放在堆中,是JVM 中内存中最大的部分,堆在jvm启动时创建,堆中对象不用显式释放,gc会帮我们释放并回收内存收。

虚拟机栈:线程私有区域,为java方法进行服务的,方法执行的内存模型,栈的结构是由帧组成,调用一个方法就是压入一栈,方法执行完成之后就出栈,帧上面包括局部变量表/操作数栈/动态链接等,生命周期和线程相同,不需要GC。

本地方法栈:线程私有区域,为本地方法native方法执行服务。不需要GC。

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

2.如何判断一个对象是否存活?

判断一个对象是否存活有两种方式:

引用计数法:

所谓引用计数器法就是给每个对象设置一个计数器,当其他的地方引用这个对象的时候就会将计数器进行加1,引用失效时,就会将该计数器进行减1,当这个对象的计数器的数值为0,那么该对象就已经死了。

问题: 引用计数器法无法解决循环引用的问题。例如当A引用B,B引用A,AB对象的计数器都是1,无法完成垃圾的回收。所有虚拟机不采用这种算法。

可达性分析法:

通过一系列称为GCROOTS的根对象作为起始节点,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到GCroots没有任何引用链进行相连,证明这个对象是不可达。他会被进行第一次标记并进行筛选,筛选的条件就是是否有必要执行finalize,当对象没有重写finalize或者之前虚拟机已经调用了该finalize方法,虚拟机视为这两种情况都没有必要进行执行。

若该对象判定为可以去执行finalize方法,则该对象会被放到F-Queue 队列,finalize方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-queue中的对象进行第二次小规模的标记,若对象要在 finalize中成功变成可达。任何一个对象的 finalize()方法都只会被系统调用一次。

能够作为GCROOTS的对象包括:

1.在虚拟机栈中引用的对象,例如各个线程被调用的方法堆栈中使用的参数/局部变量等;

public class Test {
    public static  void main(String[] args) {
        Test a = new Test();
        a = null;
    }
}

2.方法区类静态属性所引用的对象;

public class Test {
    public static Test s;
    public static  void main(String[] args) {
        Test a = new Test();
        a.s = new Test();
        a = null;
    }
}

3.方法区常量池中引用的对象;

public class Test {
    public static final Test s = new Test();
    public static void main(String[] args) {
        Test a = new Test();
        a = null;
    }
}

4.本地方法栈JNI引用的对象。

3.常见的垃圾回收算法有哪几种类型?优缺点?

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

标记-清除算法:

标记阶段:确定所有要回收的对象,进行标记;

清除阶段:将标记阶段确定不可用的对象进行清除。

缺点:1.内存碎片化严重,导致在后续程序中无法为大对象找到足够的内存空间。

2.对于大量大对象的回收,效率不是很高。

复制算法:

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

1.需要浪费额外的内存作为复制区;

2.当存活率较高时,复制算法效率较低。

标记-整理算法:

在标记清除法的基础上添加了一个整理的过程 ,即将所有的存活对象都往一端移动,紧邻排列,再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。

1.每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下

分代收集算法:

分代收集算法根据对象的存活周期的不同将堆分成新生代和老年代,默认比例时1:2,新生代又分为Eden/s0/s1;三者的比例时1:1:8;我们可以跟据新老生代的特点选择最合适的垃圾回收算法,把新生代发生的GC称为Minor GC,老年代发生的GC叫做Old GC。

垃圾回收过程是:

大部分对象都会被回收,会被分配在Eden区,当Eden区满时,发生Minor GC ,存活的对象被放到S0区,并且年龄进行加1,最后把Eden区的对象全部清理掉。

当触发下一次Minor GC时,会把Eden区的存活的对象和s0区域的对象移动到S1区域,并且年龄进行加1,同时清空Eden区域和S0区域。

重复上述第二步,当对象的年龄到达15时,就会将对象回收到老年代中。

4.如何被回收到老年代?

1.分代收集算存活的对象年龄到达15

2.大对象,当为某个对象分配连续内存时,此对象不会在分配在Eden区域,会直接分配在老年区; 3.在S0或者S1区域的相同年龄的对象大小之和大于S0或者S1的内存的一半,则年龄大于该年龄的对象也会被回收到老年代中。

5.老年代满了会发生什么?能解释一下Stw?

如果老年代满了,会触发Full GC,Full GC 会同时回收新生代和老年代,就会导致STW,所谓的STW,就是在GC期间,只有垃圾回收器线程在工作,其他的工作线程就会被挂起。

为了不快速的发生FUll GC ,这就是新生代和老年代的比例是1:2原因,避免对象过早的进入到老年代,尽可能晚点的触发FULL GC ;同时在经过s0和s1的缓冲,只有少数的对象进入到老年代。

这时我们就会在一个合适的时间点发起GC,这个时间点就是safe point;

1.循环的末尾;

2.方法返回前;

3.调用方法的回调之前;

4.抛出异常的位置。

6.垃圾收集器的种类?

新生代的垃圾回收器:Serial ParNew ParrallelScavenge

老年代的垃圾回收器:Serial Old Parallel Old CMS

同时在新老年代的回收器:G1

Serial收集器:

单线程的垃圾收集器;Client 模式下的虚拟机使用。

ParNew 收集器:

是 Serial 收集器的多线程版本;ParNew 主要工作在 Server 模式,我们知道服务端如果接收的请求多了,响应时间就很重要了,多线程可以让垃圾回收得更快,也就是减少了 STW 时间,能提升响应时间,所以是许多运行在 Server 模式下的虚拟机的首选新生代收集器,另一个与性能无关的原因是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作,CMS 是一个划时代的垃圾收集器,是真正意义上的并发收集器

Parallel Scavenge 收集器:

Parallel Scavenge 收集器也是一个使用复制算法多线程。而Parallel Scavenge 收集器关注的是吞吐量,所以更适合做后台运算等不需要太多用户交互的任务。

Serial old收集器:

Serial Old 是工作于老年代的单线程收集器,此收集器的主要意义在于给 Client 模式下的虚拟机使用。

Parallel Old 收集器:

Parallel Scavenge 收集器的老年代版本。使用多线程和标记整理法。真正实现了「吞吐量优先」的目标。

CMS 收集器:

CMS 收集器是以实现最短 STW 时间为目标的收集器,如果应用很重视服务的响应速度。

初始标记——》并发标记——》重新标记——》并发清除;

初始标记的是GCroot能关联的对象,速度很快;并发标记是进行GCROOTS 的tracing的过程,重新标记就是修正并发标记期间因用户线程继续运行而导致的产生的那一部分对象的标记记录。

7.线上常用的JVM的参数有哪些?

-Xms:初始化堆的大小;

-Xmx:最大堆的大小;

-XSS:Java每个线程的栈大小;

XX:NewSize=n:设置年轻代的大小;

XX:NewRatio=n:设置年轻代和年老代的比值。

XX:MaxPermSize=n :设置持久代的大小;

XX:+UseSerialGC:设置串行收集器;

XX:+UseParallelGC:设置并行收集器;

XX:+PrintGCDetial:详细打印GC日志。

8.什么是内存溢出和内存泄露?

内存溢出:程序在申请内存时,没有足够的空间供其使用。

产生原因:

1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

3.代码中存在死循环或循环产生过多重复的对象实体;

4.启动参数内存值设定的过小.

解决步骤:

第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)

第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

检查对数据库查询中,是否有一次获得全部数据的查询;

检查代码中是否有死循环或递归调用

检查是否有大循环重复产生新对象实体。

第四步,使用内存查看工具动态查看内存使用情况

内存泄露: 程序在申请内存之后,无法释放已经申请的内存空间。一次内存泄露不会产生什么影响,但是多次的内存泄露堆积,最终会消耗所有的内存。

产生原因:

1.各种链接,如果没有显示调用close的方法,不会被GC回收导致内存泄露。

2.监听器的使用:在释放对象的同时没有删除监听器的时候

9.什么情况会发生栈溢出?

方法创建一个很大的对象,如List和Array

是否产生了死循环或者循环调用。

是否引用较大的全局变量。

10.请说明一下四种引用?

1.强引用:new出来的对象之类的引用,只要强引用还在,就不会被回收。哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,将强引用赋值为null,这样一来,JVM就可以适时的回收对象了

2.软引用:非必须但有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。通常用来做网页缓存和图片缓存。SoftReference

3.弱引用:无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收;WeakReference。threadLocalMap中的key是弱引用。 4.虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,无法通过虚引用来获取对象。

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

加载/验证/准备/解析/初始化五个阶段。

加载:将class字节码文件加载到内存中,并将这些数据转换为方法区中的运行时的数据(静态变量/静态代码块)

在堆中生成一个Class对象代表这个类,作为方法区类数据的访问入口。

验证:为了确保Class文件的字节流的信息不会危害到虚拟机。主要验证文件格式:验证字节流是否符合Class文件的规范;验证元数据;字节码的验证;符号引用的验证:确保解析动作能正常进行。

准备: 准备阶段主要是为了类的静态变量分配内存并将其初始化默认值。

解析: 主要完成符号引用到地址应用的转换动作。

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

12.类的双亲委派模型机制?什么是类加载器?类加载器有哪些?

当一个类收到类加载请求时,不会自己先加载这个类,而是将其委派给父加载器,由父加载器去加载,依次向上,所有类的加载都会被传递到顶层的启动类加载器中,只有父类加载器没有找到所需的类时,子类才会去尝试加载该类。

类加载器:通过实现类的全限定名获取该类的二进制字节流的代码块。

启动类加载器:将jre包下的/lib文件夹下的类库加载到内存中。

扩展类加载器:将jre包下的/lib/ext里面的类库加载到内存中

自定义加载器:通过继承自java.lang.ClassLoader来实现。用于加载用户路径上的类库。

13.减少GC开销的措施有哪些?

1.不要显示调用System.gc();会增加主GC的频率,也增加了间歇性停顿的次数。

2.根据情况尽量少用静态变量。他会一直存在于内存中,不会被GC回收。

3.对象不用时尽量将其显示的设置为NULL。

14.简述下垃圾回收机制?

在java中,程序员不需要进行显示的去释放对象内存的,而是交由虚拟机自己执行。在虚拟机中,有个垃圾回收线程,优先级很低,一般不会被执行,只有在虚拟机空闲的时候或者当前堆内存满时才会被执行,回收这些对象。

\