面试整理——JVM

JVM

一. 综合

问:jre、jdk、jvm的关系 ?

  • jdk是最小的开发环境,由jre + java工具组成。
  • jre是java运行的最小环境,由jvm + 核心类库组成。
  • jvm是虚拟机,是java字节码运行的容器,如果只有jvm是无法运行java的,因为缺少了核心类库。

问:STW?

STW,即Stop the World,指Gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉。如可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。

被STW中断的应用程序线程会在完成GC之后恢复,频繁的中断会让用户感觉像是网速不快造成的电影卡顿一样,所以我们要减少STW的发生。STW事件和采用哪款GC无关,所有的GC都有这个事件。哪怕是G1也不能完全避免Stop一the一world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
开发中采用 System.gc() 会导致STW的发生。

分析工作必须在一个能确保一致性的快照中进行,一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上,如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。

二. 内存区域

问:简单介绍一下JVM内存区域?

  1. 堆{对象,静态变量,共享}
  2. 方法区{ 存放类信息,常量池,共享}(java8移除了永久代(PermGen),替换为元空间(Metaspace))
  3. 虚拟机栈{线程执行方法的时候内部存局部变量会存堆中对象的地址等等数据}
  4. 本地方法栈{存放各种native方法的局部变量表之类的信息}
  5. 程序计数器{记录当前线程执行到哪一条字节码指令位置}

问:堆空间分哪些部分?讲解新生代和老年代?以及如何设置各个部分?

堆空间包括:采用分代收集算法

  • 新生代:对象一般会先放置在新生代,经历一次Minor GC后进入Survivor区增加一岁,超过年龄阈值就移到老年代。
    • Eden区:新对象的出生地。
    • From Survivor区
    • To Survivor区:Minor GC时将另外两区存活的对象复制到此区,同时年龄+1,清空另外两区对象后,将To区和From区互换。
  • 老年代:大对象直接进入老年代,长期存活对象陆续进入老年代。
  • 运行时常量池:1.7以后迁移到堆中

配置参数:

  • -Xms :初始堆大小。
  • -Xmx :最大堆大小。

问:JDK 1.7到JDK 1.8虚拟机发生的变化?

1.7的永久代/方法区,在1.8中被移除,取而代之的是Metaspace(元空间),Metaspace不位于虚拟机内,而是使用本地内存,所以理论上最大可用空间是系统整个内存空间,将元数据剥离出Perm Gen提高GC效率,字符串与类元数据分开提升了独立性。

方法区太小会容易导致内存溢出,太多又会过于占用虚拟机空间,所以移到本地内存后不影响虚拟机所占用的内存。

问:Eden区和Survivor区的比例,为什么是这个比例,工作过程 ?

一个Eden区和一个Survivor区的比例是 8 : 1,即新生代可用空间是总空间的9 / 10。据说有95%的对象都存活期较短,所以Eden区应远大于Survivor区,具体比例应该取决于工程验证和数据统计的结果。

工作过程:初始对象一般诞生于Eden区,除非对象比较大如字符串或数组。当Eden区域不够时会发生Minor GC,根据垃圾回收算法回收部分对象,将当前存活对象(Eden区和From Survivor区剩余对象)转移到To Survivor区,年龄+1,并将To区和From区互换。如果Survivor区剩余空间不足,还要向老年代做分配担保,并转移到老年代。

问:什么是栈帧?栈帧存储了什么?

虚拟机栈中的栈帧是方法执行的内存模型,每个方法执行时会创建一个栈帧,栈帧会存放局部变量表、操作数栈、动态链接、方法出口等。

问:对象4种引用 ?Java 中都有哪些引用类型?

  1. 强引用(内存泄露主因):不会被回收。
  2. 软引用(只有软引用的话,空间不足将被回收):内存即将溢出时会被回收,适合缓存用。
  3. 弱引用(只,GC会回收):下一次GC时就会被回收。
  4. 虚引用(用于跟踪GC状态)用于管理堆外内存:不影响回收,但会在回收时通知系统。

问:对象的构成 ?对象的内存布局,涉及到锁的部分 ?

一个对象分为3个区域:对象头、实例数据、对齐填充

  1. 对象头:主要是包括两部分:
    • 运行时数据(Mark Word):存储自身的运行时数据比如hash码,分代年龄,锁标记等(但是不是绝对,锁状态如果是偏向锁,轻量级锁,是没有hash码的)
    • 指向类的元数据指针。
    • 还有可能存在第三部分,那就是数组类型,会多一块记录数组的长度(因为数组的长度是jvm判断不出来的,jvm只有元数据信息)
  2. 实例数据:会根据虚拟机分配策略来定,分配策略中,会把相同大小的类型放在一起,并按照定义顺序排列。
  3. 对齐填充:这个意义不是很大,主要在虚拟机规范中对象必须是8字节的整数,所以当对象不满足这个情况时,就会用占位符填充。

问:如果判断一个对象是否存活 ?哪些对象可被视为GC Roots?

一般判断对象是否存活有两种算法,一种是引用计数,另外一种是可达性分析。

  • 引用计数算法:通过计数器统计被引用次数,但难以处理循环依赖问题。
  • 可达性分析:判断GC Roots作为根节点,是否存在可到达对象的引用链。

可被视为GC Roots:

  • 虚拟机栈中引用的对象。
  • 方法区中静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNC引用的对象。

问: JVM参数主要有几种分类?

包括:

  • 标准参数:-开头
  • 非标准参数:-X开头
  • 不稳定参数:-XX开头

问:Java中会存在内存泄漏吗,简述一下?

会存在,首先内存泄漏即不再会被使用的对象等一直占据内存空间。

JVM通过垃圾回收机制来自动清除不再有用的内存,但仍有多种情况会导致内存泄漏。

长生命周期对象强引用短生命周期对象。

问:Java虚拟机是如何判定两个Java类是相同的?

  1. 类的全限定名相等。
  2. 类加载器相等(不同的类加载器加载相同字节代码也是两个不同的类实例)。

问:java是根据什么来执行可达性分析的 ?

根据GC ROOTS,GC ROOTS可以的对象有:

  • 虚拟机栈中的引用对象
  • 方法区的类变量的引用
  • 方法区中的常量引用
  • 本地方法栈中的对象引用

问:堆外内存的优缺点?

Ehcache中的一些版本,各种 NIO 框架,Dubbo,Memcache 等中会用到,NIO包下ByteBuffer来创建堆外内存,其实就是不受JVM控制的内存。

NIO直接通过Native函数库分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

优点:

  • 减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作。
  • 加快了复制的速度。因为堆内在 flush 到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了复制这项工作。 可以扩展至更大的内存空间。比如超过 1TB 甚至比主存还大的空间。

缺点:

  • 难以控制,内存泄漏很难排查;
  • 很难存储较复杂对象。

堆外内存难以控制,如果内存泄漏,那么很难排查,通过-XX:MaxDirectMemerySize来指定,当达到阈值的时候,调用system.gc来进行一次full gc 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合 jstat查看内存回收概况,实时查看各个分区的分配回收情况, jmap查看内存栈,查看内存中对象占用大小, jstack查看线程栈,死锁,性能瓶颈

问:说一下JVM的线程模型?这些区域都分别是干啥用的?java线程模型和jvm线程模型注意区分 ?

Java使用的是一对一线程模型,所以它的一个线程对应于一个内核线程,调度完全交给操作系统来处理

JVM内部的主要线程分为:

  • NamedThread:支持命名的非Java线程
    • VMThread:VM原始线程,用于执行VM操作
    • ConcurrentGCThread:并发GC线程
    • WorkerThread:工作线程
      • GangWorker:一组线程,类似线程池
      • GCTaskThread:GC任务线程
  • JavaThread:C++层面的Java线程实现
    • 各种子类,如:编译器线程,服务线程
  • WatcherThread:监视器线程,用于模拟计时器中断

在JVM内部产生一个线程的方法有两种:

  • 调用 java.lang.Threadstart() 方法。
  • 通过 JNI attach到一个已经存在的本地线程。

java.lang.Thread 启动时会分别创建一个相关联的JavaThread和OSThread对象,最终创建本地线程。

TODO

问:字符串常量池相关 ?

问:jdk 8 去掉方法区用元数据代替,是为什么 ?


三. 垃圾回收

问:GC 是什么?为什么要有 GC?

问:导致fullGC的原因 ?

(1):老年代空间不足

(2):永久代(方法区)空间不足

(3):显式调用system.gc()

问:对象什么时候可以被垃圾回收?

问:各种GC执行频繁的影响?

  • Minor GC,每当发生一次垃圾收集的动作,所有的用户线程都必须跑到最近的一个安全点(SafePoint),然后挂起线程等待垃圾回收。这样过于频繁的GC就会导致很多没有必要的安全点检测、线程挂起及恢复操作。如果是新生代GC频繁发生,是由于虚拟机分配给新生代的空间太小而导致的,使用 -Xmn 参数调整新生代的大小。
  • Full GC,比较耗时,并造成程序卡顿。

问:如何设置参数生成GC日志?

问:JVM七种垃圾收集器 ?

(1): Serial 收集器 复制算法,单线程,新生代)

(2): ParNew 收集器(复制算法,多线程,新生代)

(3): Parallel Scavenge 收集器(多线程,复制算法,新生代,高吞吐量)

(4):Serial Old 收集器(标记-整理算法,老年代)

(5):Parallel Old 收集器(标记-整理算法,老年代,注重吞吐量的场景下,jdk8默认采用 Parallel Scavenge + Parallel Old 的组合)

(6):CMS 收集器(标记-清除算法,老年代,垃圾回收线程几乎能做到与用户线程同时工作,吞吐量低,内存碎片)以牺牲吞吐量为代价来获得最短回收停顿时间-XX:+UseConcMarkSweepGC jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.9 默认垃圾收集器G1

问:使用场景 ?

(1):应用程序对停顿比较敏感

(2):在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS

问:cms垃圾回收过程 ?

问:GC Roots有以下几种 ?

1:系统类加载器加载的对象

2:处于激活状态的线程

3:JNI栈中的对象

4:正在被用于同步的各种锁对象

5:JVM自身持有的对象,比如系统类加载器等。

(2):并发标记(三色标记算法) 三色标记算法处理并发标记出现对象引用变化情况: 黑:自己+子对象标记完成 灰:自己完成,子对象未完成 白:未标记; 并发标记 黑->灰->白 重新标记 灰->白引用消失,黑引用指向->白,导致白漏标 cms处理办法是incremental update方案 (增量更新)把黑色变成灰色 多线程下并发标记依旧会产生漏标问题,所以cms必须remark一遍(jdk1.9以后不用cms了)

问:G1 处理方案 ?

SATB(snapshot at the begining)把白放入栈中,标记过程是和应用程序并发运行的(不需要Stop-The-World) 这种方式会造成某些是垃圾的对象也被当做是存活的,所以G1会使得占用的内存被实际需要的内存大。不过下一次就回收了 ZGC 处理方案: 颜色指针(color pointers) 2*42方=4T

(3):重新标记(stw)

(4)并发清理

备注:重新标记是防止标记成垃圾之后,对象被引用

(5):G1 收集器(新生代 + 老年代,在多 CPU 和大内存的场景下有很好的性能) G1在java9 便是默认的垃圾收集器,是cms 的替代者 逻辑分代,用分区(region)的思想(默认分2048份) 还是有stw 为解决CMS算法产生空间碎片HotSpot提供垃圾收集器,通过-XX:+UseG1GC来启用

问:G1中提供了三种模式垃圾回收模式 ?

(1):young gc(eden region被耗尽无法申请内存时,就会触发)

(2):mixed gc(当老年代大小占整个堆大小百分比达到该阈值时,会触发)

(3):full gc(对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发)

(8):ZGC和shenandoah (oracle产收费) no stw

问:arthas 监控工具 ?

(1):dashboard命令查看总体jvm运行情况

(2):jvm显示jvm详细信息

(3):thread 显示jvm里面所有线程信息(类似于jstack) 查看死锁线程命令thread -b

(4):sc * 显示所有类(search class)

(5):trace 跟踪方法

问:定位频繁full GC,堆内存满 oom ?

第一步:jps获取进程号 第二步:jmap -histo pid | head -20 得知有个对象在不断创建 备注:jmap如果线上服务器堆内存特别大,,会卡死需堆转存(一般会说在测试环境压测,导出转存) -XX:+HeapDumpOnOutOfMemoryError或jmap -dumpLformat=b,file=xxx pid 转出文件进行分析 (arthas没有实现jmap命令)heapdump –live /xxx/xx.hprof导出文件

问:G1垃圾回收器(重点) ?

回收过程 (1):young gc(年轻代回收)–当年轻代的Eden区用尽时–stw 第一阶段,扫描根。 根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等 第二阶段,更新RS(Remembered Sets)。 处理dirty card queue中的card,更新RS。此阶段完成后,RS可以准确的反映老年代对所在的内存分段中对象的引用 第三阶段,处理RS。 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。 第四阶段,复制对象。 此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段 第五阶段,处理引用。 处理Soft,Weak,Phantom,Final,JNI Weak 等引用。

(2):concrruent marking(老年代并发标记) 当堆内存使用达到一定值(默认45%)时,不需要Stop-The-World,在并发标记前先进行一次young gc

(3):混合回收(mixed gc) 并发标记过程结束以后,紧跟着就会开始混合回收过程。混合回收的意思是年轻代和老年代会同时被回收

(4):Full GC? Full GC是指上述方式不能正常工作,G1会停止应用程序的执行,使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。要避免Full GC的发生,一旦发生需要进行调整。

问:什么时候发生Full GC呢 ?

比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到full gc,这种情况可以通过增大内存解决

尽管G1堆内存仍然是分代的,但是同一个代的内存不再采用连续的内存结构

年轻代分为Eden和Survivor两个区,老年代分为Old和Humongous两个区

新分配的对象会被分配到Eden区的内存分段上

Humongous区用于保存大对象,如果一个对象占用的空间超过内存分段Region的一半;

如果对象的大小超过一个甚至几个分段的大小,则对象会分配在物理连续的多个Humongous分段上。

Humongous对象因为占用内存较大并且连续会被优先回收

为了在回收单个内存分段的时候不必对整个堆内存的对象进行扫描(单个内存分段中的对象可能被其他内存分段中的对象引用)引入了RS数据结构。RS使得G1可以在年轻代回收的时候不必去扫描老年代的对象,从而提高了性能。每一个内存分段都对应一个RS,RS保存了来自其他分段内的对象对于此分段的引用

JVM会对应用程序的每一个引用赋值语句object.field=object进行记录和处理,把引用关系更新到RS中。但是这个RS的更新并不是实时的。G1维护了一个Dirty Card Queue

问:那为什么不在引用赋值语句处直接更新RS呢 ?

这是为了性能的需要,使用队列性能会好很多。

问:线程本地分配缓冲区(TLAB: Thread Local Allocation Buffer) ?

栈上分配->tlab->堆上分配 由于堆内存是应用程序共享的,应用程序的多个线程在分配内存的时候需要加锁以进行同步。为了避免加锁,提高性能每一个应用程序的线程会被分配一个TLAB。TLAB中的内存来自于G1年轻代中的内存分段。当对象不是Humongous对象,TLAB也能装的下的时候,对象会被优先分配于创建此对象的线程的TLAB中。这样分配会很快,因为TLAB隶属于线程,所以不需要加锁

问:PLAB: Promotion Thread Local Allocation Buffer ?

G1会在年轻代回收过程中把Eden区中的对象复制(“提升”)到Survivor区中,Survivor区中的对象复制到Old区中。G1的回收过程是多线程执行的,为了避免多个线程往同一个内存分段进行复制,那么复制的过程也需要加锁。为了避免加锁,G1的每个线程都关联了一个PLAB,这样就不需要进行加锁了

问:OOM问题定位方法 ?

(1):jmap -heap 10765如上图,可以查看新生代,老生代堆内存的分配大小以及使用情况;

(2):jstat 查看GC收集情况

(3):jmap -dump:live,format=b,file=到本地

(4):通过MAT工具打开分析

问:既然jvm有垃圾回收,为什么还会出现内存溢出的情况?

问:简单介绍一下GC垃圾回收算法?说说GC的过程

垃圾回收。如何通知gc回收?(System.gc)除了这个方法还有吗?(将不用的对象赋值为null)

问:常见的垃圾回收器?

问:Java GC机制?GC Roots有哪些?

问:强制young gc会有什么问题?

问:知道G1么?回收过程是怎么样的?

你提到的Remember Set底层是怎么实现的?CMS GC有什么问题?怎么避免产生浮动垃圾?

问:Full GC 发生的条件、怎么设置永久代和堆的大小、怎么减少 Full GC 、JVM 调优?

问:GC 常见算法,CMS 以及 G1 的垃圾回收过程,CMS 的各个阶段哪两个是 Stop the world 的,CMS 会不会产生碎片,G1 的优势 ?

问:标记清除、复制和标记整理算法的理解以及优缺点 ?

问:JVM 如何判断一个对象是否该被 GC,可以视为 root 的都有哪几种类型 ?

问:强软弱虚引用的区别以及 GC 对他们执行怎样的操作 ?

问:Java 是否可以 GC 直接内存 ?

问:GC 优化的步骤 ?

问:有过GC调优的经历么?

问:CMS 和 G1 收集过程 ?

问:java new 一个对象的时候,什么情况下会发生 GC ?

问:如果新生代和老年代没有满呢 ?


四. 类加载

问:JVM 类加载顺序 ?

(1):加载 获取类的二进制字节流,将其静态存储结构转化为方法区的运行时数据结构

(2):校验 文件格式验证,元数据验证,字节码验证,符号引用验证

(3):准备 在方法区中对类的static变量分配内存并设置类变量数据类型默认的初始值,不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中

(4):解析 将常量池内的符号引用替换为直接引用的过程

(5):初始化 为类的静态变量赋予正确的初始值(Java代码中被显式地赋予的值)

问:JVM三种类加载器 ?

(1):启动类加载器(home) 加载jvm核心类库,如java.lang.*等

(2):扩展类加载器(ext), 父加载器为启动类加载器,从jre/lib/ext下加载类库

(3):应用程序类加载器(用户classpath路径) 父加载器为扩展类加载器,从环境变量中加载类

问:双亲委派机制 ?

(1):类加载器收到类加载的请求

(2):把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器

(3):启动器加载器检查能不能加载,能就加载(结束);否则,抛出异常,通知子加载器进行加载

(4):保障类的唯一性和安全性以及保证JDK核心类的优先加载

问:双亲委派模型有啥作用 ?

  • 保证java基础类在不同的环境还是同一个Class对象,避免出现了自定义类覆盖基础类的情况,导致出现安全问题。

  • 还可以避免类的重复加载。

问:如何打破双亲委派模型 ?

(1):自定义类加载器,继承ClassLoader类重写loadClass方法;

(2):SPI

问:tomcat是如何打破双亲委派模型 ?

tomcat有着特殊性,它需要容纳多个应用,需要做到应用级别的隔离,而且需要减少重复性加载,所以划分为:/common 容器和应用共享的类信息,/server容器本身的类信息,/share应用通用的类信息,/WEB-INF/lib应用级别的类信息。整体可以分为:boostrapClassLoader->ExtensionClassLoader->ApplicationClassLoader->CommonClassLoader->CatalinaClassLoader(容器本身的加载器)/ShareClassLoader(共享的)->WebAppClassLoader。虽然第一眼是满足双亲委派模型的,但是不是的,因为双亲委派模型是要先提交给父类装载,而tomcat是优先判断是否是自己负责的文件位置,进行加载的。

问:SPI: (Service Provider interface) ?

(1):服务提供接口(服务发现机制):

(2):通过加载ClassPath下META_INF/services,自动加载文件里所定义的类

(3):通过ServiceLoader.load/Service.providers方法通过反射拿到实现类的实例

问:SPI应用 ?

(1):应用于JDBC获取数据库驱动连接过程就是应用这一机制

(2):apache最早提供的common-logging只有接口.没有实现..发现日志的提供商通过SPI来具体找到日志提供商实现类

问:双亲委派机制缺陷 ?

(1):双亲委派核心是越基础的类由越上层的加载器进行加载, 基础的类总是作为被调用代码调用的API,无法实现基础类调用用户的代码….

(2):JNDI服务它的代码由启动类加载器去加载,但是他需要调独立厂商实现的应用程序,如何解决? 线程上下文件类加载器(Thread Context ClassLoader), JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC

问:说下类加载的过程?

问:什么时候需要自定义类加载器?

问:Class.forName 是否会初始化类?

这里我智障了,振振有词地说说不会初始化

问:static 块会执行几次?怎么让它执行第二次?

问:类加载的过程,以及双亲委派机制、自定义类加载器 ?

问:Java为什么要设计双亲委派模型?


五. 调优

问:Java内存泄漏问题如何进行定位?

通过一些JDK提供的命令行工具,如jmap、jstack等:

  • 通过jmap获取堆转储快照,dump文件,查看JVM各个区域的使用情况。
  • 通过jstack获取线程快照,查看线程堆栈,查看有没有哪些现场阻塞或出现死锁。
  • 通过jstat命令来查看垃圾回收情况,尤其是fullGC。
  • 通过JVisualVM等工具来分析,如Arthas等。
  • 如果频繁发生FullGC又一直没出现OOM,表示FullGC实际回收了

问:StackOverflow异常有没有遇到过?一般你猜测会在什么情况下被触发?

问:常用的 JVM 调优参数 ?

问:当出现了内存泄漏或内存溢出,怎么排错 ?

问:谈谈工作中技术优化过哪些?JVM、MySQL、代码等都谈谈?自己做过哪些调优?JVM调优、数据库调优都行