[关闭]
@boothsun 2018-03-13T08:12:42.000000Z 字数 7388 阅读 1163

JVM常见面试题

面试题


Java内存区域划分

P2SHM

  1. 程序计数器:

    • 当前线程所执行的字节码的行号指示器。
    • 线程私有区域,声明周期与线程相同。
    • 作用是标识当前线程执行到了那条指令,分支、循环、跳转、异常处理、线程切换恢复等功能都需要依赖这个计数器来完成。
    • 所占空间小且固定,故不会出现OOM。
  2. Java虚拟机栈

    • Java栈是Java方法执行过程的内存模型。每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。线程当前执行的方法所对应的栈帧就必定位于Java栈的顶部。
    • Java虚拟机栈也是线程私有的,它的生命周期与线程相同。
    • 在Java虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
  3. 本地方法栈

    • 与Java虚拟机栈的作用和原理非常相似。但是是线程共有的区域。
    • 本地方法栈的作用是为了执行本地方法(Native Method)服务的。
    • 与虚拟机栈一样,本地方法栈区也会抛出StackOverflowErrorOutOfMemoryError异常。
  4. Java堆(Heap)

    • Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例和数组,几乎所有的对象实例都在这里分配内存。
    • 在Heap中分配一定的内存来保存对象实例,实际上也只是保存对象的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(以栈帧的形式保存在Stack中),在Heap中分配一定的内存保存对象实例。而对象实例在Heap中分配好以后,需要在Stack中保存一个4字节的Heap内存地址,用来定位该对象实例在Heap中的位置,便于找到该对象实例。(这里存疑
    • 由于现在收集器基本都是采用的分代收集算法,所以Java堆还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。不过,无论如何划分,都与存放内容无关,无论哪个区域,存放的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
    • 如果在堆中没有内存空间完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
  5. 方法区

    • 方法区是各个线程共享的内存区域,也是在虚拟机启动时创建。
    • 作用:存储已被虚拟机加载的类结构信息,例如Class文件中的常量池、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
    • 这个区域的内存回收目标主要是针对于常量池的回收和对类型的卸载。
    • 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
  6. 运行时常量池

    • 运行时常量池是方法区的一部分。在Class文件中存在一个Class常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。也就是说,Class文件中有一个常量池信息,用于存放此Class文件涉及到的各种字面量和符号引用,当Class文件被加载到方法区时,会将该Class里的常量池保存的内容加载到方法的运行时常量池中。
    • 当运行时常量池无法再申请到内存时也会抛出OutOfMemoryError异常。
  7. 直接内存
    直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

    在JDK1.4中新加入了NIO(New Input/Output)类,引入了一张基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

JVM运行原理 例子(使用new产生的效果)

运行时常量池与String类

如何判定一个对象是活着还是死亡?

引用计算法:

给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。

缺点:很难解决对象之间的相互循环引用的问题。

根搜索算法

通过一系列的名为“RC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是GC Roots到这个对象不可达)时,则证明此对象是不可用的。

在Java语言里,可作为GC Roots的对象包括以下几种:

选择这些对象作为GC Roots的根据是 因为这些对象明显或者说一定是存活的对象。比如 本地变量表中引用的对象就一定是当前正在执行的方法栈中需要用到的对象。同样方法区中的常量也好,静态变量也好,都是正在存活的对象,不处于存活状态 早就被类加载器 卸载了。

非强引用关系的回收策略

在JDK1.2之后,Java将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。

垃圾回收的过程

两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列之中,并在稍后有一条由虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指由虚拟机去触发这个方法,但是并不承诺等待它运行结束。这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己 —— 只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或对象的成员变量,那在第二次标记时它将被移除“即将回收”的集合;如果对象这时候还没有逃脱,那它就真的离死不远了。

注意:任何一个对象的finalize方法只会被调用一次

java中垃圾收集的方法有哪些?

标记-清除算法

  1. 标记阶段:根据根搜索算法标记出所有需要被回收的对象。
  2. 清除阶段:回收被标记的对象所占用的空间。

缺点:

复制算法

为了解决标记清除算法容易产生内存碎片的问题,复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

优点:解决了内存碎片的问题。
缺点:内存空间利用率很低。

标记-整理算法

为了解决复制算法的内存空间利用率低的问题,提出了标记-整理算法。该算法标记阶段和标记-清除一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

分代回收算法

分代回收算法只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法进行回收。

这里可以提一下分配担保机制:

如果to Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。

常见的垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。

4大类,7小类

4大类:

串行垃圾收集器

缺点:必须暂停其他所有的工作线程
优点:简单高效

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

常见的就是Serial收集器。

并行垃圾收集器

并行收集器是工作在新生代的垃圾收集器,它只简单地将串行收集器多线程化。它的回收策略、算法以及参数和串行回收器一样。

并行回收器也是独占式的回收器,在收集过程中,应用程序会全部暂停。但由于并行回收器使用多线程进行垃圾回收,因此,在并发能力比较强的CPU上,它产生的停顿时间要短于串行收集器,而在单CPU或者并发能力较弱的系统中,并行回收器的效果不会比串行回收器好,由于多线程的压力,它的实际表现很可能比串行回收器差。

并发标记清除垃圾收集器(CMS收集器)

CMS收集器的主要关注点是尽可能降低停顿时间,CMS收集器是基于“标记 - 清除”算法实现的。它的运行过程可以分为4个步骤:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。

image.png-75.2kB

缺点:

  1. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量 + 3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程最多占用不超过25%的CPU资源。但是当CPU不足4个时,那么CMS对用户程序的影响就可能变得很大,如果CPU负载本来就比较大的时候,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,这也很让人受不了。
  2. CMS收集器无法处理“浮动垃圾”,所谓“浮动垃圾”就是在标记过程之后,产生的垃圾。
  3. 因为在清除过程中应用程序没有中断,所以在 CMS 回收过程中,还应该确保应用程序有足够的内存可用。因此,CMS 收集器不会等待堆内存饱和时才进行垃圾回收,而是当前堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在 CMS 工作过程中依然有足够的空间支持应用程序运行。默认阈值是68%。
  4. 由于标记-清除算法 可能会产生大量的空间碎片。

G1收集器

G1 收集器的目标是作为一款服务器的垃圾收集器,因此,它在吞吐量和停顿控制上,预期要优于 CMS 收集器。

与 CMS 收集器相比,G1 收集器是基于标记-压缩算法的。因此,它不会产生空间碎片,也没有必要在收集完成后,进行一次独占式的碎片整理工作。G1 收集器还可以进行非常精确的停顿控制。它可以让开发人员指定当停顿时长为 M 时,垃圾回收时间不超过 N。使用参数-XX:+UnlockExperimentalVMOptions –XX:+UseG1GC 来启用 G1 回收器,设置 G1 回收器的目标停顿时间:-XX:MaxGCPauseMills=20,-XX:GCPauseIntervalMills=200。

JVM 垃圾回收器工作原理及使用实例介绍

常见JVM参数

使用指定的垃圾收集器

配置 描述
-XX:+UserSerialGC 串行垃圾收集器
-XX:+UserParrallelGC 并行垃圾收集器
-XX:+UseConcMarkSweepGC 并发标记扫描垃圾回收器
-XX:ParallelCMSThreads= 并发标记扫描垃圾回收器 =为使用的线程数量
-XX:+UseG1GC G1垃圾回收器

GC的优化配置

配置 描述
-Xms 初始化堆内存大小
-Xmx 堆内存最大值
-Xmn 新生代大小
-XX:PermSize 初始化永久代大小
-XX:MaxPermSize 永久代最大容量

Java 堆内存划分

在Java中,堆被划分成两个不同的区域:新生代(Young)、老年代(Old)。新生代(Young)又被划分成三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使JMV能够更好的管理堆内存中的对象,包括内存的分配以及回收。

新生代(Young)与老年代(Old)的比例为1:2
Eden:from:to = 8:1:1

  1. 对象优先在Eden分配。

  2. 大对象直接进入老年代:这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存拷贝。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。

  3. 长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后任然存活,并且
    能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄加1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁)时,就会被晋升到老年代。也可通过JVM参数设置。

  4. 动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等待MaxTenuringThreshold中要求的年龄。

  5. 空间分配担保:
    在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。

    当出现大量对象Minor GC后仍然存活的情况时(最极端就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。老年代也无法容纳(担保失败),则会进行Full GC。

其他参见: Java堆内存管理

JDK命令行工具

  1. jstat:

    • 作用:对Java应用程序的资源和性能进行实时监控的命令行工具,主要包括GC情况和Heap Size资源使用情况。
    • 使用:jstat -gc 3291 500 5 查看堆内存容量、已使用空间、GC时间
    • 使用:jstat -gcutil 3291 500 5 与GC基本一致,但主要关注已使用和总空间百分比
  2. jstack:查看堆栈快照信息

    • 作用:它可以用于生成虚拟机当前时刻的线程快照(一般称为threaddump和javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,所以可以使用jstack分析线程间死锁、死循环、请求外部资源导致的长时间等待都是导致线程长时间停顿的常见原因。
    • 使用:jstack -F 进程ID 强制输出线程堆栈信息。
    • 使用jstack -l 进行Id 输出堆栈信息和锁的附加信息。
  3. jmap:主要可用于打印Java进程的内存映射和堆内存的细节。

    • 作用:主要是用来检查内存泄漏,一些严重影响性能的大对象,检查系统中什么对象创建的最多,分析各种对象占用的大小等。
    • 常用命令:jmap -heap 31846或者jmap -histo 31846

常见故障解决

CPU过高

频繁FULL GC

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注