Java面试题17 JVM
自己尝试通过打字来回答一些网上常见的面试题,答案仅代表我自己的观点
194. 说一下 jvm 的主要组成部分?及其作用?
195. 说一下 jvm 运行时数据区?
JVM的运行时数据区有这么几块:
- 程序计数器:每个栈有一个,用于记录栈中下一条要执行的指令地址
- 虚拟机栈:每个线程有一个,生命周期跟随线程,用于实现方法调用,记录线程的方法调用链,以及方法中的局部数据啥的
- 本地方法栈:每个线程有一个,和虚拟机栈一样,不过它是用来搞本地方法的,非Java方法
- 堆:堆是在线程间共享的内存区域,生命周期跟随Java程序,一共就一个,各种对象数据会记录在这里
- 方法区:运行时常量池、生命周期跟随Java程序,类信息、方法的代码
- 直接内存:向程序员提供的,并非通过JVM控制的直接内存区域,用于实现一些性能敏感的程序。
196. 说一下堆栈的区别?
好,面试官,你说下堆栈哪一样?
行,想赚钱,该低头就低头,我说。
- 栈线程私有,线程间不能共同访问,主要用于跟踪线程中方法调用的过程,记录方法中的一些局部变量。栈是可以利用寄存器来保存方法局部变量的,快极了嘿!
- 堆在一个程序的线程间共享,可以看作对物理内存的抽象,它在Java中主要用于保存对象啥的。
- 不仅仅是在Java中,在操作系统中的栈也是,调用完之后,局部变量就没用了,会被扔掉。而在传统编程语言中,堆中数据不会被自动释放的,你得手动的清除它们,而在Java这种自动内存管理的语言中,垃圾回收器会针对堆内存中的无用数据进行回收。
198. 什么是双亲委派模型?
这是Java官方推荐的实现ClassLoader时要遵循的模型。即一个ClassLoader应该先把它接收到的类加载请求委托给父类,如果上面的所有ClassLoader都遵循双亲委派模型,那么最终类加载请求会传递到根ClassLoader。如果父类不能成功加载类,子类再自行加载。
双亲委派模型被推崇的原因是,它可以避免系统中出现两个相同的类,但它们却不同。有点玄乎~ 怎么说呢?假设你也编写了一个java.lang.Object
并且尝试用类加载器加载,如果类加载器遵循双亲委派模型,它的祖先会发现java.lang.Object
加载过了,从而保证系统中只有一个java.lang.Object
。
而近些年来,双亲委派模型不断地被打破,一些在Java中实现模块化的框架打破双亲委派模型来实现模块的加载卸载,Tomcat也打破了双亲委派模型来实现Web应用的动态部署。
199. 说一下类加载的执行过程?
类加载的执行过程为:加载->链接->初始化
加载即通过名字来寻找Java类的二进制表示的过程,也就是ClassLoader该做的事。Java虚拟机规范并没有定义该从什么地方以及如何寻找类的二进制表示,可以是Class文件、Jar包、网络甚至动态生成。
链接可以分为三步,验证、准备和解析。验证就是对字节码的正确性进行验证,这也是避免恶意代码破坏虚拟机内部。准备阶段是为了初始化类中的静态变量,为其分配空间,设置初始的零值。解析阶段并不一定要与链接阶段一起发生,所以它并不一定必须在准备阶段和初始化阶段之间进行,解析阶段主要做的就是将二进制文件中的静态符号引用替换成直接引用,也就是说将静态常量值中的那些东西实际分配到虚拟机的内存布局中(一般是元空间或者说永久代中)。
初始化,就是执行类的初始化方法,也就是<clinit>
,这里才执行static
变量的赋值,静态代码块的执行。
200. 怎么判断对象是否可以被回收?
- 引用技术:每个对象有一个引用记录它被引用多少次,有一个引用+1次,只要引用计数器不为0就代表该对象还有引用,不能回收。引用计数无法解决循环引用的问题。
- GCRoots 可达性分析:这种算法的核心就是维护引用链树,GC Roots是能作为引用链树根部的一批对象,比如静态对象啥的。如果一个对象从所有GC Roots向下查找都没有找到树中有任何该对象的引用,那该对象就可以被回收。
201. java 中都有哪些引用类型?
- 强引用
- 软引用
- 弱引用
- 虚引用
对于它们都是干啥的,不清楚。
202. 说一下 jvm 有哪些垃圾回收算法?
下面是自动内存管理领域发展这么多年总结出来的一些垃圾回收算法:
- 标记/清除算法:标记可清除对象(或不可清除对象),清理掉它们(或者清理掉没被标记的)。该算法的优点是不需要复制,效率高些,缺点是内存碎片严重。
- 标记/整理算法:标记不可清除对象,将它们移动到内存的一端。然后清除掉所有后面的东西。该方法要复制移动对象,效率稍低,但不会产生内存碎片。
- 标记/复制算法:将内存区域分成两块,标记无需清除的对象,移动到内存区域的另一半,清除掉原来内存区域。有复制移动,效率较低,而且会有一半的空间浪费掉,不会产生内存碎片。
- 分代算法:分代算法基于一些经过验证的规律(假说)来设计,是将内存分成新生代和老年代,在新生代和老年代之间分别采用适合它们的内存管理方式。
我们没法说哪种方法好,你说标记清除效率高吧,如果内存碎片过多导致新对象没有空间分配的话,那么接下来需要对堆内存进行一次整理,这个整理也是非常耗时的。
JVM中不同的垃圾回收器使用了不同的垃圾回收算法,比如CMS使用并发的标记清除算法,Parallel Scavenge使用了标记复制算法,而G1则是使用标记整理和标记复制混合的算法。
203. 说一下 jvm 有哪些垃圾回收器?
Serial系列
Serial和SerialOld分别是这个系列面向新生代和老年代的垃圾收集器,它们的特点就是单线程,而且,一旦开始工作,整个工作过程必须中断用户线程的执行以保证垃圾回收的正确性。
听起来很无法接受,但由于其简单性,它无需维护复杂的数据结构所以内存占用很小,它的单线程工作模式让它无需处理多个垃圾回收线程之间的并发安全,所以,在嵌入式系统、微服务组件、移动智能设备上,它还是有用武之地的。
ParNew
Serial的多线程版本,注意,不是SerialOld的,所以是个新生代垃圾回收器。
除了使用多个线程对新生代进行清理,和Serial并无什么本质的差别,工作过程中也必须中断用户线程。
只有ParNew才能和后面出现的老年代垃圾收集器CMS合作使用。
Parallel系列
Parallel系列有两个收集器,Parallel Scavenge和Parallel Old,是多线程的新生代和老年代收集器。
Parallel Scavenge和ParNew相比,亮点是可控的停顿时间和吞吐量
-XX:+MaxGCPauseMillis
:指定垃圾回收器的最大停顿时间-XX:+GCTimeRatio
:指定垃圾收集器占用总运行时间的比值
垃圾收集器显然不会魔法,所以,它要尽量满足上面两个参数设定的值就必须做一些牺牲,比如,如果-XX:+UseAdaptiveSizePolicy
参数开着的情况下,它会动态调整新生代的大小,新生代更小了回收起来就更快了,但随之而来的问题就是更多的对象进入老年代,以及新生代的回收越来越频繁,这时吞吐量可能不增反降。
Parallel Old用于和Parallel Scavenge配合,完成新生代以及老年代的共同回收,否则,它就只能用Serial Old了。
CMS
并发的标记清理垃圾回收器,既然是垃圾清理,一定是个老年代回收器喽。
如何区分垃圾回收的并发和并行
前面我们已经看到了很多Par
开头的垃圾回收器,ParNew和Parallel系列,它们都是Parallel
的意思,这个单词的意思是并行,它主要体现了这些垃圾回收器在垃圾回收阶段,多个垃圾回收线程可以利用多个CPU进行并行工作。
而CMS开头的单词是Concurrency
,也就是并发的意思,它的主要含义在于,这个垃圾回收器进行垃圾回收的部分阶段可以和用户线程并行,它们共同交替使用CPU,而不用像之前的垃圾回收器一样完全停止用户线程,虽然在某些阶段仍需完全停止用户线程,但这些阶段占用的时间已经很少了。
现代垃圾回收器一般具有与用户线程并发的能力,比如CMS,而它的名字里没有提到Parallel,并不代表它垃圾回收时只有单个线程,只是说选择了把与用户线程并发这个亮点放在了名字里。
CMS的主要亮点就是,它的垃圾回收阶段可以有相当一部分与用户线程并发。
一般来说,这种垃圾收集器的工作阶段有四个:
- 初始标记:需要停止用户线程的阶段,只是简单的找下GC Roots,对它们进行一下标记,占用时间很短
- 并发标记:无需停止用户线程的阶段,并发的对它处理范围内所有对象进行扫描、标记,占用时间较长
- 重新标记:需要停止用户线程的阶段,占用时间很短。由于与用户线程并发执行,所以并发标记过程中,用户线程有可能改变引用关系,所以需要采用一些机制简单的进行处理,一般有两种算法:
- 增量更新(CMS采用的方式)
- 原始快照
- 并发清除:无需停止用户线程的阶段,将未被标记的对象清除
所以,垃圾回收中两个占用时间比较长的工作都与用户线程并发了,这并不会减少垃圾清理占用的时间,也就是说不会提高系统的吞吐量,但是可以让用户获得更短的停顿时间,然后系统性能在垃圾回收时平缓下降。
G1收集器
G1回收器也是一个可以和用户线程并发的垃圾回收器,所以它的工作阶段和CMS差不多,只不过它采用了原始快照算法来重新标记,而且最后一个清除阶段必须停止用户线程。
G1回收器面对整堆进行清理,也就是Mixed GC,它打破了新生代老年代的限制,采用将内存划分为一些大小相等的Region,对每个Region都通过系统的运行时信息统计它的回收收益比,然后优先选择回收收益大的Region进行清除。
G1也有的Region也分类型,比如新生代、Eden、Survivor、老年代、大对象。
G1收集器的Region设计让它可以一次不清除整个新生代或老年代,一次只清理几个Region,小步快跑。所以它很自然的支持-XX:+MaxGCPauseMillis
,即设置最大停顿时间。用户需要知道,如果它设置的太小,很容易让垃圾收集的速度赶不上产生垃圾的速度。
同时,也是由于分Region设计,这代表它要对每一个Region维护一个记忆集来存储跨Region引用,这大大的增加了内存消耗。不过现在的系统都是为了提高速度,多用一些内存也没什么。
其它收集器
ZGC
204. 详细介绍一下 CMS 垃圾回收器?
略,在上一题
205.新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
新生代:Serial、ParNew、Parallel Scavenge
老年代:Serial Old、Parallel Old、CMS
混合:G1
它们采用的算法不同,新生代由于对象多,对象规模小,大部分对象都朝生夕灭,所以天生不适合采用标记清除算法,较多使用标记复制算法,标记整理算法。而老年代由于对象规模大,大部分对象贯穿整个程序生命周期,所以天生就适合采用标记清除算法,大部分老年代收集器都采用标记清除算法。
206. 简述分代垃圾回收器是怎么工作的?
首先要阐述分代原理,要先了解两个假说
- 90%的对象都是朝生夕灭的
- 熬过越多次垃圾回收的对象往往都会贯穿程序的生命周期
所以,可以把内存分成新生代和老年代,一个对象创建之初在新生代,我们认为新生代的对象都是朝生夕灭的,所以我们用适合它们的垃圾回收算法来回收新生代,也就是——标记复制算法。而当新生代中的对象熬过很多次垃圾回收,证明它可能贯穿整个程序的生命周期,那么将它升级为老年代,采用适合老年代的标记-整理算法。
新生代的标记复制算法还有点讲究。因为标记复制算法会浪费一半的空间嘛,不划算,所以新生代的算法往往提供三个区域,Eden
区、和两个幸存者区S1
和S2
。它们按照8:1:1
的比例分配。
工作情况下,Eden区和一个幸存者区保存对象,另一个幸存者区用于稍后复制不可清除的对象。
207. 说一下 jvm 调优的工具?
就用过jvisualvm
,一个图形化的JVM监控工具,可以监控每个JVM实例的内存快照、线程、死锁、内存、GC等信息
208. 常用的 jvm 调优的参数都有哪些?
不到