JVM

一、(面试题)Java是编译型语言还是解释型语言?

java源代码由编译器编译为字节码,字节码由jvm解释器解释执行

 

二、HotSpot架构图

JVM

 

 

三、类加载机制

 

类加载机制:Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程

 

JVM

 

加载(Loading)

  时机:非一次性,需要时加载(new对象、反射等等)

  过程:ClassLoader(类加载器)通过双亲委派模型进行加载

  结果:在堆中新建java.lang.Class的对象,在方法区存储类的相关信息

连接(Linking)

  验证:验证类是否符合Java以及JVM规范。(例:java7不能加载由java8编译出来的class文件)

  准备:类的静态成员分配内存初始化默认值。(例:int初始化为0,对象初始化为NULL)

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

初始化(Intialization)

  为类的静态成员赋正确的值,执行静态代码块

 

四、双亲委派模型

 

双亲委派模型:ClassLoader自己不会首先加载类,而是将请求委派给父加载器,依次向上,若不在父加载器范围内,则再由自己加载。

 

目的:

  1.安全:核心类不会被篡改

  2.高效:防止类重复加载

JVM

 

 

源码:

JVMJVM

 

从源码可知,父加载器的实现方式:组合,而不是继承。

 

(面试题)为什么JDBC加载mysql驱动时,只能使用Class.forName,而不是Thread.currentThread().getContextClassLoader().loadClass

从源码分析:

  Class.forName源码截取:(必须完成初始化)

JVM

  classLoader.loadClass()源码截取:(不完成解析,也就不完成初始化)

JVM

分析:mysql获取链接的步骤

JVM

JVM

 

从mysql链接步骤分析,DriverManager有一个静态代码块,用来完成Driver的注册,而为类的静态成员赋值,执行静态代码块发生在类加载的初始化阶段,再从forName()方法和loadClass()方法分析,可以看出,forName()有一个initialize的参数为true,说明必须完成初始化,而loadClass()有一个resolve参数为false,说明不完成解析,也就不完成初始化。由此加载mysql驱动用forName()。

 

五、如何打破双亲委派模型机制

打破双亲委派模型机制:只要不向上委托即为打破。

场景:Tomcat的webapps目录下可以部署多个war包,每个war包都代表一个独立的web应用,web应用又同名类,但是具体实现都不一样,则需要打破,加载同名的2个不同实现的类。

如何打破:为每个Web应用创建一个WebAppClassLoader,重写loadClass,优先加载当前应用目录下的类,如果找不到,再向上委派。

Tomcat类加载器一览

JVM

 

 

 

六、什么是JIT

JIT:just in time,即时编译编译器,把翻译过的机器码保存起来,以备下次使用,能够加速 Java 程序的执行速度。

工作原理:

 JVM

热点代码:

  被多次调用的方法

  被多次执行的循环体

热点判断:计数器热点探测,-XX: CompileThreshold 阈值次数。(Client模式下默认1500次,Server下默认10000次)

  方法调用计数器:统计方法调用次数

   回边计数器:统计循环体调用次数

过程:JVM默认情况下对于即时编译请求在编译完成之前,都按照解释方式执行,编译动作在后台线程执行(-XX:-BackgroundCompilation禁止后台编译,此时编译请求会等待,直到编译完成后直接执行本地代码)

 

 

 

 七、JVM运行时数据区域

线程私有:

  程序计数器:用于存储当前线程所执行的字节码的行号指示器。

  虚拟机栈:JVM栈中存放着栈帧,用于执行Java方法,一个方法一个栈帧。

  本地方法栈:本地方法栈存放着栈帧,用于执行本地方法(native关键字修饰的方法)。

线程共享:

  方法区:存放已被JVM加载的class信息、常量、静态变量、JIT编译后的代码缓存。

    也称为非堆(Non-Heap)

    在Hotspot虚拟机实现中也称为永久代,java8后Hotspot使用元空间代替了永久代。(永久代、元空间只是虚拟机实现方法区的方式)

  运行时常量池:方法区的一部分,存放编译期生成的字面量和符号引用,运行期间也可以动态添加。

  堆:存放对象实例。GC的主要活动区域。

  其他:

    直接内存:不属于JVM运行时数据区的一部分,不受堆大小的限制,自然也不会触发GC,需要程序员自己管理内存。(Unsafe类、NIO的DirectByteBuffer类)

 

 JVM

 

 

 

 

 八、对象的分配方式

分配方式1(堆内存)-指针碰撞:

JVM

 

分配方式2(堆内存)-空闲列表

JVM

 

 

分配方式3(堆内存)-TLAB(Thread Local Allocation Buffer,本地线程分配缓冲)

 

背景:多个线程同时申请内存,那么势必会出现线程安全问题

说明:在线程初始化时,同时也会申请一块指定大小的堆内存,只给当前线程使用,不存在竞争的情况。(大对象除外)

如何开启(默认开启): -XX:+UseTLAB

 

分配方式4(虚拟机栈内存)-栈上分配

背景:分配到堆内存的目的,是为了对象在线程间共享,如果此对象不会在线程间共享,只局限于当前方法,可以考虑不分配在栈内存上,而是分配在栈上。

 

 

说明:针对作用域不会逃逸出方法的对象(非大对象),在分配内存时不在将对象分配在堆内存中,而是将对象属性打散后分配在栈上。

 

 

优点:

  1.伴随方法执行结束(栈帧出栈)而销毁,减轻GC负担

  2.分配速度快,提高性能

 

 

依赖技术(编译期间):逃逸分析 + 标量替换

  逃逸分析:逃逸指的是逃出当前线程,判断对象是否会逃出当前方法体。

  标量替换:标量是指不可分割的量(例:Java的基本数据类型),替换是指把对象的成员变量的访问替换为基本数据类型

 

 

九、什么是mark word?

对象自身运行时数据,大小为32位或者64位。

一种动态的数据结构,根据当前对象状态的不同(标示位不同),存储的内容不同。

JVM

 

 

十、JVM方法区会回收垃圾吗?

 方法区存放的是已被JVM加载过的类信息(完整类名,修饰符 public、final等)、静态常量、JIT编译后的代码缓存。

 针对废弃的常量,可以垃圾回收。 针对于不再使用的class,可以垃圾回收,回收的条件非常苛刻,可以使用-Xnoclassgc关闭。

 

 

十一、可达性分析算法中哪些对象可以是GC Roots对象?

 1.虚拟机栈-栈帧中形式变量引用的对象。

 2.本地方法栈-JNI栈帧中形式变量引用的对象。

 3.方法区-类静态属性引用的对象。

 4.堆-被同步锁持有(synchronized关键字)的对象。

 5.虚拟机内部的引用-基本数据类型对应的Class对象,系统类加载器,常驻的异常对象。

 6.其他

 

 

 

  

十二、GC算法有几种?

1.标记-清除:Mark-Sweep,先标记,再清除。

 优点:简单

 缺点:有内存碎片

JVM

 

2.复制:Copying,将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,GC时将存活的对象复制到另一块内存。

 优点:无内存碎片

 缺点:内存空间浪费、效率低

 JVM

 

3.标记整理:Mark-Compact,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

 优点:无内存碎片、不浪费内存空间

 缺点:效率低

 

算法对比:

JVM

 

十三、GC分代假说

堆内存划分为新生代和老年代,新对象直接存储在“新生代”上,大对象直接进入老年代,若对象熬过N轮GC仍然存活,移动到“老年代”上。

分代的好处:针对不同特点的对象,采用不同的回收策略

  老年代都是难以消亡的对象,可以较为低频的扫描;新生代都是“短命”的对象,需要较为高频的扫描。

  新生代适合使用“标记-复制”算法(剩下的对象少,复制的代价小),老年代适合使用“标记-清除”算法(剩下的对象多,复制代价大,适合直接清除)

 

分代收集的概念:

  新生代收集:Young GC/Minor GC

  年代老收集:Old GC/Major GC

  整堆收集:Full GC

 

新生代的划分:Eden区、Survivor from区、Survivor to区(为了标记-复制算法)

新生代通用的标记-复制算法(Serial、ParNew、 Parallel Scavenge)过程:

  1.新对象,分配到eden(大对象到老年代)

  2.对eden与From进行GC,存活的放入To

  3.原先的From变成了To,原先的To变成了From

  4.如此反复

  5.当对象年龄到达15岁,移动到老年代

如果To区装不下存活的对象,根据担保机制,这些对象会直接进入老年代

 

十四、跨代引用会发生什么问题?怎么解决?

跨代引用造成的问题:

  例1:进行Young GC的时候,若老年代引用了年轻代,此时年轻代的对象不能被错误回收。

  例2:进行Old GC的时候,若新年代引用了老年代,此时老年代的对象不能被错误回收。

解决方案:(主流垃圾收集器均采用此种方法)

  记忆集(RSet),一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。

  利用记忆集,在GC-Roots枚举时,将跨代引用的对象加入GC Roots进行扫描,从而避免了扫描整个年轻代/老年代。

 

十五、CMS垃圾收集器

CMS: Concurrent Mark Sweep,获取最短回收停顿时间为目标的收集器。

垃圾识别算法:可达性分析法

垃圾清理算法:

  多数时间:标记-清除

  少数时间:标记-整理(碎片过多无法分配对象时,则使用Serial-Old兜底)

垃圾回收过程:

  初始标记:stop-the-world,标记GCRoots直接关联的对象

  并发标记:并发追溯标记,程序不会停顿

  重新标记: stop-the-world,修正并发标记期间导致的标记变动 (使用了增量更新解决“错标”问题)

  并发清理:清理垃圾对象,程序不会停顿

  并发重置:重置CMS收集器的数据结构

优点:低停顿、并发收集

缺点:

  1.对CPU资源敏感,会和工作线程争抢

  2.无法处理浮动垃圾(并发标记和并发清理阶段新产生的垃圾),只能等到下一次GC清理

  3.会产生内存碎片

  4.执行过程中的不确定性—Concurrent mode failure):Serial Old登场,触发STW

    GC过程中,Survivor区的对象要晋升,老年代存不下(Promotion failed)

    GC过程中,大对象直接进入老年代,老年代存不下

    适当降低触发CMS的阀值: -XX:CMSInitiatingOccupancyFraction

 

场景:B/S结构下的老年代收集器的首选,JavaWeb最常使用,配合新生代的ParNew

 

并发的可达性标记可能出现什么问题?怎么解决?

可能产生的问题:

  “漏标”-该清理的标记为不清理:可以接受,是“浮动垃圾”,下次再清理。

  “错标”-不该清理的标记为该清理:不可以接受,这个是Bug,会导致应用程序NPE(空指针异常)。

JVM

 

 

解决办法:

   增量更新(CMS的选择):

    记录1 并发标记阶段:把新插入的引用记录下来[写屏障](记录了A[黑] -> D[白])

    重新标记阶段:再以黑色对象为根重新扫描一次(从A重新扫描,D变黑)

   原始快照(G1的选择):

    记录2 并发标记阶段:把删除的引用记录下来[写屏障](记录了C(灰) ->D(白))

    重写标记阶段:把白色对象标记为黑色(把D标记为黑色)

二种解决办法优缺点:

  增量更新:速度慢,准确 (CMS选择增量更新是因为CMS无法容忍碎片过多,需要准确性)

  原始快照:速度快,不准确

    

 

十六、G1垃圾收集器 

G1:Garbage First,垃圾回收的里程碑,一种以获取最短回收停顿时间为目标的收集器。局部收集的设计思路,基于Regin的内存分布形式, JDK7首发,JDK9的默认垃圾收集器,目标是取代CMS。

垃圾识别算法:可达性分析法

垃圾清理算法:复制算法(从一个regin复制到另一个regin)

内存分布形式:(连续的regin块)

JVM

 

比CMS优秀在哪里?

  垃圾碎片少:G1采用复制算法,CMS采用标记-清除

 

  使用范围更广:G1可以用于新生代+老年代,CMS用于老年代

  STW更短、更加可控:

    部分收集:优先回收性价比最高的区域,不是整体都回收

    可预测垃圾回收的停顿时间(-XX:MaxGCPauseMillis=200)

    重新标记阶段采用原始快照模式

    大对象(大于Region大小的50%)单独存储,不占用老年代

  内存使用率更高: 每一个Regin的类型不是固定不变的,可以动态调整

 

G1中的GC:

  Young GC:Eden区满则触发,同时回收大对象垃圾

    eden到surivor的复制;

    新生代到老年代的晋升和之前一样 跨代引用问题使用记忆集

  Mix GC:整堆空间占一定比例则触发( -XX:InitatingHeapOccupancyPercent=45)

  Full GC:Mixed GC时更不上应用分配对象内存的速度,导致老年代满,降级为Serial Old

 

 

场景:B/S架构下服务端,CMS解决不了的场景

垃圾回收过程:

  初始标记:只标记GC  Roots能直接关联到的对象

  并发标记:进行GC Roots Trancing的过程

  最终标记:修正并发标记期间,因程序员运行导致标记发生变化的那一部分对象 (使用原始快照解决“错标”问题)

  筛选回收:根据时间来进行价值最大化的回收

 

为什么G1选择原始快照来解决“错标”?

  CMS使用标记-清除算法,由于空间碎片可能会导致STW问题,要尽量避免浮动垃圾;G1采用复制算法,浮动垃圾更能接受。

  G1追求更短的STW时间,原始快照的速度更快。  

 

上一篇:ZGC


下一篇:避免使用双for