JVM常见面试题以及解答汇总

一. 内存模型以及分区,需要详细到每个区放什么。

Java虚拟机在程序执行过程会把jvm的内存分为若干个不同的数据区域来管理,这些区域有自己的用途,以及创建和销毁时间。 
jvm管理的内存区域包括以下几个区域: 


栈区: 
栈分为java虚拟机栈和本地方法栈

重点是Java虚拟机栈,它是线程私有的,生命周期与线程相同。
每个方法执行都会创建一个栈帧,用于存放局部变量表,操作栈,动态链接,方法出口等。每个方法从被调用,直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。
通常说的栈就是指局部变量表部分,存放编译期间可知的8种基本数据类型,及对象引用和指令地址。局部变量表是在编译期间完成分配,当进入一个方法时,这个栈中的局部变量分配内存大小是确定的。
会有两种异常StackOverFlowError和 OutOfMemoneyError。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError。
本地方法栈 为虚拟机使用到本地方法服务(native)
堆区:

堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。
堆区是gc的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区最要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1。 
不过很多文章介绍分为3个区块,把方法区算着为永久代。这大概是基于Hotspot虚拟机划分, 然后比如IBM j9就不存在永久代概论。不管怎么分区,都是存放对象实例。
会有异常OutOfMemoneyError
方法区:

被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)
垃圾回收很少光顾这个区域,不过也是需要回收的,主要针对常量池回收,类型卸载。
常量池用于存放编译期生成的各种字节码和符号引用,常量池具有一定的动态性,里面可以存放编译期生成的常量;运行期间的常量也可以添加进入常量池中,比如string的intern()方法。
程序计数器:

当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。
Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。
唯一一块Java虚拟机没有规定任何OutofMemoryError的区块
jvm分区大致就这个块,具体里面还有很多细节,及其各个模块工作的算法都很复杂,这里只是对分区进行简单介绍,掌握一些基本的知识点。
 

二. 堆里面的分区:Eden,survival from to,老年代,各自的特点。

在垃圾回收算法中,有一个算法称之为复制算法。其基本思想是把内存分为大小相等的2块,每次只在其中一块区域内进行内存分配,当发生GC时,将这块区域内存活的对象复制到另外一个区域内,然后对这块区域进行Full GC,这样可以保证内存的连续性,减少了空间碎片的产生.然后这种方式牺牲了一半的内存使用空间。Eden区与Survior区的概念也由此得来。

1、Eden区

    Edeb区位于JVM中的新生代,是新对象分配内存的地方,由于堆是所有线程共享的,所以在堆上分配内存需要加锁。

而Sun JDK为了提升效率,会为每个新建的线程分配一个独立的内存区域,这块区域称之为TLAB(Thread Location Allocation

Buffer).在TLAB上分配内存是不需要进行加锁的,所以Eden区域的对象内存分配会优先在TLAB上进行.若是对象过大或者是TLAB的内存空间使用完,则对象的内存分配会在堆上进行。如果Eden区内存耗尽,则会触发Minor GC(Young GC)。

2、From Survivor和To Survivor区

针对新生代对象"朝夕生死"的特点,将新生代划分为3块区域u,分别为Eden、From Survior、ToSurvior,比例为8:1:1。

From和To是相对的,每次Eden和From发生Minor GC时,会将存活的对象复制到To区域,并清除内存。To区域内的对象每存活一次,它的"age"就会+1,当达到某个阈值(默认为15)时,ToSurvior区域内的对象就会被转移到老年代。

  可以通过设置参数-XX:MaxTenuringThreshold来设置晋升的年龄。

虚拟机提供了一个参数:-XX  PertenureSizeThreshold 使得大于这个参数的对象直接在老年代中分配内存,这样就避免了在Eden区域以及Survior区域进行大量的内存复制。

3、老年代

    老年代中是存活时间久的大对象(很长的字符串或者是数组),因此老年代使用标记-整理算法。当老年代容量满的时候,会触发一次MajorGC (FullGC)
 

三. 对象创建方法,对象的内存分配,对象的访问定位。

对象创建方法:

  JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、连接和初始化过。

如果没有,那必须先执行相应的类的加载过程。

对象的内存分配:

  对象所需内存的大小在类加载完成后便完全确定(对象内存布局),为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

  根据Java堆中是否规整有两种内存的分配方式:

  指针碰撞:所有用过的内存在一边,空闲内存在另一边,中间放着一个指针作为分界点的指示器,

分配内存就是把指针往空闲内存那边挪一段与对象大小相等的距离。在使用Serial,ParNew等收集器,

(也就是用复制算法,标记-整理算法的收集器),分配算法通常采用指针碰撞。

  空闲列表:虚拟机维护一个列表,记录哪些内存是可用的,分配的时候从列表中找到一块足够大的空间划分给对象,并更新列表。

使用CMS这种基于标记-清除算法的收集器,通常用空闲列表。

  对象创建在虚拟机中时非常频繁的行为,即使是仅仅修改一个指针指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

同步

  虚拟机采用CAS配上失败重试的方式保证更新操作的原子性

本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)

  把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存(TLAB)。

  哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。

  内存分配完之后,虚拟机要将分配到的内存空间都初始化为零值(不包括对象头),保证了对象的实例字段在Java代码中可以不赋初始值就直接使用。

对象的内存布局:

  对象在内存中可分为3个部分,对象头,实例数据,对齐填充。

  对象头的第一部分用于存储对象自身的运行时数据,如对象的哈希码,GC分代年龄,锁状态标志,线程持有的锁等。

  另一部分是类型指针,即对象指向它的类元数据的指针,通过这个来确定这个对象是哪个类的实例。

  实例数据是对象真正存储的有效信息。

对象的访问定位:

  程序要通过栈上的reference数据来操作堆上的具体对象。对象的访问方式有使用句柄直接指针

  使用句柄:java堆会划分一块内存作为句柄池,reference中存的是对象的句柄地址,而句柄中包含了对象的实例数据的地址和类型数据的地址(在方法区)

优点:对象被移动,reference不用修改,只会改变句柄中保存的地址。

  使用直接指针:reference中存的是对象的地址,对象中分一小块内存保存类型数据的地址。优点:速度快。

四. GC的两种判定方法:引用计数与引用链。

对象是否死亡的2中判定方法:引用计数和可达性分析(又称引用链)

1.引用计数

        对象再被创建时,对象头里会存储引用计数器,对象被引用,计数器+1;引用失效,计数器 -1;GC时会回收计数器为0的对象。但是JVM没有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况,无法解决对象互相循环引用。

2.引用链

        程序把所有的引用看作图(类似树结构的图),选定一个对象作为GC Root根节点,从该节点开始寻找对应的引用节点并标记,找到这个节点之后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点认为是不可达的无用节点,会被回收。

        可以作为GC Root根节点的对象有:

            a.虚拟机栈中的引用对象(本地变量表)

            b,方法区类静态属性的引用对象

            c,方法区常量引用的对象

             d,本地方法栈中的引用对象

Java中存在的4种引用类型:

   a 强引用

是指创建一个对象并把这个对象赋给一个引用变量 类似 string s="hello",只要引用存在,GC永远不会回收

  b 软引用

非必需引用,内存不足时回收。软引用主要用于用户实现类似缓存的功能,在没有被回收前可以直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真实的来源查询这些数据。

c 弱引用

描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。弱引用主要用于监控对象是否已经被标记为即将回收的垃圾,可以通过弱引用的isEnQueues方法返回对象是否被垃圾回收器标记。

d 虚引用

虚引用是每次垃圾回收的时候都会被回收,唯一作用当对象被回收时,可以收到通知。

五. GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?

第一种:标记清除 
它是最基础的收集算法。 
原理:分为标记和清除两个阶段:首先标记出所有的需要回收的对象,在标记完成以后统一回收所有被标记的对象。 
特点:(1)效率问题,标记和清除的效率都不高;(2)空间的问题,标记清除以后会产生大量不连续的空间碎片,空间碎片太多可能会导致程序运行过程需要分配较大的对象时候,无法找到足够连续内存而不得不提前触发一次垃圾收集。 
地方 :适合在老年代进行垃圾回收,比如CMS收集器就是采用该算法进行回收的。

第二种:标记整理 
原理:分为标记和整理两个阶段:首先标记出所有需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 
特点:不会产生空间碎片,但是整理会花一定的时间。 
地方:适合老年代进行垃圾收集,parallel Old(针对parallel scanvange gc的) gc和Serial old收集器就是采用该算法进行回收的。

第三种:复制算法 
原理:它先将可用的内存按容量划分为大小相同的两块,每次只是用其中的一块。当这块内存用完了,就将还存活着的对象复制到另一块上面,然后把已经使用过的内存空间一次清理掉。 
特点:没有内存碎片,只要移动堆顶指针,按顺序分配内存即可。代价是将内存缩小位原来的一半。 
地方:适合新生代区进行垃圾回收。serial new,parallel new和parallel scanvage 
收集器,就是采用该算法进行回收的。 
复制算法改进思路:由于新生代都是朝生夕死的,所以不需要1:1划分内存空间,可以将内存划分为一块较大的Eden和两块较小的Suvivor空间。每次使用Eden和其中一块Survivor。当回收的时候,将Eden和Survivor中还活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Suevivor空间。其中Eden和Suevivor的大小比例是8:1。缺点是需要老年代进行分配担保,如果第二块的Survovor空间不够的时候,需要对老年代进行垃圾回收,然后存储新生代的对象,这些新生代当然会直接进入来老年代。

优化收集方法的思路 
分代收集算法 
原理:根据对象存活的周期的不同将内存划分为几块,然后再选择合适的收集算法。 
一般是把java堆分成新生代和老年代,这样就可以根据各个年待的特点采用最适合的收集算法。在新生代中,每次垃圾收集都会有大量的对象死去,只有少量存活,所以选用复制算法。老年代因为对象存活率高,没有额外空间对他进行分配担保,所以一般采用标记整理或者标记清除算法进行回收。
-

六. GC收集器有哪些?CMS收集器与G1收集器的特点。

1、CMS收集器

  CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现,它的运作过程如下:

1)初始标记

2)并发标记

3)重新标记

4)并发清除

  初始标记、从新标记这两个步骤仍然需要“stop the world”,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,熟读很快,并发标记阶段就是进行GC Roots Tracing,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生表动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长点,但远比并发标记的时间短。

 

  CMS是一款优秀的收集器,主要优点:并发收集、低停顿。

缺点:

1)CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。

2)CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。

浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”。

3)CMS是一款“标记--清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

2、G1收集器

G1是一款面向服务端应用的垃圾收集器。G1具备如下特点:

1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。

3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,

5、G1运作步骤:

1、初始标记;2、并发标记;3、最终标记;4、筛选回收

上面几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短,并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

注:一般堆内存4G以上的可用G1垃圾回收方式。4G以下用CMS。

.-XX:+UseConcMarkSweepGC

八. Minor GC与Full GC分别在什么时候发生?

何时发生? 
(1)Minor GC发生:当jvm无法为新的对象分配空间的时候就会发生Minor gc,所以分配对象的频率越高,也就越容易发生Minor gc。

(2)Full GC:发生GC有两种情况,①当老年代无法分配内存的时候,会导致MinorGC,②当发生Minor GC的时候可能触发Full GC,由于老年代要对年轻代进行担保,由于进行一次垃圾回收之前是无法确定有多少对象存活,因此老年代并不能清除自己要担保多少空间,因此采取采用动态估算的方法:也就是上一次回收发送时晋升到老年代的对象容量的平均值作为经验值,这样就会有一个问题,当发生一次Minor GC以后,存活的对象剧增(假设小对象),此时老年代并没有满,但是此时平均值增加了,会造成发生Full GC
 

九 几种常用的内存调试工具:jmap、jstack、jconsole。

常用的内存调试工具:jps、jmap、jhat、jstack、jconsole,jstat:

jps:查看虚拟机进程的状况,如进程ID。

jmap: 用于生成堆转储快照文件(某一时刻的)。

jhat:对生成的堆转储快照文件进行分析。

jstack:用来生成线程快照(某一时刻的)。

生成线程快照的主要目的是定位线程长时停顿的原因(如死锁,死循环,等待I/O 等),通过查看各个线程的调用堆栈,就可以知道没有响应的线程在后台做了什么或者等待什么资源。
jstat:虚拟机统计信息监视工具。如显示垃圾收集的情况,内存使用的情况。

jconsole:主要是内存监控和线程监控。

内存监控:可以显示内存的使用情况。线程监控:遇到线程停顿时,可以使用这个功能。
 

十. 类加载的五个过程:加载、验证、准备、解析、初始化。

https://blog.csdn.net/chenge_j/article/details/72677766

十一. 双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。

https://blog.csdn.net/u014629433/article/details/51645271

十二. 分派:静态分派与动态分派。

这里所谓的分派指的是在Java中对方法的调用。Java中有三大特性:封装、继承和多态。分派是多态性的体现,Java虚拟机底层提供了我们开发中“重写”和“重载”的底层实现。其中重载属于静态分派,而重写则是动态分派的过程。除了使用分派的方式对方法进行调用之外,还可以使用解析调用,解析调用是在编译期间就已经确定了,在类装载的解析阶段就会把符号引用转化为直接引用,不会延迟到运行期间再去完成。而分派调用则既可以是静态的也可以是动态(就是这里的静态分派和动态分派)的。

1.静态分派

静态分派只会涉及重载,而重载是在编译期间确定的,那么静态分派自然是一个静态的过程(因为还没有涉及到Java虚拟机)。静态分派的最直接的解释是在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。比如创建一个类O,在O中创建了静态类内部类A,O中又有两个静态类内部类B、C继承了这个静态内部类A,那么实际上当编写如下的代码:

public class O{
    static class A{}
    static class B extends A{}
    static class C extends A{}
    public void a(A a){
        System.out.println("A method");
    }
    public void a(B b){
        System.out.println("B method");
    }
    public void a(C c){
        System.out.println("C method");
    }
    public static void main(String[] args){
        O o = new O();
        A b = new B();
        A c = new C();
        o.a(b);
        o.a(c);
    }
}


运行的结果是打印出连个“A method”。原因在于静态类型的变化仅仅在使用时发生,变量本省的类型不会发生变化。比如我们这里中A b = new B();虽然在创建的时候是B的对象,但是当调用o.a(b)的时候才发现是A的对象,所以会输出“A method”。也就是说在发生重载的时候,Java虚拟机是通过参数的静态类型而不是实际参数类型作为判断依据的。因此,在编译阶段,Javac编译器选择了a(A a)这个重载方法。

虽然编译器能够在编译阶段确定方法的版本,但是很多情况下重载的版本不是唯一的,在这种模糊的情况下,编译器会选择一个更合适的版本。

2.动态分派

动态分派的一个最直接的例子是重写。对于重写,我们已经很熟悉了,那么Java虚拟机是如何在程序运行期间确定方法的执行版本的呢?

解释这个现象,就不得不涉及Java虚拟机的invokevirtual指令了,这个指令的解析过程有助于我们更深刻理解重写的本质。该指令的具体解析过程如下:

找到操作数栈栈顶的第一个元素所指向的对象的实际类型,记为C
如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回非法访问异常
如果在类型C中没有找到,则按照继承关系从下到上依次对C的各个父类进行第2步的搜索和验证过程
如果始终没有找到合适的方法,则抛出抽象方法错误的异常

从这个过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者程称为接受者)的实际类型,所以当调用invokevirtual指令就会把运行时常量池中符号引用解析为不同的直接引用,这就是方法重写的本质。

3.虚拟机动态分派的实现

其实上面的叙述已经把虚拟机重写与重载的本质讲清楚了,那么Java虚拟机是如何做到这点的呢?

由于动态分派是非常频繁的操作,实际实现中不可能真正如此实现。Java虚拟机是通过“稳定优化”的手段——在方法区中建立一个虚方法表(Virtual Method Table),通过使用方法表的索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址(由于Java虚拟机自己建立并维护的方法表,所以没有必要使用符号引用,那不是跟自己过不去嘛),如果子类没有覆盖父类的方法,那么子类的虚方法表里面的地址入口与父类是一致的;如果重写父类的方法,那么子类的方法表的地址将会替换为子类实现版本的地址。

方法表是在类加载的连接阶段(验证、准备、解析)进行初始化,准备了子类的初始化值后,虚拟机会把该类的虚方法表也进行初始化。
 

参考地址:

https://blog.csdn.net/qq_35181209/article/details/77931494

http://blog.sina.com.cn/s/blog_61d758500102xijx.html

https://blog.csdn.net/FateRuler/article/details/81158510?utm_source=blogxgwz9

http://www.cnblogs.com/feicheninfo/p/9684736.html

https://www.cnblogs.com/mengchunchen/p/7859668.html

https://blog.csdn.net/weixin_30300689/article/details/79888642

https://blog.csdn.net/honjane/article/details/51542183

猜你喜欢

转载自blog.csdn.net/qq_34433210/article/details/85006757