JVM之问

一、类加载机制

1、类的生命周期
加载-连接(验证-准备-解析)-初始化-使用-卸载。其中加载、验证、准备、初始化、卸载顺序一致,但解析可能在初始化后,目的:为了支持运行时绑定特性。
2、加载时机
有且只有:
1、遇到new、putstatic、getstatic、invokestatic指令时,有如下场景:new对象,读取和设置静态字段(对于静态字段,只有直接定义的类才会被初始化),调用静态方法。
2、使用java.lang.reflect包的方法对类型进行反射调用时。
3、初始化类时,如果其父类还没有初始化,则初始化其父类。
4、虚拟机启动时主类
5、调用java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic,REF_putstatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的句柄,并且此类没有初始化过。
6、存在default方法的接口的实现类被初始化时,此接口要被初始化(没有定义default方法的接口,只要被使用到其方法时才会被初始化)。
3、加载的流程
通过一个类的全限定类名来获取定义此类的二进制字节流(可以从不同的地方获取,比如网络,zip压缩包)。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
4、数组的加载
如果数组的组件类型(去掉一个维度的类型)是引用类型,加载这个引用类型,数组C将会被标识在加载该组件类型的类加载器的类名称空间上。
如果不是引用类型,JVM把数组C与引导类加载器关联。
数组类的可访问类型与组件类型一致,如果不是引用类型,默认为public。
5、验证
保证Class文件的字节流中包含的信息是安全的。
6、会验证哪些东西
文件格式(如:是否以魔数开头)
元数据验证(如:是否有父类)
字节码验证
符号验证
7、准备的作用
为类中的静态变量分配内存(逻辑上在方法区)并赋初值(一般为0初值,如果是final变量,则为final定义的,因为在编译时,ConstantValue已经有值了)。
8、解析的作用
符号引用被替换为直接引用。
对字段和方法的可访问性进行检查。
要求在anewarray,checkcast,getfield,getstatic,instanceof,invokedynaimic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,ldc2_w,multianewarray,new,putfield,putstatic指令之前解析。
除了invokedynamic指令外,都应该是幂等的(第一次成功,后续一直成功,第一次失败,后续失败。)
9、类或接口的解析
假设当前代码为D,符号引用N,类或接口的直接引用C。
如果C不为数组,那虚拟机会把代表N的全限定类名传递给D的类加载器去加载这个类C。在加载过程中,可能触发其他类的加载动作,一旦有任何异常,就宣告失败。
若C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会使类似“[Ljava/lang/Integer”的类型,那将会按照第一点的规则加载数组类型元素。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个 代表该数组维度和元素的数组对象。
若上面没有异常,则进行符号引用验证,确认D对C的访问权限。
10、字段解析
如果C本身就包含了简单名称和字段描述符都与目标匹配的字段,返回直接引用。
否则,如果C实现了接口,从下往上递归搜索,看各个接口与父接口是否有简单名称和字段描述符都匹配的字段。
否则,如果C不是Object,从下往上递归搜索,看父类是否存在。
否则,抛出java.lang.NoSuchFieldError异常。
11、方法解析
如果方法表中class_index中索引的C是个接口,抛出java.lang.IncompatibleClassChangeError
如果C本身就就包含了简单名称和字段描述符都与目标匹配的方法,返回
否则查找父类
否则查找接口
否则,抛出,java.lang.NoSuchMethodError
12、接口方法解析
如果方法表中的class_index中索引的C是个类,抛出java.lang.IncompatibleClassChangeError
否则,在接口C中查找
否则,查询父接口,若有多个,返回其中一个
否则,抛出java.lang.NoSuchMethodError
13、什么是类初始化?
执行类构造器方法的过程
14、什么是clinit方法
由编译器自动收集类中的类变量与static语句块中的语句合并产生的,收集顺序是由编写代码的顺序决定的。
15、编写在static语句后的静态变量,可否访问和赋值?
不行,只能赋值,不能访问。
16、是否需要显式调用父类构造器?
不用,JVM保证
17、父类中定义的静态语句块是否先于子类变量赋值操作?

18、是否必要?
不必要,如果没有静态语句块和对变量的赋值操作,编译器不会生成。
19、执行接口的是否需要执行父类的方法?
不需要,用到父接口中定义的才需要
20、什么是类初始化锁?
保证执行一次
20、类加载器的作用?
加载类,与类本身确定类的唯一性。
21、如果两个类是由同一个.class文件生成,但类加载器不同,这两个类是同一个类?
不是
22、有哪些类加载器?
JDK1.8以前:启动类加载器(由C++编写,加载JAVA_HOME/lib目录下,或者被-Xbootclasspath参数指定的目录,且是能够被Java虚拟机识别的),扩展类加载器(加载JAVA_HOME\lib\ext目录下,或java.ext.dirs系统变量中指定路径的类库,JDK9后被平台类加载器取代),应用程序类加载器(加载用户类路径上所有类库)
23、什么是双亲委派模型?
除启动类加载器外,其他所有类加载器都以组合的方式复用父加载器代码,当有类需要加载时,会传递给父加载器加载,如果启动类加载器无法加载,才依次往下让子类加载器加载。
24、双亲委派模型有什么好处?
Java中的类随加载他的类加载器具备了一种带有优先级的关系?
保证了程序的安全性。比如我们重新写了一个String类,加载的时候并不会去加载到我们自己写的String类,因为当请求上到最高层的时候,启动类加载器发现自己能够加载String类,因此就不会加载到我们自己写的String类了。
25、双亲委派模型的破坏有哪几次?
对于JDK2以前的代码做出的妥协。
SPI机制,引入了线程上线文加载器
追求程序的动态性,如热部署
JDK9以后的模块化,加载类时需要判断是否可以归属到某个系统模块中,由那个模块的加载器加载
26、SPI机制,除线程上下文加载器有无其他方法?
有JDK6时,引入了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式
27、收到类加载请求时,OSGI搜索类的流程?
将java.*开头的类,委派给父类加载器加载
否则,将委派列表名单中的类,委派给父类加载器
否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载
否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
否则,判断类是否则Fragment Bundle中,如果在,委派给他的类加载器加载
否则。查找Dynamic Import列表的Bundle,委派给对应的。
否则,失败
29、什么是模块化?
可配置的封装隔离机制
30、可配置的封装隔离机制有何作用?
解决JDK9之前基于类路径来查找依赖的可靠性问题。JDK9以前,如果类路径中缺失了运行时依赖的类型,那么必须到运行到发生该类型的加载、链接时才会出现异常。在JDK9以后,如果启用了模块化进行封装,模块就可以声明对其他模块的显示依赖,这样就可以在启动时就查看到依赖是否完备。
31、什么是模块路径?
某个类库到底是模块还是jar包,取决于他放在哪个路径上
32、为什么有模块路径?
兼容传统的类路径查找机制

运行时内存区域篇

1、JVM有运行时内存区域的划分?
分为程序计数器,虚拟机栈,本地方法栈,堆,方法区,运行时常量池。
2、什么是程序计数器?程序计数器的作用?生命周期?
程序计数器是一块较小的空间,可以看作是当前线程所执行的字节码的行号指示器。对于Java方法来说,程序计数器记录的是正在执行的虚拟机字节码指令的地址,对于本地方法则为空。
程序计数器是线程私有的
对于程序计数器来说,是不会有OOM异常的
3、什么是虚拟机栈?有什么用?生命周期?
虚拟机栈描述的是Java方法执行的线程内存模型,每个方法执行时都会在里面创建一个栈帧,栈帧当中存放有局部变量表,操作数栈,动态链接,方法返回地址等信息。
线程私有。
4、什么是局部变量表?
局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。
5、局部变量表的大小是否可以动态改变?
不行,在Java被编译为.class时,就在方法的Code属性max_locals数据项中确定了最大容量。
6、局部变量表的基本单位?
槽。
7、如何让64位的slot看起来和32位一样?
使用对其和补白等手段。
8、一个slot可以存放多大的数据类型?
32位以内。
9、引用类型是否在JVM规范中被固定了大小?
没有。只需要它至少满足1、根据引用直接或间接的查找到对象在Java堆中的数据存放的起始地址或索引,2、根据引用直接或间接查找到对象所属数据类型在方法区中的存储的类型信息。
10、HotSpot对象创建流程?
当JVM一到一条字节码new指令,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有执行类加载流程。
在类加载检查通过后,会为这个新对象分配内存。分配内存有两种方法,一种是空闲列表,一种是指针碰撞。至于使用哪种分配算法,则有Java堆是否规整决定,Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理的能力来决定。此外,在这个过程中还需要考虑同步的问题,解决同步问题有两种方案,一种是对内存分配的动作直接进行同步;第二种是使用TLAB。
在内存分配完毕后需要对内存初始化为零(使用TLAB的话可以考虑在TLAB分配时进行)。
接下来,JVM还会对这个对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。
最后会执行构造方法。
11、JVM中对象结构
大致分为,对象头,实例数据和对齐填充。
12、对象头结构?
MarkWord和类型指针
32位
32位MarkWord
64位https://img-blog.csdnimg.cn/20200802171848152.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk4NTM0,size_16,color_FFFFFF,t_70
类型指针的作用是,通过这个指针来确定这个对象是哪个类的实例。
13、如何定位一个对象
句柄访问和直接指针访问。
直接指针访问的好处是少一次指针定位的时间开销成本。
14、操作数栈是什么?
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。在方法执行过程中,需要把数据和指令放入数栈中,也就是出栈和入栈。
15、什么是动态链接?
字节码中的方法调用指令以常量池里指向方法的符号引用作为参数,在每一次运行期间都转化为直接引用的符号引用叫动态链接。
16、方法有哪两种退出方式
遇到方法返回的字节码指令,或者遇到了异常。
17、方法退出可能会做哪些事?
恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
18、Java中有哪些方法适合在类加载时解析?为什么?
静态方法和私有方法。因为他们在编译器可知,运行期不变。
19、Java有哪些调用方法的指令?

  • invokestatic:调用静态方法
  • invokespecial,调用实例构造器< init >()方法、私有方法和父类中的方法
  • invokevirtual,调用所有虚方法
  • invokeinterface,调用接口方法,会在运行时确定一个实现该接口的对象。
  • invokedynamic,先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。前面四条指令,分配逻辑都在JVM内部,本条指令分派逻辑由用户设定的引导方法来决定。

20、哪些方法是非虚方法?
静态方法,父类方法,构造方法,私有方法,final方法(使用invokevirtual指令调用)

21、什么是静态分派?什么是动态分派?
依据静态类型来决定方法执行版本的分派动作((非int)基本数据类型 > int > long > 包装类 > 实现接口 > object)。
依据实际类型确定方法执行版本的分派动作叫动态分派。

22、invokevirtual指令的解析步骤?
1)、找到操作数栈顶的第一个元素所指向的对象的实际类型,记为C。
2)、如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限,如果通过则返回这个方法的直接引用,不通过则返回java.lang.IllegalAccessError异常。
3)、否则,按第二步搜索父类.
4)、如果都没有找到,返回java.lang,AbstractMethodError异常。
23、多态中的两个特性到底是什么分派?
重载是静态多分派
重写是动态单分派
24、JVM是怎么实现动态分派的?
虚方法表或者类型继承关系分析或者守护内联、内联缓存等多种非稳定的激进优化。
25、JVM的指令集是基于栈还是基于寄存器?
基于栈
26、基于栈的指令集优点是什么?缺点呢?
- 优点:可以移植
- 缺点:执行速度会慢一些

GC

1、什么是可达性分析?为什么要有可达性分析?
可达性分析指的是从GCRoots开始,根据引用关系向下遍历,如果某个对象到GCRoots是可达的,称为可达。
可达性分析是为了解决引用计数法所不能解决的互相引用问题,比如A引用B,B又引用了A,这样就无法解决。
2、哪些可以作为GCRoots?
在虚拟机栈中引用的对象,比如局部变量,临时变量等。
在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
在方法区中引用的对象,比如字符串常量池中的引用。
在本地方法栈中JNI引用的对象。
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,还有系统类加载器。
所有被同步锁持有的对象。
反应JVM内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等。
3、引用有哪些类型
强引用
软引用
若引用
虚引用
4、什么时候回进行垃圾回收?
System.gc():发出一个信号,告知JVM需要垃圾回收了,具体啥时候回收,由JVM决定,不建议手动调用这个方法,因为垃圾回收会占用CPU资源,回收次数越少越好。
Eden不够用了(新生代的GC 称为Young GC 或 Minor GC)
老年代不够用了
Metaspace(方法区)不够用了(Metaspace GC)

5、怎么才能判断一个对象已死
一个对象被判断为死会经历两次标记,第一次标记是对象到GCRoots不可达,此时JVM会把对象放到F-Queue的队列中,如果下一次扫描时,这个对象没有与某个GCRoots建立连接,就会被JVM所回收,如果建立了连接,就会被移出队列。
6、针对方法区,主要回收哪些内容?
废弃的常量和不再使用的类型
7、分代收集理论假说包含哪三条?
弱分代假说:绝大多数对象都是朝升夕灭的。
强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
跨代引用假说:跨代引用相对于同代引用来说仅占少数。
8、什么是Remembered Set
这是一种数据结构,这种数据结构把老年代划分成若干个小块,标识出老年代的哪一块内存会存在跨代引用,是典型的空间换时间(非收集区域指向收集区域的指针集合)。
9、什么是Minor GC/Young GC,Major GC/Old GC,Mixed GC
MinorGC/YoungGC:指目标是新生代的垃圾收集。
Major GC/Old GC:指目标是老年代的垃圾收集。
Mixed GC:指目标是收集整个新生代以及部分老年代的垃圾收集,现在只有G1收集器会有这种行为。
10、有哪些常见的垃圾收集算法
标记-清理
标记-复制
标记-整理
11、什么是OopMap?对垃圾回收有什么好处?
通过OopMap,JVM可以有办法直接得到哪些地方存放着对象引用。
OopMap记录了栈到堆的引用关系,其作用是:垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 Reference 类型。如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收。但问题是,栈上的本地变量表里面只有一部分数据是 Reference 类型的(它们是我们所需要的),那些非 Reference 类型的数据对我们而言毫无用处,但我们还是不得不对整个栈全部扫描一遍,这是对时间和资源的一种浪费。一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 gc 的时候就可以直接读取,而不用再一点一点的扫描了。
12、什么是safepoint(安全点)?
可达性分析算法必须是在一个确保一致性的内存快照中进行。如果在分析的过程中对象引用关系还在不断变化,分析结果的准确性就不能保证。
安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC。
13、如何选定safepoint?为什么?
1、循环的末尾
2、方法临返回前
3、调用方法之后
4、抛异常的位置
主要的目的就是避免程序长时间无法进入 Safe Point。比如 JVM 在做 GC 之前要等所有的应用线程进入安全点,如果有一个线程一直没有进入安全点,就会导致 GC 时 JVM 停顿时间延长。比如这里,超大的循环导致执行 GC 等待时间过长。
14、如何在 GC 发生时,所有线程都跑到最近的 Safe Point 上再停下来?
抢先式中断和主动式中断
抢先式中断不需要线程的执行代码去主动配合,在垃圾收集发生时,系统首先把所有用户线程中断,如果发现有线程没有到安全点上,就恢复这条线程,让他到达安全点。
主动式中断是在垃圾收集发生需要中断线程时,不直接对线程操作,仅仅简单的设置一个标志位,让线程不停的去轮询这个标志位,一旦这个标志位为真时,就在离自己最近的安全点上主动中断挂机,轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方。
15、什么是安全区域?
如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。因此 JVM 引入了 Safe Region。Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。
16、卡表有集中记录精度?
字长精度:精确到一个机器字长(就是处理器寻址位数,比如32位或64位)
对象精度:精确到一个对象
卡精度:精确到一块内存区域
17、在HotSpot中一个卡页是多大?
2的9次方,即512字节(一般来讲卡页大小是2的N次幂)
18、卡表的工作方式?
只要卡页内有一个或多个对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称这个元素变脏,在垃圾收集时,只要筛选出卡表中变脏的元素就能得出哪些卡页内存块中包含跨代指针,把他们加入GCRoots。
19、什么是写屏障
这里的写屏障不是并发当中的写屏障,这里是一种类似于AOP的东西,在引用对象赋值时会产生一个环形通知,供程序执行额外的动作。
之所以要引入这个东西,是因为如果一段代码已经被编译成了二进制后,又发生了对象赋值操作,此时JVM无法更新卡表,所以引入了写屏障来通知JVM维护卡表。
在赋值前的部分写屏障叫写前屏障,在赋值后的叫写后屏障。
20、三色标记法是什么
三色标记法是一种垃圾回收算法,它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的。
21、什么是对象消失问题
一种是把原本消亡的对象错误的标记为存活,这不是好事,但是其实是可以容忍的,只不过产生了一点逃过本次回收的浮动垃圾而已,下次清理就可以。
一种是把原本存活的对象错误的标记为已消亡,这就是非常严重的后果了,一个程序还需要使用的对象被回收了,那程序肯定会因此发生错误。
22、对象消失哪种情况会发生
赋值器插入了一条或多条从黑色对象到白色对象的新引用
赋值器删除了全部从灰色对象到白色对象的直接或间接引用
条件一和条件二是有先后顺序的,即必须是赋值器插入了一条或者多条从黑色对象到白色对象的新引用,然后赋值器又删除了全部从灰色对象到该白色对象的直接或间接引用。在这样的情况下,才会出现“对象消失”的情况。
23、如何解决对象消失问题?
第一种是增量更新
第二种是原始快照(SATB)
原始快照:当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描 一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
这样做的实际效果是:如果一个灰对象的字段原本指向一个白对象,但在concurrent marker能扫描到这个字段之前,这个字段被赋上了别的值(例如说null),那么这个字段跟白对象之间的关联就被切断了。SATB write barrier保证在这种切断发生之前就把字段原本引用的对象变灰,从而杜绝了上述条件二的发生。
其中:“把旧的引用所指向的对象都变成非白的。”在我们这个场景下含义如下:
旧的引用指的是:灰色对象6到白色对象9之间的引用。
所指向的对象指的是:白色对象9。
都变成非白的:指的是白色对象9变成了灰色。

SATB原始快照
增量更新:增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。
24、垃圾收集器的配合 在这里插入图片描述
25、说说Serial、ParNew、Parallel Scavenge垃圾收集器
Serial收集器是最基础、历史最悠久的收集器到,这个收集器是一个单线程工作的收集器,但它的“单线 程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强 调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数。除了Serial收集器外,目前只有它能与CMS 收集器配合工作。ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分之百保证超Serial收集器。 当然,随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时 系统资源的高效利用还是很有好处的。它默认开启的收集线程数与处理器核心数量相同,在处理器核 心非常多(譬如32个,现在CPU都是多核加超线程设计,服务器达到或超过32个逻辑核心的情况非常 普遍)的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是 能够并行收集的多线程收集器,Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能 地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值, 即:在这里插入图片描述
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
-XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的 时间不超过用户设定值。不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得 系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的: 系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得 更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间 的确在下降,但吞吐量也降下来了。
-XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的 比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5% (即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
除上述两个 参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得我们关注。这是一 个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区 的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数 了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时 间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略
25、说说Serial Old和Parallel Old垃圾收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收 集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用 途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用[1],另外一种就是作为CMS 收集器发生失败时的后备预案。
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
26、CMS收集器的步骤?
1)、初始标记:标记一下GCRoots直接关联到的对象。需要Stop The World,速度很快。
2)、并发标记:从GCRoots的直接对象开始遍历整个对象图,耗时很长,但不需要STW。
3)、重新标记:了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(增量更新)
4)、并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
27、CMS的优缺点?
优点:低停顿,并发收集。
缺点:对处理器资源很敏感。

在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程
(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。CMS默认
启动的回收线程数是(处理器核心数量 +3)/4,也就是说,如果处理器核心
数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,
并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时, 
CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,
还要分出一半的运算能 力去执行收集器线程,就可能导致用户程序的执行速度
忽然大幅降低。为了缓解这种情况,虚拟机提 供了一种称为“增量式并发收集
器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,
所做的事情和以前单核处理器年代PC机操作系统靠抢占式多任务来模拟多核并
行多任务的思想一样, 是在并发标记、清理的时候让收集器线程、用户线程交
替运行,尽量减少垃圾收集线程的独占资源的时间,这样整个垃圾收集的过程
会更长,但对用户程序的影响就会显得较少一些,直观感受是速度变慢的时间更
多了,但速度下降幅度就没有那么明显。实践证明增量式的CMS收集器效果很一
般,从JDK 7开始,i-CMS模式已经被声明为“deprecated”,即已过时不再
提倡用户使用,到JDK 9发布后i- CMS模式被完全废弃。

无法处理浮动垃圾

由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间
提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待 到老年代几
乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使
用。在JDK5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激
活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以
适当调高参数-XX:CMSInitiatingOccu-pancyFraction的值 来提高CMS
的触发百分比,降低内存回收频率,获取更好的性能。到了JDK6时,CMS收集
器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是
CMS运行期间预留的内存无法满 足程序分配新对象的需要,就会出现一次“并发
失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预
案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的
垃圾收集, 但这样停顿时间就很长了。
所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易
导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用
情况来权衡设置。

CMS是一款基于“标记-清除”算法实现的收集器,如果 读者对前面这部分介绍还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。空间 碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找 到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
28、说说G1收集器是怎么划分内存区域的
对于G1收集器来说,并没有严格规定哪一块内存区域是新生代,或者是老年代,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要来扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的 Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的 旧对象都能获取很好的收集效果。除此之外它还有一类特殊的区域叫做 Humongous,专门用来存储大对象(超过Region大小的一半就被认为是大对象)。Region的大小在1-32MB之间,且为2的N次幂,默认有2048个Region
29、G1为什么可以建立起可预测的时间停顿模型?
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作 为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免 在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默 认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。 这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
具体来说,为了建立起可靠的预测时间停顿模型,G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
30、将Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决?
使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应用其实要复杂很多,它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一 种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这 种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更 复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃 圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额 外内存来维持收集器工作。
在这里插入图片描述
可以理解成”Hashtable<key,int[]>“结构,其中key对应的就是其他Region,而int[]对应其他Region的卡表。如上图Region2的RSet第一个元素对应Region,int[]中第二个与第四个元素值是1表示”脏“数据,也就是说Region1中的第2块与第4块区域存在对Region的引用,同理也可以得出Region3的第1块与第4块存在对Region2的引用。
31、G1在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
使用原始快照(SATB)。
此外,垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在 这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。与CMS中 的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度, G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。
具体来说是这样的:
在这里插入图片描述
32、G1垃圾收的步骤
· 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。
· 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
· 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。
· 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。

33、什么是ZGC?
ZGC垃圾收收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

34、ZGC的Region?
ZGC的Region具有动态性,可以动态创建和销毁,以及动态的区域容量大小。
在x64硬件平台下,Region具有以下容量:

  • 小型Region:容量固定为2MB,用于放置小于256kb的对象。
  • 中型Region:容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
  • 大型Region:容量不固定,可以动态变化,但必须放置为2MB的整数倍,用于放置4MB或以上的大对象。每个Region只放一个大对象,也就是说他的大小可以小于中型Region,大型Region在ZGC实现中不会被重新分配,因为复制一个大对象的代价很高昂。

35、什么是染色指针?
染色指针指的是把一些对象的信息标记在引用对象的指针上,这些信息包括了其引用的三色标记状态、是否进入了重分配集,是否只能通过finalize()方法访问到,但是由于这些信息是被标识在寻址指针的高4位上(linux下64位指针,只支持46位)所以,对于ZGC可管理的内存来说,最高只有2 ^ 42次方,也就是4TB。
在这里插入图片描述

36、ZGC有什么好处?

  • 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用被修正后才能清理。
  • 染色指针可以大幅度减少在垃圾收集器过程中内存屏障的使用,到目前为止,ZGC都没有使用写屏障,只使用了读屏障,因为ZGC不支持分代收集,不存在跨代引用。
  • 染色指针可以作为一种扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能(比如linux下64位平台前18位并没有使用,他们虽然不能用来寻址,但是可以通过其他手段用于信息记录)
  • 支持“NUMA-Aware”的内存分配。所谓的NUMA-Aware指的是非统一内存访问架构,多个处理器合在一起,某个处理器需要访问其他处理器管理的内存,就会变慢,因此在NUMA架构下,ZGC会优先尝试在请求线程所处的处理器中的本地内存上分配对象。

37、对于处理器来说,是别的都是二进制流,那么ZGC是怎么让处理器知道哪部份是真正的寻址地址,哪部份是记录的数据呢?
ZGC使用多重映射的方式来实现的,染色指针前四位会有四种状态,因此JVM包括堆内存在内申请了四块内存,其余三块被命名为,Remapped,Marked1,Marked0,这四块虚拟内存共同指向了一个物理地址,因此无论状态位怎么变化,都可以正常寻址。

38、ZGC的工作流程?

  • 初始标记:标记一下能够与GCRoots直接关联到的对象。
  • 并发标记:对整个对象图做可达性分析,更新染色指针中的Marked0、Marked1标志位。
  • 并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理哪些Region,这些Region将会组成分配集。与G1不同的是,ZGC划分Region并非是为了做收益优先的增量回收,相反,ZGC会扫描所有Region,这样就不需要维护一个容量巨大的记忆集了。ZGC的重分配只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放,而不能说回收行为就是针对这个集合里面的Region。JDK12后的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段完成的。
  • 并发重分配:这个过程把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。由于使用染色指针,如果用户在此时并发的访问了位于重分配集中的对象,这次访问将会被预置的内存屏障截取,转发到新的对象上,并修改引用的值,这就是自愈。这样的好处是只会有一次转发,只慢一次。一旦重分配集的某个Region对象复制完毕后,这个Region就可以被释放用于新对象的分配(转发表不能释放)。
  • 并发重映射:修正整个堆中指向重分配集中旧对象的引用,由于这个工作并没很迫切(但是有可以不变慢和清理掉转发表的好处),ZGC很巧妙的把重映射阶段要做的工作与下一次垃圾收集器中并发标记阶段合在一起,因此都要遍历对象图。

39、什么是Epsilon收集器?
Epsilon(A No-Op Garbage Collector)垃圾回收器控制内存分配,但是不执行任何垃圾回收工作。一旦java的堆被耗尽,jvm就直接关闭。设计的目的是提供一个完全消极的GC实现,分配有限的内存分配,最大限度降低消费内存占用量和内存吞吐时的延迟时间。一个好的实现是隔离代码变化,不影响其他GC,最小限度的改变其他的JVM代码。可以用在短时间、小规模的服务形式上。

40、JVM内存分配和回收策略

  • 对象优先在Eden分配,当Eden没有足够空间时发起MinorGC
  • 大对象直接进入老年代,通过-XX:PretenureSizeThreshold来设置多少算大对象
  • 长期存活对象将进入老年代,通过-XX:MaxTenuringThreshold来控制阈值
  • 动态对象年龄判定,如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一般,年龄大于等于该年龄的对象可以直接进入老年代,无需等待-XX:MaxTenuringThreshold的值
  • 空间分配担保,在发生Minor GC之前,JVM必须检查老年大最大可用空间是否大于新生代所有对象空间总和,如果小于,JVM会查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败;如果允许,则会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试MinorGC,如果小于或者-XX:HandlePromotionFailure不允许,则进行FullGC。所谓的担保指的是,如果Survivor无法容纳对象,那么可能会被直接送入老年代。

后端编译与优化

1、解释器与编译器各有什么优点?
解释器的优点:

  • 启动速度快
  • 节约内存
  • 作为编译器激进优化后的“逃生门”
    编译器的优点:
  • 执行效率高

2、HotSpot有几个即时编译器?
有三个,分别是C1(客户端编译器),C2(服务端编译器),Graal编译器(目的是代替C2)
3、分层编译有哪些层次?
第0层。程序纯解释执行,并且解释器不开启性能监控功能。
第1层。使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能。
第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。
第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部统计信息。
第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的优化。
以上层次并非固定不变,可以根据运行参数和版本,调整分层的数量。

4、热点代码主要有哪些?

  • 被多次调用的方法
  • 被多次执行的循环体

5、什么是栈上替换
栈上替换指的是方法还在栈上,方法就被替换了。
当我们发现循环很热时,此时循环已经执行了多次,并且此时JIT编译器还没执行完,所以JIT编译的并非是完整的代码,而是此循环已经循环多次后再次进入循环,于是我们需要从原本该方法所执行的层级的栈帧中,把需要的状态找出来,然后迁移到新的优化层级栈帧去。
6、如果判断代码是否为热点代码?
使用热点探测
- 基于采样的热点探测。采用这种方法的虚拟机会周期性地检查各个线程的调用栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是“热点方法”。这样做的好处是简单高效,可以容易地获取方法调用关系(将调用堆栈展开即可),缺点是很难精确的确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
- 基于计数器的热点探测。采用这种方法的虚拟机会为每个方法(代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为是热点方法。这样的好处是较为精确,但是缺点是要为每个方法维护计数器,而且不能直接获取到方法的调用关系。

7、方法调用计数器热度的衰减?
当超过一定时间限度,如果方法的调用次数仍然不足以让他提交给即时编译器编译,那该方法的调用计数器就会被减少一半,这个过程就叫方法调用计数器的衰减,这段时间就叫半衰期。
8、什么是回边计数器?
统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令就叫“回边”。
9、回边调用起的阈值计算公式?

  • 客户端模式下:方法调用计数器阈值(-XX: CompileThreshold) * OSR比率(-XX: OnStackReplacePercentage) / 100,如果都取默认值,回边计数器默认值为13995
  • 服务端模式:方法调用计数器阈值 * (OSR比率 - 解释器监控比率(-XX: InterpreterProfilePercentage) / 100,默认为10700

10、回边指令的执行过程?
当JVM遇到回边指令时,首先判断要执行的代码片段是否已被即时编译,如果有执行编译好的代码。否则把回边计数器加1,然后判断方法调用计数器与回边计数器之和是否超过回边计数器的阈值。当超过阈值的时候,会提交栈上替换的请求,并且把回边计数器的值降低,以便继续在解释器中执行循环,等待编译器输出编译器结果。
11、回边计数器是否有热度衰减?
没有,当回边计数器溢出后,会把方法计数器也调整到溢出状态。
12、在后台编译的过程中,C1编译器会做什么?

  1. 第一阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(HIR)。HIR使用静态单分配(SSA)的形式来代表代码值,这可以使得一些在HIR的构造过程之中和之后进行的优化动作更容易实现。在此之前编译器已经会在字节码上完成一部分基础优化,如方法内联、常量传播等优化将会在字节码被构造成HIR之前完成。
  2. 第二个阶段,一个平台相关的后端从HIR中产生低级中间代码表示(LIR)而在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等,以便让HIR达到更高效的代码表示形式。
  3. 最后的阶段是在平台相关的后端使用线性扫描算法在LIR上分配寄存器,并在LIR上做窥孔(Peephole)优化,然后机器代码。
    在这里插入图片描述

13、服务端编译器会进行哪些优化?
无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序。范围检查消除,空值检查消除,守护内联,分支频率预测

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值