二、JVM
- 其它
1、JVM内存模型。
答:1)栈:用来存储局部变量、操作栈、动态链接、方法出口等,调用方法时执行入栈,方法返回时执行出栈;
2)本地方法栈:保存native方法信息;
3)程序计数器:保存当前线程执行的字节码位置,执行native方法时为空;
4)堆:被线程共享,存放所有的对象实例;
5)方法区(非堆区):用来存放已被虚拟机加载的类信息,常量,静态变量,即时编译优化后的代码。1.7的永久代和1.8的元空间是方法区的实现。
2、类的加载过程。
答:1)加载文件到内存;
2)链接:验证class文件,准备(进行内存分配,static分配内存并设置初始值),解析(将常量池中的符号引用变为直接引用,直接引用可以是指向目标的指针、相对偏移量或一个定位到目标的句柄);
3)初始化:类级别,是程序执行前的准备工作。在这个阶段,静态的(变量、方法,代码块)会被执行。同时会开辟一块存储空间来存放静态的数据。只执行一次。
4)实例化:实例对象,是指创建一个对象的过程。这个过程中会在对重开辟内存,将一些费静态的方法,变量存放在里面。
5)卸载。
3、双亲委派加载机制
答:对于任意一个类,需要由加载它的类加载器和这个类本身来确定其在Java虚拟机中的唯一性。双亲委派是一个类加载器收到加载类的请求时,会把请求递归委派给父类加载器,自顶而下尝试加载。启动类加载器(BootStrap ClassLoader)->扩展类加载器(Extension ClassLoader)->应用类加载器(Application ClassLoader)->自定义类加载器。
4、什么时候要破坏双亲委派。
答:第一次破坏:向前兼容(JDK1.2之前是重写loadClass,后续不提倡);
第二次破坏:加载SPI接口实现类,基础类要调用用户的代码,如:JNDI,JDBC等,通过线程上下文类加载器(Thread Context ClassLoader),JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,违背了双亲委派模型;
第三次破坏:热部署,OSGI,为了实现热插拔,只需要把这个模块连同类加载器一起换掉。但是最终基于JVM,只适合构建单一服务节点的内部应用,无法实现计算节点的线性扩展。
Tomcat为什么要破坏双亲委派机制,因为默认的类加载机制无法加载两个相同类库的不同版本。
Tomcat类加载器也不遵循双亲委派(步骤3、4),但只是自定义的classLoader顺序不同,顶层还是相同的。
1)现在本地缓存缓存查找是否加载过该类;
2)让系统类加载器尝试加载该类,防止一些基础类被web中的类覆盖;
3)web应用类加载器加载;
4)如果加载不到,则委托父类加载器(Common ClassLoader)去加载。
- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp(web应用)访问;
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
5、1.8为什么用Metaspace替换掉PermGen?MetaSpace保存在哪?
答:MetaSpace保存在非堆区,本地内存中。原因是:1)字符串在永久代中容易出现性能问题及内存溢出;2)类的信息及大小难以确定,因而难以确定永久代的大小;3)简化了FullGC的过程,仅当类的加载器不再存活,其对应的MetaSpace才能被回收。
6、什么情况下会发生栈内存溢出?
答:方法执行时创建的栈帧超过了栈的深度,例如:方法递归。栈帧包含:函数的返回地址和参数,临时变量,函数调用上下文。
7、部分jvm参数含义。
答:XX:MaxTenuringThreshold新生代晋升到老年代的最大阈值;XX:CMSInitiatingOccupancyFraction老年代达到的占比会触发CMS垃圾回收;XX:UseCMSInitiatingOccupancyOnly不自动触发CMS GC,而是根据CMSInitiatingOccupancyFraction值来确定是否触发。
8、安全点
-
HotSpot中,安全点位置主要在:
- 方法返回之前
- 调用某个方法之后
- 抛出异常的位置
- 循环的末尾
为什么把这些位置设置为jvm的安全点呢,主要目的就是避免程序长时间无法进入safepoint,比如JVM在做GC之前要等所有的应用线程进入到安全点后VM线程才能分派GC任务 ,如果有线程一直没有进入到安全点,就会导致GC时JVM停顿时间延长。比如写了一个超大的循环导致线程一直没有进入到安全点,GC前停顿了8秒。
- GC
1、新生代(Eden+2*Survivor)
答:新生代的作用是为了让生命周期较短的对象先进行回收,Eden与Survivor比例大致为8:1,一次Minor GC存活的对象会加入Survivor区,如果存活了16岁(默认)或者To Survivor填满后对象会加入老年代,Survivor的作用是减少送到老年代的对象,进而减少Full GC的发生。两个Survivor区是为了避免碎片化。
2、CMS收集器。
答:初始标记(STW,标记GC Roots引用的老年代对象,标记老年代中被新生代引用的对象)->并发标记(从初次标记收集对象的根开始遍历所有被引用的对象)->并发预处理(与重新标记功能类似,目的是为了减少GC停顿)->重新标记(STW,处理并发标记中引用发生变化的更新,避免收集到被引用的对象)->并发清除。
GC roots,被GC roots引用的对象不被GC回收。
- 虚拟机栈中栈桢中的局部变量(也叫局部变量表)中引用的对象
- 方法区中类的静态变量、常量引用的对象
- 本地方法栈中 JNI (Native方法)引用的对象
3、如何确定老年代的对象时活着的?
答:GC Root Tracing 可到达的对象就是活着的。
4、G1收集器。
答:堆被划分成多个连续的区域,每个区域大小相等。新生代和老年代是逻辑上,物理上是不连续的。Young GC过程类似CMS,存活的对象被复制到多个区域空闲中,并发回收,回收时STW。Mix GC:初始标记(initial-Mark,STW,对根进行标记且触发一次Minor GC)->扫描根区域(Root Region Scanning,标记Survivor对old区的引用,且完成该阶段后才能开始下一次STW young GC)->并发标记(Concurrent Marking)->最终标记(Remark,STW)->清除垃圾(Cleanup,STW)。
5、GC三色标记算法
答:黑色:已被扫描;灰色:对象本身扫描,但子未完成扫描;白色:未扫描,扫描完成后为不可达对象。CMS是增量更新,在写屏障里发现白对象的引用被赋值到一个黑对象时,将它修改为灰色。G1,初始标记时保存快照,并发标记时将改变对象入队,写屏障中改变。
6、G1对比CMS。
答:CMS缺点是:会产生内存碎片;并发执行会占用一定CPU,导致吞吐量下降;由于并发清理,导致产生浮动垃圾。G1是复制清除,收集同时会做空间整合,减少了内存碎片;可预测的停顿,即根据用户设定的回收时间,制定回收计划,因此也是每次只回收一部分的增量清理。
7、Remembered Set。
答:记录了old->young,old->old之间的引用,避免Minor GC时扫描全部old generation region,在写引用和移动GC时都要去修改RSet,用空间换时间。
8、ZGC。
答:ZGC是直接在指针上做标记,GC移动时,指针颜色不对,在访问指针时加读屏障,读屏障会先把指针更新为有效的地址再返回,也就是读取单个对象会概率减速,而不会因为需要应用与GC一致而STW。Region也不是固定大小。由于不是增量回收,也不需要使用占用内存的RSet。全程并发,因此没有分代也不用写屏障。
9、Full GC触发条件。
答:1)老年代空间不足(即Eden存放不下大对象会直接存放老年代,老年代也存放不下这个对象);2)持久代空间不足;3)promotion failed:YGC到达年龄发生晋升,老年代内存不足;4)YGC晋升的对象总大小大于老年代的大小;5)显式调用System.gc。
10、性能调优。
答:指令:1)jps:查看java进程;2)jinfo:查看运行时参数;3)jstat:查看虚拟机统计信息,例如:jstat -class查看类加载统计,jstat -gc查看垃圾回收统计,jstat -compiler查看JIT(动态编译)成功数;4)jmap+MAT:手动导出dump
,也可以在运行参数中加入-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./参数,在内存溢出后自动导出dump;5)jstack:查看线程信息。
调优目标:尽量减少Full GC,在不影响Full GC的前提下可以适当加大年轻代。
调优参数参考:
1)调整JVM堆的设置;
2)调整年轻代和老年代之间的比例,默认1:2;
3)如果存在大量临时对象应该选择更大的年轻代,反之则适当增大老年代;
11、OutOfMemory原因?
答:jvm内存分配不足,死循环、对象过大、集合类中的对象使用完未释放。
12、 如果想在 GC 中生存 1 次怎么办?
答:覆盖Finalize(),不过同一个对象只会被调用一次。
13、如果想不被 GC 怎么办?
答:声明成static。
14、young GC为什么比Full GC快
minor gc 只针对 young 区, full gc 针对所有区,包括young gen、old gen、perm gen.
minor gc 和 full gc 都是从 gc root 开始扫描的.
minor gc root 是指:当前线程stack+ Dirty cards(可以知道yonug区对象被old区引用).
full gc root 是指:当前线程stack+ Perm Gen .
- 编译器优化
1、JIT(Just In Time Compiler)。
答:为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成与本地平台相关的机器语言,并进行各种层次的优化。编译器:将代码生成机器语言,并保存二进制文件;解释器:执行程序时才将代码逐条解释成机器语言。javac将Java程序代码编译成Java字节码,即.class文件,字节码不是机器语言,执行还需要把字节码翻译成机器指令,即深层次的编译,为了解决逐条翻译效率低的问题,引入JIT。只有热点代码会被编译成机器语言(即本地代码)。
2、公共子表达式消除。
答:如果一个表达式已经计算过,且变量值都没有改变,则用之前的结果直接替代。仅限于程序的基本块内。
3、方法内联。
答:方法内联就是把被调用方函数代码直接复制到调用者,减少函数入栈、出栈的开销。JVM会自动识别热点方法,通过-XX:CompileThreshold参数进行设置执行多次触发优化,通过-XX:MaxFreqInlineSize、-XX:MaxInlineSize设置内联方法的大小。如果相对热点方法使用内联优化,尽量使用final、private、static避免方法因为继承,导致额外的类型检查。
4、逃逸分析。
答:方法逃逸是指当一个对象在方法中被定义后,可能被外部方法所引用,也就是方法中的局部变量可以被外部方法修改。线程逃逸就是被其它线程所访问到。
如果证明对象不会发生逃逸,可以对其优化:1)栈上分配:将不会逃逸的对象分配到栈上,对象会随着方法的结束而自动销毁;2)同步消除:如果变量不会发生线程逃逸,就将同步措施消除掉;3)标量替换:Java原始数据类型就是标量,对象再不会发生逃逸的情况会被直接拆解成若干成员变量。