JVM面试题

1. 说说JAVA内存分配策略?

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配。对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

具体介绍可以参考:https://www.jb51.net/article/92311.htm

2. JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots?

通过引用计数法和可达性分析法来确定垃圾。

(1)引用计数法: 为每个对象添加一个引用计数器,每当有一个引用指向它时,计数器就加1,当引用失效时,计数器就减1,

当计数器为0时,则认为该对象可以被回收(由于引用计数法在循环引用时会导致内存泄露,目前在Java中已经弃用这种方式了)。

(2)可达性分析法:将“GC ROOT”对象作为起点,从这些节点向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。

Java中可以作为GC ROOT对象的有:虚拟机栈中引用的对象、方法区中的类静态属性引用的对象、方法区中常用引用的对象、本地方法栈中JNI引用的对象。
GC ROOT根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等。

3. JVM是如何回收垃圾的,可以说一下它的回收策略吗?

JVM通过一系列的回收算法来进行垃圾的回收,针对各种垃圾回收算法总结如下:

引用计数法: 为每个对象添加一个引用计数器,每当有一个引用指向它时,计数器就加1,当引用失效时,计数器就减1,
当计数器为0时,则认为该对象可以被回收。
优点:
1 实时性较高(效率高),无需等到内存不足时的时候才开始回收,运行的时候根据对象的计数器是否为0,就可以直接回收。
2 在垃圾回收过程中,应用无需挂起,如果申请内存时内存不足,则立刻报outofmember错误。
3 区域性,更新对象的计数器时只影响到该对象,不会扫描全部对象。
缺点:
1 每次对象被引用时,都需要去更新计数器有一点时间开销。
2 浪费CPU资源,即时内存够用仍然在运行时进行计数器的统计。
3 无法解决对象间的相互循环引用问题。(最大的缺点)

标记清除算法 : 哪些对象不用,将他们标记出来之后清除。分为2个阶段,分别是标记和清除
标记:从根节点开始标记引用的对象。
清除:未被标记的引用的对象就是垃圾,可以被清理。

优点:解决了引用计数法中的循环引用的问题,没有被root节点引用的对象都会被回收。
缺点:
1. 效率低,标记和清除两个动作都需要遍历所有的对象,并且在GC时需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。
2. 通过标记清除算法清理出来的内存碎片化比较严重。因为被回收的对象可能存在于内存的各个角落。所以清理出来的内存是不连贯的。
问题:标记清除的时候,为什么需要停止应用程序?
因为做标记清除算法的时候是需要遍历所有对象的,如果不停止应用程序,那么在标记的时候会不断有新的对象添加进来就无法确定他们之间的引用关系,
进而标记就会不正确的,造成清除的不正确。还有可能把一些不是垃圾的对象给清理掉。

标记(整理)压缩算法:在标记清除算法的基础上做了改进,它也是从根节点开始逐步对对象的引用做标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端。然后清理掉边界以外的垃圾,从而解决了碎片化的问题。
优点:解决了之前内存碎片的问题,特别是在存活率高的时候,效率远高于复制算法。
缺点:标记压缩算法多了一步对象移动内存位置的步骤,效率会降低。

复制算法:将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉,交换两个内存的角色。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
优点:
1 在垃圾对象多的情况下,效率较高
2 内存空间完整,无碎片化
缺点:
1 在垃圾对象较少的情况下不适用,如老年代内存。
2 可使用的内存降为原来一半。

分代收集算法:根据内存对象的存活周期不同,将内存划分成几块,java虚拟机中一般将内存划分成新生代和老年代,当新建对象时一般在新生代中分配内存,在新生代垃圾收集器回收几次后仍然存活的对象,将被移动到老年代,或者当大的对象在新生代中无法分配到足够连续的内存空间时也会直接分配到老年代。

4. 说说你知道JVM常用的垃圾收集器,各自的优缺点和适用场景。

常用的四个收集器:
串行:Serial收集器(复制算法)
-串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。

-新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;

-垃圾收集的过程中会Stop The World(服务暂停)

-一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。

-通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。

-特点:CPU利用率最高,停顿时间即用户等待时间比较长。

-适用场景:小型应用

并行:ParNew收集器(停止-复制算法)
-ParNew垃圾收集器是工作在年轻代上的,只是将串行的垃圾收集器改成了并行。老年代依然使用串行垃圾回收。

-新生代并行,老年代串行;新生代复制算法、老年代标记-压缩

-常用JVM参数控制:
-XX:+UseParNewGC 启用ParNew收集器,只影响新生代收集,不影响老年代。开启后会使用:ParNew(Young区用)+Serial Old的收集器组合。

-XX:ParallelGCThreads 限制线程数量。

-新生代并行,老年代串行目的:回收频率决定的。

并行:Parallel Scavenge收集器 (停止-复制算法)
-Parallel Scavenge收集器类似ParNew收集器,只是在此基础上新增了两个和系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。 与ParNew回收器不同的是它非常关注系统的吞吐量。既可以工作在老年代上也可以工作在年轻代上。

新生代复制算法、老年代标记-压缩

开启ParallelGC可以使用以下参数
-XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行回收器。
-XX:+UseParallelOldGC:使用Parallel old收集器,设置参数后, 新生代Parallel+老年代Parallel Old两者参数互为启用

特点:停顿时间短,回收效率高,对吞吐量要求高。

适用场景:大型应用,科学计算,大规模数据采集等。

CMS收集器(标记-清理算法)
CMS全称(Concurrent Mark Sweep),是一款并发的,使用标记-清除算法的垃圾回收器。该回收器是针对老年代垃圾回收的,
以获取最短回收停顿时间为目标。通过-XX:+UseConcMarkSweepGC进行设置。

CMS垃圾回收器的执行过程如下:
初始标记、并发标记、预清理、重新标记、并发清除和并发重置。其中初始标记和重新标记是独占系统资源的,而预清理、并发标记、
并发清除和并发重置是可以和用户线程一起执行的。因此它可以在应用程序运行过程中进行垃圾回收。

优点:并发收集低停顿

缺点:并发执行,对CPU资源压力大;采用标记清除算法会导致大量碎片。

G1收集器
G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器。相比于其他收集器而言,最大的区别在于它取消了年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域,这些区域包含了逻辑上的年轻代、老年代区域。这样做的好处是,我们再也不用单独的对每个代进行设置了,不用担心每个代内存是否足够。

G1提供了3种垃圾回收模式,Young GC、Mixed GC、Full GC。在不同的条件下将被触发。

具体介绍可以参考:https://www.cnblogs.com/hanlinhu/archive/2018/08/16/9487135.html

5. 说说 JVM串行回收、并行回收和并发回收的区别。

垃圾回收器从线程运行情况分类有三种:

串行回收:Serial回收器,单线程回收,全程stw;指使用单线程进行垃圾回收,垃圾回收时只有一个线程在工作,并且Java应用中的所有线程都要暂停工作等待垃圾回收的完成。这种现象称之为STW(stop-the-world)。

缺点:只有一个线程,执行垃圾回收时程序停止的时间比较长。对于交互性比较强的应用而言,这种垃圾收集器是不能被接收的。

并行回收:名称以Parallel开头的回收器,多线程回收,全程stw;指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。简答来说在串行垃圾收集器基础上做了改进,将单线程改为了多线程垃圾回收,这样可以缩短垃圾回收的时间。但是并行垃圾收集器在收集的过程中也会暂停应用程序,这个和串行垃圾回收器是一样的,只是并行执行速度快些,暂停的时间更短一些。

多个线程执行垃圾回收适合于吞吐量的系统,回收时系统会停止运行 高并发项目适合

并发回收:cms与G1,多线程分阶段回收,只有某阶段会stw;
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替进行),用户程序在继续镜像,而垃圾收集程序运行于另一个CPU上。

6. 说说Java中的内存溢出和内存泄漏的区别。

内存泄漏:程序在申请内存后,无法释放已申请的内存空间。简单来说就是你用malloc或new申请了一块内存,但是没有及时将内存释放,导致这块内存一直处于占用状态。一次内存泄漏危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早被占光。

内存泄漏分为4类:

常发性内存泄漏:发生代存泄漏的代码会被多次执行到,每执行一次将会产生一次内存泄漏。

偶发性内存泄漏:发生内存泄漏只有在某些特定情况下或操作过程中才会发生。它和常发性内存泄漏是相对的,如果一直处于某一特定环境下,偶发性也会变成常发性。所以测试环境和测试方法对检测内存泄漏至关重要。

一次性内存泄漏:发生内存泄漏的代码只会执行一次,由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

隐式内存泄漏:程序在运行中不停的分配内存,但是直到结束时才释放内存。严格的说这里并没有发生内存泄漏,因为最终释放了所有申请的内存。但对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致耗尽系统的所有内存。所以我们称这类内存泄漏为隐式内存泄漏。

内存溢出:指程序在申请内存时,没有足够的内存空间供其使用,出现outofmemory。简单来说就是,你申请了10个字节的空间,
但是你在这个空间写入11或以上字节的数据,这样就会产生溢出。

7. 如果发生了内存泄漏和内存溢出,我们应该如何去解决?

内存溢出的原因以及解决办法:
1.内存中加载的数据量过于庞大,如一次从数据库中取出过多数据;
2.集合类中有对对象的引用,使用后未清空,是的JVM不能回收;
3.代码中存在死循环或者循环产生过多重复的对象实体;
4.使用第三方软件中的BUG;
5.启动参数内存值设定的过小
内存溢出的解决方案:
1.修改JVM启动参数,直接增加内存。(-Xms,-Xms参数一定不要忘记加)
2.检查错误日志,查看“OutOfMemory”错误前是否有其他异常或错误。
3.对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
1)检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,
就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库
中数据多了,一次查询就有可能引起内存溢出。对数据库查询尽量采用分页查询。
2)检查代码是否有死循环或递归调用。
3)检查是否有大循环重复产生新对象实体。
4)检查List、Map等集合对象是否有使用后,未清除的问题。List、Map等集合对象
会始终存有对对象的引用,使得这些对象不能被GC回收。
4.使用内存查看工具动态查看内存使用情况。
内存泄漏的解决方案:
1. 尽早释放无用对象的引用。好的办法是使用临时变量的时候,让引用变量在退出活动域后自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露。

2.程序进行字符串处理时,尽量避免使用String,而应使用StringBuffer。

3. 尽量少用静态变量。因为静态变量是全局的,GC不会回收。

4.避免集中创建对象尤其是大对象,如果可以的话尽量使用流操作。

5.尽量运用对象池技术以提高系统性能。生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略。 

6.不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用hashtable,vector创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃。

7. 优化配置。

8. 如何判断我们项目中存在内存泄漏?

可以通过一些性能监测分析工具,如 JProfiler、OptimizeitProfiler。

9. 说说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值

先通过jps -l 查看当前运行程序的进程号,然后根据当前进程通过jinfo -flag+具体参数+进程编号 查看当前运行程序的配置。
从初始默认值调到我们的期望值。

JVM的参数类型:标配参数、x参数、xx参数

XX类型:Boolean类型,KV设值类型,jinfo举例,如何查看当前运行程序的配置。

第一种
jps
jinfo -flag 具体参数 java进程编号
jinfo -flags java进程编号
第二种
java -XX:+PrintFlagsInitial
java -XX:+PrintFlagsFinal
java -XX:+PrintFlagsFinal -version

10. 你平时工作用过的JVM常用基本配置参数有哪些?

-Xms : 初始内存大小,默认为物理内存的1/64,等价于-XX:lnitialHeapSize

-Xmx : 最大分配内存,默认为物理内存的1/4,等价于-XX:MaxHeapSize

-Xss:设置单个线程栈的大小,一般默认为512k-1024k,等价于-XX:ThreadStackSize

-Xmn:设置年轻代大小

-XX:MetaspaceSize:设置元空间大小。元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间大小仅受本地内存限制。

-XX:+PrintGCDetails : 输出详细GC收集日志

-XX:SurvivorRatio:设置新生代中eden和SO/SI空间的比例。默认 -xx:SurvivorRatio=8 ,Eden:SO:SI=8:1:1。假如 -xx:SurvivorRatio=4 ,Eden:SO:SI=4:1:1。SurvivorRatio值就是设置eden区的比例占多少,SO/SI相同

-XX:NewRatio:配置年轻代与老年代在堆结构的占比。默认-XX:NewRation=2 新生代占1,老年代2,年轻代占整个堆的1/3。
假如 -XX:NewRatio=4 新生代占1,老年代4,年轻代占整个堆的1/5。NewRatio值就是设置老年代的占比,剩下的1给新生代。

-XX:MaxTenuringThreshold:设置垃圾最大年龄。

11. 说说强引用,软引用,虚引用,弱引用,它们之间的关系和作用?

强引用:即正常引用也就是Java中默认的引用,如果没有对象指向的时候将会被回收。但是只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,都不会去回收,因此强引用是造成Java内存泄漏的主要原因之一。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了

软引用:在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。

弱引用:无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。可以解决某些地方的内存泄漏检测。

虚引用:顾名思义就是形同虚设,与其它几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。它不能单独的使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。主要作用是: 跟踪对象被垃圾回收的状态,当对象被回收时,先把回收的对象放入queue,然后清理堆外内缓存。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必须的清理工作。在 JDK1.2 之后,用 PhantomReference 类来表示。换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。

引用队列(ReferenceQueue):引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,
如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

具体引用类型介绍可以参考: https://www.cnblogs.com/liyutian/p/9690974.html

12. 思考:假如有一个应用需要读取大量的本地图片:如果每次读取图片都从硬盘读取则会严重影响性能,如果一次性加载到内存中又可能造成内存溢出。该如何解决?

此时使用软引用可以解决这个问题。

设计思路:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,
jvm会自动回收这些缓存图片对象所占用的空间,从而有效避免了OOM的问题。
Map<String,SoftReference<Bitmap>> imageCache = new HashMap<String,SoftReference<BitMap>>();

13. 你知道弱引用的话,能谈谈WeakHashMap 吗?

weakHashMap也是Map的一种,不同的是当它的key值设置为null值后会被Java垃圾回收机制回收掉。

14 . 请谈谈你对OOM的认识。

1.什么是OOM
OOM从字面意思可以理解为“内存用完了”, 意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error。

2.为什么会OOM
1)分配的内存少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
2)应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。

3.常见的OOM的情况有哪些

(1) java.lang.StackOverflowError(栈溢出)
发生原因:程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出(栈溢出)
发生区域:java虚拟机栈或本地方法栈
解决方案:通过虚拟机参数-Xss来设置栈的大小

(2) java.lang.OutOfMemoryError: Java heap space(java堆内存溢出)
发生原因:不停的new了很多的对象,或者new了一个很大的对象。
发生区域:Java堆
解决方案:1.可以通过虚拟机参数-Xms,-Xmx等修改堆大小。2.考虑内存泄漏,排查代码。

(3) java.lang.OutOfMemoryError: GC overhead limit exceeded
发生原因:GC回收时间过长,超过98%的时间用来做GC,并且回收了不到2%的堆内存
解决方案:
a,查看项目中是否有大量的死循环或有使用大内存的代码,优化代码。
b,JVM给出这样一个参数:-XX:-UseGCOverheadLimit  禁用这个检查,其实这个参数解决不了内存问题,
只是把错误的信息延后,替换成 java.lang.OutOfMemoryError: Java heap space。
c,增大堆内存 set JAVA_OPTS=-server -Xms512m -Xmx1024m -XX:MaxNewSize=1024m -XX:MaxPermSize=1024m  

(4) java.lang.OutOfMemoryError: Direct buffer memory
发生原因:直接内存不足。
写NIO程序经常使用ByteBuffer来读取或写入数据,这是一种基于通道与缓冲区的I/O方式,ByteBuffer.allocate() 分配JVM堆内存,属于GC管辖范围,需要拷贝所以速度相对较慢。

ByteBuffer.allocateDirect() 分配操作系统本地内存,不属于GC管辖范围,不需要内存拷贝所以速度相对较快,但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会抛出OutOfMemoryError,那程序就直接奔溃了。

解决方案:
a.检查是否直接或间接使用了 NIO ,例如手动调用生成 buffer 的方法或者使用了 NIO 容器如 netty, jetty, tomcat 等等;
b.-XX:MaxDirectMemorySize 加大,该参数默认是 64M ,可以根据需求调大试试;
c.检查 JVM 参数里面有无: -XX:+DisableExplicitGC ,如果有就去掉.

(5) java.lang.OutOfMemoryError : unable to create new native thread
高并发请求服务器时经常出现。
发生原因:
a.一个应用进程创建了多个线程,超过系统承载极限
b.你的服务器不允许你的应用程序创建这么多线程,Linux默认允许单个进程可以创建的线程数1024
解决办法:
1.想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多的线程,如果不是,改代码将线程数量降到最低。
2.对于有的应用,确实需要创建很多线程,远超过linux系统默认的1024个线程的限制,可以通过修改linux服务器配置扩大linux默认限制。
(6) java.lang.OutOfMemoryError: Metaspace
JVM参数
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
java8及之后的版本使用Metaspace来代替永久代。
Metaspace是方法区在HotSpot中的实现,它与永久代最大的区别是:元空间并不在虚拟机内存中,而是使用本地内存。

具体OOM介绍请参考
https://www.cnblogs.com/wjh123/p/11143379.html
https://segmentfault.com/a/1190000019910501

15.GC垃圾回收算法和垃圾收集器的关系,分别是什么请你谈谈

GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。因为目前为止还没有完美的收集器出现,更加没有万能的收集器,知识针对具体应用最合适的收集器,进行分代收集。

16. 怎么查看服务器默认的垃圾收集器是哪个?生产上如何配置垃圾收集器的?请谈谈你对垃圾收集器的理解

a. 怎么查看

java -XX:+PrintCommandLineFlags -version

b. 如何配置

c. 如何选择垃圾收集器

单CPU或小内存,单击程序

-XX:+UseSerialGC

多CPU,需要最大吞吐量,如后台计算型应用

-XX:+UseParallelGC 或者

-XX:+UseParallelOldGC

多CPU,追求低停顿时间,需快速响应如互联网应用

-XX:+UseConcMarkSweepGC

-XX:+ParNewGC

原文地址:https://www.cnblogs.com/Eugene-Jin/p/12943844.html