标签: JVM
前言:JVM相关知识也是面试过程中常问的一点,学习JVM知识有助于了解Java底层实现原理,在开发过程中遇到问题时可以从底层角度去思考问题,在进行系统性能优化时,也可以从GC角度是进行调优,本文总结了JVM常见问题并结合面试经历加以阐述。
1.简述一下JVM:
-
思路:考验的是对JVM的整体理解,可以先阐释整体框架,再深入分析某一点
-
解答:从javac到GC
-
首先通过IDE编写好完整Java代码后,由javac编译生成与平台无关的class文件(Java跨平台原因),该过程分为:语法分析、语法分析、语义分析、代码生成四个阶段,在语义分析阶段,又分成填充符号表、标注检查、数据流分析和控制流分析。标注检查:比如定义int a = 1+2,在这个阶段就会被解析成a=3,又如在控制流分析阶段去除语法糖的动作,类似foreach的解语法糖等。
-
编译生成字节码文件之后,进行JVM加载,类加载采用双亲委派模型,其工作过程如下:当一个类加载器收到了类加载的请求,首先不会自己去尝试加载这个类,而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
使用该机制的原因:如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统将会出现多个不同的Object类, Java类型体系中最基础的行为就无法保证,即为了确保java核心类库的安全。
打破双亲委派机制的例子:Tomcat的类加载机制,详细可参考Tomcat架构相关资料。 -
类加载之后就进行验证-准备-解析-初始化操作,各过程如下,
- 验证:目的是为了确保Class文件的字节流中包含的信息符合当前的虚拟机规范,不会危害虚拟机自身安全
- 准备:正式为类变量分配内存并设置类变量初始值的阶段,如public static int value = 123,这时赋值value为0
- 解析:虚拟机将常量池内的符号引用转换为直接引用的过程
- 初始化:始化一个对象,首先要加载该对象所对应的class文件,该文件的数据会被加载到永久代,并创建一个底层的instanceKlass对象代表该class,再为将要初始化的对象分配内存空间,优先在线程私有内存空间中分配大小,如果空间不足,再到eden中进行内存分配.
- 初始化之后便可以使用了,加载的类信息存入了运行时数据区的方法区,也即是永久代,运行时数据区分为:java堆、java栈、本地方法栈、方法区、pc寄存器
- new一个对象需要在Java堆中开辟内存,使用完后需要进行垃圾回收操作,即GC:以Hot spot为例,java堆分为年轻代和老年代,通过GC Roots可达性分析标记不可达内存对象进行回收处理,GC算法有MS、Copying、MC,年轻代有Serial, ParNew, Parallel Scavenge等都是采用复制算法。老年代有Serial-Old, Parallel-Old, CMS。还有一个G1收集器。之后简单论述了一下CMS,CMS分为5个部分:初始标记,并发标记,重新标记,并发清除和并发重置,其中初始标记和重新标记是需要Stop the World的。CMS还有一个概念就是Concurrent Mode Failure,发生之后需要来一记Serial-Old的干活。
2.简述JVM运行时数据区:
-
解答:运行时数据区整体分成两类:线程私有与线程共享
-
线程私有的包括:
-
程序计数器pc:若正在执行的是java方法,则计数器记录的是正在执行的字节码指令的地址,若正在执行的是native方法,则计数器为空,该区域是唯一一个不会导致outofmemoryError的区域
-
JVM栈:描述的是Java方法执行的内存模型:每个方法都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息,局部变量表存放了编译期可知的基本数据类型,对象引用,和returnAddress类型(指向一条字节码指令地址),局部变量表的内存空间在编译器确定
在运行期不变,可导致两种异常:
线程请求的栈深度大于虚拟机允许的深度-StackOverflowError;
虚拟机无法申请到足够的内存-OutOfMemoryError -
本地方法栈:和虚拟机栈类似,但它是为Native方法服务的
-
线程共享的包括:
-
堆:java堆是被所有线程共享的内存区域,在虚拟机启动时创建,用来分配对象实例和数组,堆是垃圾回收器主要管理的区域,堆分成了新生代和老年代,1.7下还有永久代,新生代中又分为了eden区和survivor区,survivor区又分成了S0和S1,或着是from和to,其中eden,from和to的内存大小默认是8:1:1(细节),从内存分配角度看,堆可划分出多个线程私有的分配缓冲区(TLAB),大小可通过 -Xmx 和 -Xms 控制
-
方法区:用来存放虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等信息,GC会回收该区域的常量池和进行类型的卸载
-
运行时常量池:Class文件的常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放在运行时常量池中,还把翻译出来的直接引用也放在运行时常量池中,运行时产生的常量也放在里面
3.简述下GC机制:
- 思路:进行垃圾回收,首先要判断一个对象是否活着,这就引出了两种方法:引用计数法和可达性分析法,gc roots 类型,引用类型,两次标记过程,垃圾回收算法,内存分配策略,触发垃圾回收,垃圾回收器,也会回收方法区
- 解答:要进行垃圾回收,首先要判断对象是否存活,引出了两个方法:
- 引用计数法:
* 思想:给对象设置引用计数器,没引用该对象一次,计数器就+1,引用失效时,计数器就-1,当任意时候引用计数器的值都为0时,则该对象可被回收
* Java不适用原因:无法解决对象互相循环引用的问题
-
可达性分析法,以GC Roots为起点,从这些起点开始向下搜索,经过的路径称为引用链。若一个对象到GC Roots之间没有任何引用链,则该对象是不可达的。那么可作为GC Roots的对象有如下:
* 虚拟机栈(栈帧中的局部变量表)中引用的对象 * 方法区中类静态属性引用的对象 * 方法区中常量引用的对象 * 本地方法栈中JNI(Native方法)引用的对象
-
在可达性分析过程中,对象引用类型会对对象的生命周期产生影响,JAVA中有这几种类型的引用:
- 强引用:只要该引用还有效,GC就不会回收
- 软引用:内存空间足够时不进行回收,在内存溢出发生前进行回收、用SoftReference类实现
- 弱引用:弱引用关联的对象只能存活到下一次Gc收集、用WeakReference类实现
- 虚引用:无法通过虚引用获得对象实例,也不会对对象的生存时间产生影响、唯一目的:当该对象被Gc收集时,收到一个系统通知。用PhantomReference类实现
-
一个对象真正不可用,要经历两次标记过程:
- 首先进行可达性分析,筛选出与GCRoots没用引用链的对象,进行第一次标记
- 第一次标记后,再进行一次筛选,筛选条件是是否有必要执行finalize()方法。若对象有没有重写finalize()方法,或者finalize()是否已被jvm调用过,则没必要执行,GC会回收该对象
- 若有必要执行,则该对象会被放入F-Queue中,由jvm开启一个低优先级的线程去执行它(但不一定等待finalize执行完毕)。Finalize()是对象最后一次自救的机会,若对象在finalize()中重新加入到引用链中,则它会被移出要回收的对象的集合。其他对象则会被第二次标记,进行回收
- JAVA中的垃圾回收算法有:
-
标记-清除(Mark-Sweep):
-
两个阶段:标记, 清除
- 缺点:两个阶段的效率都不高;容易产生大量的内存碎片
-
复制(Copying):
- 把内存分成大小相同的两块,当一块的内存用完了,就把可用对象复制到另一块上,将使用过的一块一次性清理掉
- 缺点:浪费了一半内存
-
标记-整理(Mark-Compact):
- 标记后,让所有存活的对象移到一端,然后直接清理掉端边界以外的内存
-
分代收集:
- 把堆分为新生代和老年代
- 新生代使用复制算法
- 将新生代内存分为一块大的Eden区和两块小的Survivor;每次使用Eden和一个Survivor,回收时将Eden和Survivor存活的对象复制到另一个Survivor(HotSpot的比例Eden:Survivor = 8:1)
- 老年代使用标记-清理或者标记-整理
-
-
触发GC又涉及到了内存分配规则:(对象主要分配在Eden,若启动了本地线程分配缓冲,将优先在TLAB上分配):
-
对象优先在Eden分配
-
当Eden区没有足够的空间时就会发起一次Minor GC
-
大对象直接进入老年代
-
典型的大对象(需要分配大量连续内存)是很长的字符串和数组
-
长期存活的对象进入老年代
-
每个对象有年龄计数器,每经过一次GC,计数器值加一,当到达一定程度时(默认15),就会进入老年代
-
年龄的阈值可通过参数 -XX:MaxTenuringThreshold设置
-
对象年龄的判定
-
Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可直接进入老年代,无须等到MaxTenuringThreshold要求的年龄
-
空间分配担保:发生YGC前,jvm会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若大于,则YGC是安全的
若不大于,jvm会查看HandlePromotionFailure是否允许担保失败,若不允许,则改为一次FGC,若允许担保失败,则检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,则尝试进行FGC;若小于,则要改为FGC -
垃圾收集器:
-
Serial(串行收集器)
- 特性:单线程,stop the world,采用复制算法
- 应用场景:jvm在Client模式下默认的新生代收集器
- 优点:简单高效
-
ParNew:
- 特点:是Serial的多线程版本,采用复制算法
- 应用场景:在Server模式下常用的新生代收集器,可与CMS配合工作-
Parallel Scavenge:
- 特点:并行的多线程收集器,采用复制算法,吞吐量优先,有自适应调节策略
- 应用场景:需要吞吐量大的时候
-
SerialOld
- 特点:Serial的老年代版本,单线程,使用标记-整理算法
-
Parallel Old
-
Parallel Scavenge的老年代版本,多线程,标记-整理算法
CMS:
-
-
过程:
- 初始标记:stop the world 标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing
- 重新标记:stop the world;修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
- 并发清除:清除对象
- 特点:以最短回收停顿时间为目标,使用标记-清除算法
-
优点:并发收集,低停顿
- 缺点:对CPU资源敏感,无法处理浮动垃圾(并发清除时,用户线程仍在运行,此时产生的垃圾为浮动垃圾)产生大量的空间碎片
G1
- 初始标记:stop the world 标记GC Roots能直接关联到的对象
- 并发标记:可达性分析
- 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
- 筛选回收:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
并行与并发
分代收集
-
空间整合:从整体看是基于“标记-整理”的,从局部(两个region之间)看是基于“复制”的。
-
可预测的停顿:使用者可明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
- 特点:面向服务端应用,将整个堆划分为大小相同的region。
- 执行过程:
GC自适应调节策略 Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
-
回收方法区:永久代中主要回收两部分内容:废弃常量和无用的类
- 废弃常量回收和对象的回收类似
- 无用的类需满足3个条件
- 该类的所有实例对象已被回收
- 加载该类的ClassLoader已被回收
- 该类的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
备注:内容整理于网络与个人学习总结,希望能对你有所帮助,欢迎回来~
网友评论