JVM方面-----面试汇总大全(未完待续)

面试题1. JVM内存模型

在这里插入图片描述
先说堆吧:
堆是所有对象分配的地方,也是GC垃圾回收的主要的地方,而且现在在年轻代中使用的都是分代收集算法,首先堆分为新生代和老年代,但是在新生代中,又分为Eden,From Survivor ,To Survivor 且为8:1:1的一个分配,如果一个创建的新对象都会分配到Eden区(其实每一个对象都有一个分代年龄,当每一次GC如果存活下来一次之后年龄就会加一,到了15次左右就会移动到survivor中),但是如果Eden满了之后就会进行一个Full GC。
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值,一般都是把这个堆内存的初始值设置为何堆内存的最大值保持一致,能够防止内存抖动

方法区:也被称为是永久区

  • 方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。
  • 类型信息包括类的完整名称、父类的完整名称、类型修饰符( pub Ii c/protected/pri va )和类型的直接接口类表。
  • 常量池包括类方法 域等信息所引用的常量信息。域信息包括域名称、域类型和域修饰符。在str s = “你好”的时候这个字符串其实就加载到常量池中去了
  • 方法信息包括方法名称、返回类型、方法参数、方法修饰符、方法字节码、操作数技和方法梳帧的局部变量区大小以及异常表。

Java虚拟机栈:
其实就是每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
在这里插入图片描述

面试题2. 为什么要有双亲委派模型

  • 为了安全,也可以防止一些别人自己写的 “病毒代码” 代码注入到JVM的内存中,使用双亲委派模型来组织类加载器间的关系,能够使类的加载也具有层次关系,这样能够保证核心基础的Java类会被根加载器加载,而不会去加载用户自定义的和基础类库相同名字的类,从而保证系统的有序、安全。
  • 还一个原因就是避免多份字节码的加载,而占用过多的内存空间

面试题3. GC的过程

在这里插入图片描述
1. Minor GC过程

  • 首先如果当Eden区满了之后,会先进行一次minorGC,然后把Eden区中的存放对象放到S1中
  • 但是随着Eden区一直不断的有新的对象产生,Eden区又满了,此时需要再次进行一次minnorGC 这次GC的范围不仅有Eden区还有上一步S1中存活的对象,然后把S1中存活的对象复制到S2中。(补充:在S1和S2中他们是相对的,如果说当前对象存活在S1区,就去清理S1然后把存活的对象放到S2区,反之如果存活的对象在S2区时也是一样的)

2, 对象进入老年代的4种情况

  • 第一种是当假如进行Minor GC时发现,存活的对象在ToSpace区中存不下,那么把存活的对象存入老年代
  • 第二种就是比如一个创建的比较大的新对象放Eden区的话,就可能放不下或者说是当一个新建的对象达到一个最大值之后(也就是PretenureSizeThreshold这个参数),这个时候就需要放到老年代
  • 第三种就是当一个对象的年龄超过了配置的这个参数-XX:MaxTenuringThreshold默认的是15,也就是新生代Eden区每次向S1或者S2区中复制一次年龄就增加1岁,当到达15岁之后就会被转移到老年代中
  • 第四种就是动态对象年龄判定,如果在From空间中,相同年龄所有对象的大小总和大于From和To空间总和的一半,那么年龄大于等于该年龄的对象就会被移动到老年代,而不用等到15岁(默认):

3. Full GC
由于一直的在往老年代添加新的对象,直到老年代的内存也开始不够用的时候,这个时候就会发生一次Full GC来进行对整个堆内存进行清理,然后依次往复执行

4. 空间分配担保
在发生Minor GC前,虚拟机会先进行去检查老年代最大可以使用的连续内存是不是大于新生代所有对象的总空间,如果这个条件成立,代表执行Minor GC可以确保是安全的。如果不成立的话,就会去看HandlerPromotionFailure这个参数的值设置的是false还是true,也就是是否允许担保失败。代表虚拟机担保说:你尽管GC出了了事我负责。

如果是HandlerPromotionFailure为true的话,会继续进行判断老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于的话,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的,如果小于的话,那么这次Minor GC将升级为Full GC。

如果HandlerPromotionFailure为false,那么这次Minor GC也将升级为Full GC

面试题4. GC涉及了什么算法

1.标记-清除算法
在这里插入图片描述
缺点:

  • 效率不高
  • 空间问题,会产生大量不连续的的内存碎片

2.复制算法
在这里插入图片描述
缺点:

  • 虽然解决了标记清除算法的效率不足问题,但是又带来了浪费内存空间的问题

3.标记整理算法
在这里插入图片描述

4.分代收集算法:

  • 分代收集算法在对堆的垃圾回收中用到,把堆内存分为新生代和年老代,所以现代的虚拟机基本都是这种思想
  • 新生代使用:复制算法
  • 老年代使用:标记 - 清除 或者 标记 - 整理 算法

GC中的分带收集算法的实现原理:

首先由于GC在分代收集算法中,是把年轻代和老年代进行分开收集的,但是有没有考虑过一个问题,就是当在对年轻代进行回收的时候,由于是按照GCroots开始进行根不可达的算法检索,那么就会出现在年轻代中没有任何引用,但是在老年代中有一个引用,分带搜索的时候又不能连同老年代也进行一起GCroot,所以就会把年轻的对象误回收的这种情况?

  1. 针对这种情况Java虚拟机用了一个叫做CardTable(卡表)的数据结构来标记老年代的某一块内存区域中的对象是否持有新生代对象的引用,

  2. 这个卡表其实是一个比特位的集合,每一个位置的只能是1或者0,每个位置对应这个老年代的一块内存区域(卡表的数量取决于老年代的大小和每张卡对应的内存大小),卡表位置的1 就表示该卡位置对应的老年代的内存区域含有老年代对新生代的引用,0 就表示没有老年代对新生代的引用

  3. 当每次年老代对象中某个引用新生代的字段发生变化时,这个老年代中对应的卡表的位置的状态就要随之变化来维持卡表的准确性,每次变化Hotspot VM就必须将该卡所对应的卡表元素设置为1还是0,从而将该引用字段所在的卡标记为脏,这个过程中会执行一个写屏障,在高并发情况下,频繁的写屏障很容易发生虚共享(false sharing),从而带来性能开销。
    在这里插入图片描述
    那么为什么要多使用一个卡表来进行这么做呢,每次变化都还要维护这个卡表?

  4. 首先他能从根部解决我上面提出的问题,不会再清理新生代的时候,不知道老年代有没有引用新生代的对象,然后把新生代的对象给误回收的问题解决了

  5. 还有一点就是在进行新生代的垃圾回收的时候,并不用在对老年代也进行GCroots遍历老年代有没有对新生代的引用,而是直接去扫描一下卡表中状态为1 的位置引用的老年代的内存块(就是对应的4K大小的那个内存区域)
    在这里插入图片描述
    由于上面提到的每次维护卡表的时候,都会有一次写屏障,

  6. 就是JDK 7中引入的解决方法,引入了一个新的JVM参数-XX:+UseCondCardMark,在执行写屏障之前,先简单的做一下判断。如果卡页已被标识过,则不再进行标识。


if (CARD_TABLE [this address >> 9] != 0)
  CARD_TABLE [this address >> 9] = 0;

面试题5. JVM中如何判断一个对象是否被回收?

  • 其实采用的是:根搜索算法 是 JVM 虚拟机判断一个对象是否存活的算法。它把内存中的每一个对象看成一个节点,并定义了一些对象作为根节点(GC Roots)
  • 如果一个对象中有对另一个对象中的引用,那么就认为第一个对象有一条指向第二个对象的边。JVM会起一个线程从所有的GC Roots开始往下遍历,当遍历完之后如果发现有一些对象不可到达,那么就认为这些对象已经没有用了,需要被回收。

面试题6. 哪些对象是GCRoots

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

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

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

4.本地方法栈中JNI(即一般说的Native方法)中引用的对象

面试题7. 哪些算法是运用在年轻代,哪些算法那是运用在老年代的,以及年轻代和年老代都有哪些垃圾收集器?

年轻代运用的算法:

  • 复制算法

新年代运用的垃圾收集器:

  • Serial
  • ParNew
  • Paralle Scavenge

老年代运用的算法:

  • 标记-清除算法

老年代运用的垃圾收集器:

  • Serial Old
  • Paralle Old
  • CMS(当前用的最多的)
  • G1(当前用的最多的)

收集器的特点对比:

  • Serial 和 SerialOld 单线程,简单高效。进行GC的时候会把所有工作线程停掉,是早期的回收器。
  • ParNew是Serial的并发版本,都是新生代收集器
  • Paralle Scavenge(采用复制算法,吞吐量优先收集器) 和 Paralle Scavenge Old(标记-整理) 组合追求吞吐量,可以高效的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的程序
  • CMS 是一种以获取最短回收停顿时间为目标的收集器。对于尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验的系统很适合。采用标记清除算法,所以缺点就是会有大量的空间碎片。

面试题8. CMS工作原理,会stop the world吗?CMS在哪个阶段会停顿,哪个阶段停顿的时间最长?

会。因为CMS分为四个阶段,且在并发标记和标记清除两个步骤的用时会相对较长一些

  • 初始标记(需要进行Stop The World停顿) (只是简单的标记一下GC Roots能关联到的对象,速度很快)
  • 并发标记 (并发标记其实就是进行GCroots Tracing 的过程)
  • 再次标记(也需要进行Stop The World且时间比初始标记要长一些)(是为了修正并发标记期间因用户程序继续运行导致标记产生变动的那一部分对象的记录)
  • 标记清除
    CMS收集器的优点:
  • 低停顿
  • 并发收集垃圾
    CMS收集器的缺点:
  • CMS对CPU的资源非常敏感
  • CMS无法清除浮动垃圾(就是在并行执行期间应用程序不断产生新的垃圾,CMS也在不断收集,肯定有一部分不能收集到,这个部分就是浮动垃圾,回流到下次GC的时候进行清理)
  • 以为CMS是基于标记-清除的的算法来实现的,所以会产生大量的空间碎片

说一下G1收集器:

  1. 首先G1是把整个Java堆中分为很多个大小相等的Region,虽然还保留着分代垃圾回收的概念,但是新生代和老年代之间并不是进行物理隔离的了,但是每个对象又不可能只是在一个Region中,所以这就衍生出了 卡表 也就是RemembSet的出现了
  2. G1的特点就是并行和并发,充分利用当代多核多CPU的硬件特点来缩短STW的时间
  3. 然后是分代收集 这里就用到了卡表来进行实现的
  4. 然后不会产生内存碎片,是因为G1从整体宏观来看其实他主要采用的是“标记-整理“”算法思想实现的垃圾收集器,但是从局部来看其实他是两个Region之间基于复制算法实现的垃圾收集器,,因此这两种算法都不会产生内存空间碎片的问题,主要是采用化整为0 的思想。

面试题9、讲一下Java中的四种引用?

  1. 第一种是强引用,也就是我们经常用到的User user = new User()这种模式
  2. 第二种是软引用,也就是说如果你要是定义且是用了这种软引用的话,那么如果JVM中的内存空间足够,垃圾回收器就不会回收它【可以用来做缓存一种比较大的对象,当内存不够用的时候会自动回收对象】
SoftReference aSoftRef=new SoftReference(这里传进来的是一个对象);  
  1. 第三种是弱引用,这种就是容易引起内存泄漏的一种引用,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。比如在ThreadLocal中就有用到弱引用(需要谨慎使用)
 WeakReference<People>reference=new WeakReference<People>(这里传进来的也是一个对象);  
  1. 第四种是虚引用,就是说只要你用到了虚引用,如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

在这里插入图片描述
补充几种利用这个四种引用可能会使用到的场景:

  • 一般在使用软引用的时候主要是为了解决一些当加载一些大的文件或者资源的时候可能会导致内存溢出,用来做缓存,当加载的资源太大的话防止报错OOM
    在这里插入图片描述
  • 弱引用是只要进行垃圾回收就会被清理,主要用处也是为了降低OOM的概率,一般可以和WeakHashMap进行连用
  • 而虚引用一般使用的时候都是跟一个ReferenceQueue队列进行连用,因为由于虚引用有没有GC他都会被回收,他的主要作用就是可以进行对一个对象进行监控,如果被回收了就放到ReferenceQueue队列中,比如Spring中的AOP的后置通知实现就是这样的原理实现

面试题10. 如何判断一个对象存活,如果要让对象不被回收该怎么办?

在这里插入图片描述
首先判断一个对象是不是存活着,那么关键主要是看在JVM中通过GC roots引用的对象是如何判断的?(就是能否在方法区或者堆栈中找到一个该对象的引用,如果有这个对象就是不可回收的,如果没有JVM就会回收该对象)

  1. 第一个就是虚拟机栈中引用的对象(其实就是上面的每一个栈帧中,这个每个栈帧对应java中的每一个方法,说白了就是每一个方法中的成员变量)
  2. 方法区中的类静态属性引用的对象
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI的引用对象

然后是不让对象回收,其实就是知道JVM中回收类的条件:

  1. Java堆中不存在该类的所有实例;
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类
  4. JVM对类的回收要求比较严格,即使同时满足上面的所有条件也只是能够有资格回收类,但是并不能够保证一定会回收

面试题11. gc的原理,有哪些垃圾收集器,优缺点,有哪些垃圾收集算法,优缺点

面试题12. gc内存管理

面试题13. 类加载机制,一个类加载到虚拟机中一共有几个步骤,类加载过程中实例变量会被初始化几次,这些步骤的顺序哪些是固定的,哪些是不固定的,为什么不固定?

要想先回答这个一连串问题,那么就需要把整个类加载的机制都给搞明白,里面的问题也就自然而然的出来了。

(1)加载阶段

  • 首先加载的阶段的事情都是由ClassLoader来进行完成的功能,主要的功能就是下面几个
  • 通过一个类的全限名来获取定义此类的二进制字节流
  • 把这个二进制字节流所代表的的静态存储结构给转换为JVM中运行时数据区的数据结构,其实就是把什么对象啊,变量啊,方法啊之类的都放到如:堆中,方法区中,虚拟机方法栈中等各自的位置上
  • 在内存中生成一个代表该类的Class对象,作为方法区这个类的各种数据的入口
  • 补充:其实也就是因为类在加载这一步是以二进制字节流的形式存在,才得以让我们在此基础上做一些很多的“”小动作“”,比如:我们可以对这些二进制字节流来使用动态代理技术、

(2)链接(验证—>准备—>解析)

  • 链接里面又分了三小步,
  • 第一小步验证其实就是为了保证JVM虚拟机的安全,来阻止一些不安全的因素和保证class文件格式的正确性
  • 第二小步就是进行第一次赋值 的时候,会先把一些我们定义的各种类型的变量先进行一个默认的初始化赋值,如:int i;就会先把i =0,Long id 会先把id = 0L,如果是String s 会默认初始化为 null ,Boolean 的话默认为 false
  • 第三小步的目的其实就是虚拟机把常量池内的符号引用替换为直接引用的过程(这个过程不一定是必须在链接这个过程中的,他也有可能是在初始化之后来进行完成的,所以这个步骤的顺序不是完全固定的

(3)初始化

  • 这一步其实就是二次初始化,也就是会去执行我们自己的定义的初始化类的代码

所以上面的问题也就都知道了

面试题14. 哪些情况下会触发java的类加载

  • 第一种就是在用new关键字实例化对象的时候,读取或者设置一个类的静态变量且被final修饰的时候,调用一个类的静态方法的时候都会触发类的加载
  • 使用java.lang.reflect包下的方法对类进行反射的时候,如果对类没有初始化过,就会触发其初始化
  • 当需要初始化一个类的时候,发现其父类还没有被初始化,会先去初始化父类
  • 当虚拟机启动的时候,用户需要去执行一个主类(包含main方法的那个类)

面试题15. 说一下JVM的内存结构,哪些是共享的 ,哪些是线程私有的,java虚拟机栈里面存放的是什么?

  • 其实一共就分为5大主要的区域分为堆、方法区(常量池)、本地方法栈、虚拟机栈、程序计数器、其实还有常量池,期中堆内存和方法区是线程共享的
  • 而本地方法栈,虚拟机栈,程序计数器都是属于线程私有的,都存放在方法栈贞中

java虚拟机栈中存放的其实有以下几部分:

  • 栈帧:当线程去执行方法的时候,其实每个方法的执行都会创建一个栈帧,当方法开始执行就是入栈,方法调用结束然后出栈。
  • 局部变量表:方法中会有各种基本类型的数据类型、引用类型。其实在进入一个方法的栈贞的时候,这个栈帧的大小就已经被固定下来了,在运行期间就不会改变的。所以局部变量表的内存大小其实都是在编译期间就已经完成了内存大小的分配。

面试题16. JVM的垃圾回收策略

  • 第一种:引用计数法进行决定是否需要垃圾回收
  • 第二种:可达性分析来决定一个类是否需要被回收

面试题17:JVM(内存模型、GC垃圾回收,包括分代,GC算法,收集器、类加载和双亲委派、JVM调优,内存泄漏和内存溢出)

面试题18:G1和CMS的区别(JDK1.8)?

G1的描述:

  • G1将Java堆分成多个连续的相等大小的内存区域(Region),跟老的垃圾回收一样,它在逻辑上也分为不同的年龄段(Eden,Survivor,Old),但是不同的是,这些区域并不是固定大小

区别一:堆分配的空间不同

  • CMS 将堆逻辑上分成Eden,Survivor(S0,S1),Old,并且他们是固定大小JVM启动的时候就已经设定不能改变,并且是连续的内存块
  • G1 将堆分成多个大小相同的Region(区域),默认2048个,在1Mb到32Mb之间大小,逻辑上分成Eden,Survivor,Old,巨型,空闲,他们不是固定大小,会根据每次GC的信息做出调整

区别二:G1可以 预测停顿的时间,但是CMS就不能预测停顿的时间

  • 相比CMS,G1可以设定每次GC的时间,从而让GC在规定时间内回收效益最大的内存

区别三:压缩策略不同

  • 如果当CMS不启用压缩的时候会产生很多的垃圾碎片,当产生很多内存碎片的时候,找不到空间来分配剩余的对象,或者设定参数,使它合并相邻的的空闲内存,当合并超过一定次数后触发Full GC,进行压缩
  • G1中每次回收过程中,将多个Region拷贝到空闲Region的时候都会进行压缩

面试题19:jvm调优参数

面试题20. 讲讲 GC 机制,知道担保机制吗

担保机制就是:在发生MinorGC的时候虚拟机会先进行检测每次晋升到老年代的平均大小是不是大于老年代的剩余大小的空间容量大小,如果大于的话,就可以直接进行一次(在老年代发生)full gc ,如果小于的话,则会去查看HandlePromotionFailure设置是不是是允许担保失败,如果允许,那就只会进行MinorGC,如果不允许的话,还是会进行一次full GC

面试题21. 垃圾回收算法,新生代和年老代用的什么算法,为什么用这个算法?

新生代使用的是复制算法,主要有以下几点原因:

  1. 首先使用复制算法的特点就是:两块内存,先把B进行预留出来,然后把A中的存活对象进行标记,然后在清除回收的对象,把存活的对象复制到B区,这样不会有内存碎片产生,让内存可以连续性。
  2. 第一点就是存活的对象复制后产生的是一组连续的内存空间
  3. 第二点就是由于在新生代中垃圾对象多于存活的对象,这样的场景复制算法那更加的高效。

老生代使用的是标记-整理算法,主要有以下几点原因:

  1. 首先因为老年代的存活对象多于垃圾对象,而且老生代区域中的对象生命周期较长,采用复制算法将会产生频繁的复制操作,对应的开销较大,而标记整理算法可以很好的避免这个问题,提升垃圾回收的效率。

面试题22. 知道为什么loadClass和class.forName()之间的区别和好处吗?

(1)你得先能说出JVM中loadClass()的类装载的过程(分三大步):

  1. 第一大步是加载:先通过ClassLoader对class字节码文件进行加载,然后加载到JVM中并将这些静态数据转化为运行时的数据区
  2. 第二大步是链接:其中又包含了三小步:
    1. 分别是效验:检查加载过来的Class文件的安全性和正确性
    2. 然后是准备:为类变量分配存储空间,然后并设置类中的变量设置初始值比如(int 就是默认值为0,boolea 就是false,都是在这个过程完成的)
    3. 最后是解析:JVM将长量池中的符号引用转换为直接引用,这个是可选的
  3. 第三大步是:类的初始化就是执行类变量的真实赋值(这里的赋值就是你定义的int i = 10 ,就把10付给变量i),和执行静态代码块的内容

(2)他们之间的区别和各个地方的用的时候的好处?

  1. 首先我们使用class.forName(“com.mysql.cj.jdbc.Driver”)一般都是这么用,他的好处是使用forName加载之后的类都是经过初始化的,比如我们一般加载数据库驱动的时候是这样的,就是为了进行初始化Driver驱动对象,这个相当于完成了三大步整个类的装载和初始化,
    在这里插入图片描述
    上面的这个forName加载就需要把这个类进行初始化
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
使用了class.forName的时候下面这个静态代码块也会进行加载
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

  1. 但是我们在使用loadclass的时候其实就只是完成了上面所说的类的装载的第一大步而已,并没有全部进行加载完成,比如说在Spring中,这么做的目的就是为了在启动Spring容器的时候加快加载classpath下面的类的时候使用的就是loadclass()的懒加载机制,不用把类进行初始化节省了时间,下图中到那个resolve 为false
    在这里插入图片描述

测试:

  1. G1内部是如何分区的(region)
  2. GC 可达性分析中哪些算是GC ROOT
  3. 一个类在什么情况下会被加载到虚拟机中
  4. 双亲委派模型,怎么打破双亲委派
  5. CMS哪个阶段是并发的哪个阶段是串行的?(其实都是并发的,只不过初始标记和重新标记是垃圾回收线程的并发,而并发标记和并发回收是用户线程和回收线程的并发)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章