JVM Java虚拟机相关基础知识问答
1. 什么情况下会产生栈溢出错误?
首先要明白什么是栈:栈是线程私有的,它的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态连接,方法出口等信息。局部变量表包含基本数据类型和对象引用类型。
产生栈溢出的错误主要原因有以下几种:
线程请求的栈深度超过虚拟机所允许的最大深度。方法的递归调用可能产生这种结果。(旧式虚拟机),此时异常为StackoverflowError。
现在的虚拟机已经不会再有这种问题,虚拟机栈已经实现了动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者新线程建立时没有足够内存区创建对应的栈,那么会抛出异常OutOfMemoryError(线程启动过多。)
2. JVM的内存模型
JVM内存结构分为五个区域,三块是线程私有,两块是线程公用。
线程私有:程序计数器(当前线程所执行的字节码的行号指示器),虚拟机栈(存放基本数据类型,对象的引用,方法出口等),本地方法栈(服务于本地方法,和虚拟栈相似)
线程共享:堆区(java内存最大的一块,所有对象实例,数组都存在堆区,也是GC回收的地方),
方法区(存放已经被加载的类信息,常量,静态变量,代码数据等),也被称为永久代,此区域回收目标主要是常量池的回收和类型卸载。
3.JVM内存如何分代与分区?如何配置参数?
如上面的问题所示,JVM的内存模型中,共享内存有堆+方法区(方法区加其他内容一起也被称为永久代)。
Java堆内存中分为新生代和老年代,这两代的内存分配和回收策略都不同。默认设置中,新生代和老年代的内存比例为1:2,可以通过参数-XX:NewRatio配置
新生代分为一个Edon区和两个Survivor区,默认内存比例为8:1:1,参数配置可使用 -XX:SurvivorRatio。
Survivor区中对象被复制次数为15,存活到第16次的对象可被移到老年代,可使用 -XX:+MaxTenuringThreshold来配置。
4. GC的类型和触发?
针对 HotSpot JVM 而言,GC分为minor GC和 major GC(full GC和major GC等价),触发频率和触发条件都不同。其中,minor GC是发生于新生代中的,触发条件是当Edon区空间填满,此时GC回收新生代中的内存,同时把存活对象移到S区。minor GC触发非常频繁。而当老年代空间填满之后,会触发major GC,major GC会清理整个内存堆,包括新生代和老年代,所以它的耗时长,而且触发频率往往是minor GC的十分之一。
(例外:在parallel scavenge框架下,默认是在进行major GC之前都一定要触发一次minor GC,可以减少全回收的工作量)
此外,还有当System.gc()被显式调用的时候,也会触发Full GC。
上一次GC之后Heap各区域的分配策略动态变化也可能会导致Full GC。
5. JVM新生代为什么设置Survivor区?
JVM新生代中有两个Survivor区,各占新生代内存的10%,这样是因为:如果不设置S区,每次进行一次minor GC之后,存活的对象直接移到老年代,那么就很容易把老年代填满,从而触发major GC。而老年代的内存空间比新生代大很多,一次full GC耗时也更长,所以需要在新生代对象和老年代之间设置缓冲区。
S区存在的意义就是减少被送到老年代的对象,默认规定只有在新生代经过16次minor GC还存活的对象才会被送老年代。
6. JVM新生代为什么设置两个S区?
两个S区的设置可以解决碎片化的问题。在每一次minor GC过后,Edon区清空,存活的对象移到第一片S区S0,等到Eden再次填满触发GC,Eden和S0中存活的对象会一起被复制进S1。这样就可以保证存活对象占用连续内存空间。
7. JVM中GC调优指标?
GC的实现目标是:准确,高效,低停顿,以及空闲内存规整(少碎片化)。
GC的优化主要看两个参数:响应时间和吞吐量。
响应时间是指应用对请求的响应时间,如一个桌面应用对时间的响应时间,网站返回页面的时间或数据库查询结果返回的时间。对于专注最小响应时间的应用,不能接受长时间的停顿。
吞吐量是应用在一段时间的最大工作量,比如说在给定时间内完成的事务数,每小时能够完成的任务,每小时数据库可完成的查询操作等。对于专注吞吐量的应用而言,稍长时间等待是可以接受的。
8. GC中如何判断对象是否存活?
早期的内存回收策略是引用计数法,在这种方法中,每个对象实例都有一个引用计数,当对象被其他对象引用的时候计数+1,当对象被设置新值或引用超过生命周期时,引用计数-1,任何引用计数为0的对象可以被当做垃圾收集。而当一个对象被内存回收时,它引用的其他对象计数也应-1。
引用计数法直观比较好理解,优点是执行很快,且可穿插在程序运行中,不会造成打断,但问题是无法解决对象之间循环引用的问题,就算二者同被赋值为空,也不能回收,所以被废弃。
后期内存回收策略主要采用可达性分析法,这是从一个节点GC ROOT开始,在引用关系图中根据连线搜索所有可达到的其他节点,整个搜索所走过的路径被称为引用链,当一个对象不存在于到GC ROOT的引用链,就被认为不可达,而不可达的节点被判定为可回收对象。
可以作为GC ROOT节点的节点有以下几种:
虚拟机栈引用的对象。
方法区中静态类型引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI引用的对象。
可以看出,能作为GC ROOT的节点需要是在对象图之外被特别定义的,不可能被对象图内的对象循环引用,所以也就解决了交叉引用的问题。
9. 对象如何被确认标记死亡?
在可达性分析算法中不可达的对象,并不一定会被回收,因为对象要真正被宣告死亡,至少经历两次标记的过程:第一次标记是对象可达性分析后发现没有与GC ROOT相连的引用链,那么它会被第一次标记并且筛选。
筛选的条件是执行finalize()方法。
finalize方法需要用户指明,且对象之前没有调用过,则对象并不会在本次内存回收中被清理,而是会等到下一次再回收。
10. 为什么内存回收要有finalize()方法?
因为在一些特殊情况下,垃圾回收器不能处理,无法识别特殊内存区域。所以java允许自定义一个finalize方法来释放出这部分内存空间。这些特殊的内存区域可能是:native method中调用了C或C++的方法分配内存空间,或者是文件资源这些不属于内存回收器的回收范围。
由于java中没有“析构”函数,所以在做特殊清理时,需要手工创建一个清理工作的普通方法。一个例子:如果程序中某对象打印内容到屏幕上,被GC清理的时候并不会自动擦除,这时需要在本轮调用自定义的finalize方法来手动清除这个图像。而真正的对象则会在下一轮清理。
11. Java中的引用?
无论在引用计数法还是可达性分析中,判断对象存活与否都离不开判断对象的引用。在Java中,对象的引用分为强引用,软引用,弱引用,虚引用四种,这四种引用强度依次减弱。
判断对象生存与否仅基于强引用。
强引用 :普遍的正常引用,类似 Object obj = new Object() 这类引用,只要强引用还存在,就不会被回收。
软引用:一些有用但并非必须的对象。在系统内存占满时,会回收这些对象,之后如果还是没有足够的内存才会抛出内存溢出异常。
弱引用:强度比软引用还要弱一些,这些对象只会生存到下一次GC之前,无论当前内存是否足够,都会回收。
虚引用:是最弱的引用关系,对象是否有虚引用完全不影响对象的生存时间,也不能获得对象实例。虚引用存在的意义就是在对象被回收的时候得到一个系统通知。
12. 方法区什么可被回收,如何判断是否需要回收?
如上文所述,方法区中回收的对象主要是废弃常量和无用的类。对于废弃常量,可以用引用可达性判断,但对于无用的类,需要满足三个条件:
该类所有实例都被回收,在堆区不存在任何该类的实例;
加载类的ClassLoader也已经被回收;
该类对应的java.lang.Class对象已经没在任何地方被引用。
13. JVM的垃圾收集算法?
标记清除算法:是最基础的内存回收算法,分为两个阶段:标记阶段和清除阶段。标记阶段会标记出所有需要被回收的对象,清除阶段就回收被标记的内存。该算法的优点是:实现容易,不需开辟新的内存空间,不需移动对象,响应快,但是一个严重的问题是会产生很多的内存碎片,可能导致无法找到一块完整的内存区域存放大的对象而频繁GC。
复制算法:为了解决标记清除法的碎片化问题,就产生了复制算法。复制算法把可用内存分为两份,每次只用其中一块,当一块用完后就把存活的对象复制到另一块上去,再一次性清理掉已使用的空间。该算法的优点是:不产生碎片内存,缺点是可使用的内存变为原来的一半,而且复制算法的效率与存活对象的多少有很大关系,如果存活对象较多,则效率大大降低。
标记整理算法:是一种融合算法,该算法标记阶段和标记清除算法一样,标记出所有需要回收的对象,但是完成标记后,先不进行清理,而是把存活对象都向一端移动,然后清理掉边界以外的内存。它的成本比标记清除算法更高,但是解决了碎片化问题,效率也比复制算法高。
标记类算法不需要停止程序(stop-the-world)但是复制类算法需要。
14. JVM中的垃圾收集是按照什么规则进行的?
GC的原则是:分代收集,因为新生代每次垃圾回收都有大量的对象被回收,而老年代只有很少的对象被回收,在不同代的回收策略应该不同。
目前大部分新生代的垃圾收集器采用的都是复制算法。也就是一大块Eden区和两块Survivor区(S0,S1),在每次触发minor GC的时候采用复制的方式回收Eden区和一块S区的内存,复制到另一块空闲的S区上。
而老年代采用的多为标记整理类法,(CMS采用的标记清除类算法)因为老年代存活率高,每次回收的内存不多,而且回收频率远低于新生代。
15. HotSpot中几种垃圾收集器及各自优缺点。
目前JVM里已经产生了几种主流的垃圾收集器。垃圾收集器分为作用于新生代和老年代的收集器。以及可以同时作用于新生代和老年代的收集器G1.
新生代收集器:
Serial收集器,ParNew收集器,Parallel Scavenge收集器
image.png
image.png
image.png
老年代收集器:
Serial Old收集器,Parelle Old收集器,CMS收集器。
image.png
image.png
image.png
G1收集器(不需结合其他收集器一起使用)