Java面试题整理
JVM
1.对象是否可回收判断方法?
-
引用计数法
每个对象都有一个引用计数器,当对象被引用一次计算器就加1;当引用失效时计数器就减1。当对象的计数器为0时,对象就是要被回收的。简单高效,缺点是无法解决对象之间相互循环引用的问题。
-
可达性分析算法
以 Roots 节点作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。此算法解决了上诉循环引用的问题。
2.简述几种垃圾回收算法?
-
标记-清除算法
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,分为“标记”和“清除”两个阶段;首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它的主要不足有两个:- 效率问题,标记和清除两个过程的效率都不高。
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-清除算法的执行过程如下图:
-
复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。复制算法的执行过程如下图:
现在的商业虚拟机都采用这种算法来回收新生代,IBM研究指出新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot虚拟机默认 Eden:Survivor=8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(其中一块Survivor不可用),只用10%的内存会被“浪费”。
-
标记-整理算法
复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
标记-整理算法的示意图如下:
-
分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块并采用不同的垃圾收集算法。
一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
3.简述几种垃圾收集器?
-
Serial 收集器
Serial收集器是最基本的、历史最悠久的收集器,曾经是JDK1.3.1之前虚拟机的新生代收集的唯一选择。Serial这个名字揭示了这是一个单线程的垃圾收集器,特点如下:
-
仅仅使用一个线程完成垃圾收集工作。
-
在垃圾收集时必须暂停其他所有的工作线程,直到垃圾收集结束。
-
Stop the World 是在用户不可见情况下执行的,会造成某些应用响应变慢。
-
使用复制算法。
Serial收集器的工作流程如下图:
虽然如此,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。它的优点同样明显:简单而高效(单个线程相比),并且由于没有线程交互的开销,专心做垃圾收集自然获得更高的单线程效率。在一般情况下,垃圾收集造成的停顿时间可以控制在几十毫秒甚至一百多毫秒以内,还是可以接受的。
-
-
ParNew 收集器
ParNew收集器其实是Serial收集器的多线程版本,与Serial不同的地方就是在垃圾收集过程中使用多个线程,剩下的所有行为包括控制参数、收集算法、Stop the World、对象分配规则和回收策略等都一样。ParNew收集器也使用复制算法。
ParNew收集器的工作流程如下图:
ParNew收集器看似没有多大的创新之处,但却是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为除了Serial收集器外,目前只有ParNew收集器能够与CMS收集器配合工作,而CMS收集器是HotSpot在JDK1.5时期推出的具有划时代意义的垃圾收集器。
ParNew收集器在单个线程的情况下由于线程交互的开销没有Serial收集器的效果好。不过,随着CPU个数的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同。可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。
-
Parallel Scavenge 收集器
Parallel Scavenge收集器和ParNew类似,是一个新生代收集器,使用复制算法,又是并行的多线程收集器。不过和ParNew不同的是,Parallel Scavenge收集器的关注点不同。
CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的则是达到一个可控制的吞吐量。吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)。如果虚拟机一共运行100分钟,垃圾收集运行了1分钟,那么吞吐量就是99%。
停顿时间越短就越适合与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。Parallel Scavenge收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数。MaxGCPauseMillis 参数允许的值是一个大于0的毫秒数,收集器将尽可能在给定时间内完成垃圾收集。不过垃圾收集时间的缩短是以牺牲吞吐量和新生代空间为代价的,短的垃圾收集时间会导致更加频繁的垃圾收集行为,从而导致吞吐量的降低。
-
Serial Old 收集器
Serial Old是Serial的老年版本,在Serial的工作流程图中可以看到,Serial Old收集器也是一个单线程收集器,使用“标记-整理”算法。这个收集器主要个Client模式下的虚拟机使用。如果在Server模式下,它有两个用途:一个是在JDK1.5之前的版本中与Parallel Scavenge收集器搭配使用;另一个就是作为CMS收集器的后背预案,在并发收集发生Concurrent Mode Failure时使用。这个收集器的工作流程在Serial的后半部分有所体现。
-
Parallel Old 收集器
Parallel Old收集器是Parallel Scavenge收集器的老年版本,它使用多线程和“标记-整理”算法。这个收集器是JDK1.6开始提供。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器的组合。
-
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。在重视响应速度和用户体验的应用中,CMS应用很多。
CMS收集器使用“标记-清除”算法,运作过程比较复杂,分为4个步骤:
-
初始标记(CMS Initial Mark)
-
并发标记(CMS Concurrent Mark)
-
重新标记(CMS Remark)
-
并发清除(CMS Concurrent Sweep)
其中,初始标记和并发标记仍然需要Stop the World、初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC RootsTracing的过程,而重新标记阶段则是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段长,但远比并发标记的时间短。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以整体上说,CMS收集器的内存回收过程是与用户线程一共并发执行的。
CMS收集器的工作流程如下图:
CMS的优点就是并发收集、低停顿、是一款优秀的收集器。不过,CMS也有缺点,如下:
- CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4,当CPU个数大于4时。垃圾收集线程使用不少于25%的CPU资源,当CPU个数不足时,CMS对用户程序影响很大。
- CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC。
- CMS使用“标记-清除”算法,会产生内存碎片。
-
-
G1 收集器
G1(Garbage First)收集器是最先进的收集器之一,是面向服务端的垃圾收集器。与其他收集器相比,G1收集器有如下优点:
-
并发与并行:有些收集器需要停顿的过程G1仍然可以通过并发的方式让用户程序继续执行。
-
分代收集:可以不使用其他收集器配合管理整个Java堆。
-
空间整合:使用”标记-整理”算法,不产生内存碎片。
-
可预测的停顿:G1除了降低停顿外,还能建立可预测的停顿时间模型。
G1中也有分代的概念,不过使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),G1收集器之所以能建立可预测的停顿时间模型,是因为他可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次优先收集价值最大的Region。这样就保证了在有限的时间内尽可能提高效率。
G1收集器的大致步骤如下:
-
初始标记(Initial Mark)
-
并发标记(Concurrent Mark)
-
最终标记(Final Mark)
-
筛选回收(Live Data Counting and Evacuation)
G1收集器的流程如下图:
-
4.常用的JVM调优工具?
JDK自带了很多监控工具,都位于JDK的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款试图监控工具。
- jconsole:用于对JVM中的内存、线程和类等进行监控。
- jvisualvm:JDK自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化,GC变化等。
5.常用的JVM的参数有哪些?
- -Xms2g:初始化堆大小为2g。
- -Xmx2g:堆最大内存为2g。
- -XX:NewRatio=4:设置年轻代和老年代的内存比例为1:4。
- -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2。
- -XX:+UseSerialGC:指定使用 Serial + Serial Old 垃圾回收器组合。
- -XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合。
- -XX:+UseParallelGC:指定使用 Parallel Scavenge + Serial Old 垃圾回收器组合。
- -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合。
- -XX:ParallelGCThreads:设置用于垃圾回收的线程数量。
- -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合。
- -XX:ParallelCMSThreads:设置CMS收集器的线程数量。
- -XX:+UseG1GC:指定使用 G1 垃圾回收器。
- -XX:+PrintGC:开启打印 GC 信息。
- -XX:+PrintGCDetails:打印 GC 详细信息。