JVM高频面试题20道

大家好,我是大彬~

今天给大家分享20道JVM常考的面试题,答案也整理好了,不会的快快查漏补缺~

以下是本期JVM面试题的目录:

讲一下JVM的内存结构?

JVM内存结构分为5大区域,程序计数器虚拟机栈本地方法栈方法区

JVM高频面试题20道

程序计数器

线程私有的,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。程序计数器主要有两个作用:

  1. 当前线程所执行的字节码的行号指示器,通过它实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候能够知道它上次执行的位置。

程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

虚拟机栈

Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表操作数栈动态链接方法出口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。

局部变量表是用于存放方法参数和方法内的局部变量。

每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。

Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。Java 虚拟机栈会出现两种错误:StackOverFlowErrorOutOfMemoryError

可以通过-Xss参数来指定每个线程的虚拟机栈内存大小:

java -Xss2M

本地方法栈

虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Native 方法一般是用其它语言(C、C++等)编写的。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

堆用于存放对象实例,是垃圾收集器管理的主要区域,因此也被称作GC堆。堆可以细分为:新生代(Eden空间、From SurvivorTo Survivor空间)和老年代。

通过 -Xms设定程序启动时占用内存大小,通过-Xmx设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。

java -Xms1M -Xmx2M

方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载

永久代

方法区是 JVM 的规范,而永久代PermGen是方法区的一种实现方式,并且只有 HotSpot 有永久代。对于其他类型的虚拟机,如JRockit没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的场景比较容易出现永久代的内存溢出。

元空间

JDK 1.8 的时候,HotSpot的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是使用直接内存。

说一下堆栈的区别?

  1. 堆的物理地址分配是不连续的,性能较慢;栈的物理地址分配是连续的,性能相对较快。
  2. 堆存放的是对象的实例和数组;栈存放的是局部变量,操作数栈,返回结果等。
  3. 堆是线程共享的;栈是线程私有的。

什么情况下会发生栈溢出?

类文件结构

Class 文件结构如下:

ClassFile {
    u4             magic; //类文件的标志
    u2             minor_version;//小版本号
    u2             major_version;//大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//类的访问标记
    u2             this_class;//当前类的索引
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//字段属性
    field_info     fields[fields_count];//一个类会可以有个字段
    u2             methods_count;//方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}

主要参数如下:

魔数class文件标志。

文件版本:高版本的 Java 虚拟机可以执行低版本编译器生成的类文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的类文件。

常量池:存放字面量和符号引用。字面量类似于 Java 的常量,如字符串,声明为final的常量值等。符号引用包含三类:类和接口的全限定名,方法的名称和描述符,字段的名称和描述符。

访问标志:识别类或者接口的访问信息,比如这个Class是类还是接口,是否为 public 或者 abstract 类型等等。

当前类的索引:类索引用于确定这个类的全限定名。

什么是类加载?类加载的过程?

类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个此类的对象,通过这个对象可以访问到方法区对应的类信息。

JVM高频面试题20道

加载

  1. 通过类的全限定名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的Class对象,作为方法区类信息的访问入口

验证

确保Class文件的字节流中包含的信息符合虚拟机规范,保证在运行后不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证

准备

为类变量分配内存并设置类变量初始值的阶段。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。

初始化

开始执行类中定义的Java代码,初始化阶段是调用类构造器的过程。

什么是双亲委派模型?

一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

JVM高频面试题20道

双亲委派模型的具体实现代码在 java.lang.ClassLoader中,此类的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。源码如下:

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        //把类加载请求委派给父类加载器去完成
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

为什么需要双亲委派模型?

双亲委派模型的好处:可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证。

什么是类加载器,类加载器有哪些?

实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。

主要有一下四种类加载器:

如何判断一个对象是否存活?

对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不再被任何途径引用的对象)。判断对象是否存活有两种方法:引用计数法和可达性分析。

引用计数法

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

这种方法很难解决对象之间相互循环引用的问题。比如下面的代码,obj1obj2 互相引用,这种情况下,引用计数器的值都是1,不会被垃圾回收。

public class ReferenceCount {
    Object instance = null;
 public static void main(String[] args) {
  ReferenceCount obj1 = new ReferenceCount();
  ReferenceCount obj2 = new ReferenceCount();
  obj1.instance = obj2;
  obj2.instance = obj1;
  obj1 = null;
  obj2 = null;
 }
}

可达性分析

通过GC Root对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到GC Root没有任何的引用链相连时,说明这个对象是不可用的。

JVM高频面试题20道

可作为GC Roots的对象有哪些?

  1. 虚拟机栈中引用的对象
  2. 本地方法栈中Native方法引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中常量引用的对象

什么情况下类会被卸载?

需要同时满足一下 3 个条件类才可能会被卸载 :

虚拟机可以对满足上述 3 个条件的类进行回收,但不一定会进行回收。

强引用、软引用、弱引用、虚引用是什么?

强引用:在程序中普遍存在的引用赋值,类似Object obj = new Object()这种引用关系。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

//软引用
SoftReference softRef = new SoftReference(str);

弱引用:在进行垃圾回收时,不管当前内存空间足够与否,都会回收只具有弱引用的对象。

//弱引用
WeakReference weakRef = new WeakReference(str);

虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要是为了能在对象被收集器回收时收到一个系统通知

Minor GC 和 Full GC的区别?

内存的分配策略?

对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,触发 Minor GC

大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象有长字符串和大数组。可以设置JVM参数 -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配。

长期存活的对象进入老年代

通过参数 -XX:MaxTenuringThreshold 可以设置对象进入老年代的年龄阈值。对象在Survivor区每经过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。

动态对象年龄判定

并非对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需达到 MaxTenuringThreshold 年龄阈值。

空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 是安全的。如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败。如果允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值为不允许担保失败,那么就要进行一次 Full GC

垃圾回收算法有哪些?

垃圾回收算法有四种,分别是标记清除法、标记整理法、复制算法、分代收集算法

标记清除算法

首先利用可达性去遍历内存,把存活对象和垃圾对象进行标记。标记结束后统一将所有标记的对象回收掉。这种垃圾回收算法效率较低,并且会产生大量不连续的空间碎片

JVM高频面试题20道

复制清除算法

半区复制,用于新生代垃圾回收。将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。

特点:实现简单,运行高效,但可用内存缩小为了原来的一半,浪费空间。

标记整理算法

根据老年代的特点提出的一种标记算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

JVM高频面试题20道

分类收集算法

根据各个年代的特点采用最适当的收集算法。

一般将堆分为新生代和老年代。

在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,使用复制算法比较合适,只需要付出少量存活对象的复制成本就可以完成收集。老年代对象存活率高,适合使用标记-清理或者标记-整理算法进行垃圾回收。

有哪些垃圾回收器?

垃圾回收器主要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1

Serial 收集器

单线程收集器,使用一个垃圾收集线程去进行垃圾回收,在进行垃圾回收的时候必须暂停其他所有的工作线程( Stop The World ),直到它收集结束。

特点:简单高效;内存消耗小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。

ParNew 收集器

Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其他行为、参数与 Serial 收集器基本一致。

Parallel Scavenge 收集器

新生代收集器,基于复制清除算法实现的收集器。特点是吞吐量优先,能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))。Parallel Scavenge 收集器关注点是吞吐量,高效率的利用 CPU 资源CMS 垃圾收集器关注点更多的是用户线程的停顿时间

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

Serial Old 收集器

Serial 收集器的老年代版本,单线程收集器,使用标记整理算法

Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。多线程垃圾收集,使用标记整理算法

CMS 收集器

Concurrent Mark Sweep ,并发标记清除,追求获取最短停顿时间,实现了让垃圾收集线程与用户线程基本上同时工作

CMS 垃圾回收基于标记清除算法实现,整个过程分为四个步骤:

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

优点:并发收集,停顿时间短。

缺点

G1收集器

G1垃圾收集器的目标是在不同应用场景中追求高吞吐量和低停顿之间的最佳平衡

G1将整个堆分成相同大小的分区(Region),有四种不同类型的分区:Eden、Survivor、Old和Humongous。分区的大小取值范围为 1M 到 32M,都是2的幂次方。分区大小可以通过-XX:G1HeapRegionSize参数指定。Humongous区域用于存储大对象。G1规定只要大小超过了一个分区容量一半的对象就认为是大对象。

JVM高频面试题20道

G1 收集器对各个分区回收所获得的空间大小和回收所需时间的经验值进行排序,得到一个优先级列表,每次根据用户设置的最大回收停顿时间,优先回收价值最大的分区。

特点:可以由用户指定期望的垃圾收集停顿时间。

G1 收集器的回收过程分为以下几个步骤:

常用的 JVM 调优的参数都有哪些?

JVM调优工具有哪些?

jps:列出本机所有 Java 进程的进程号

常用参数如下:

jps -lvm
//output
//4124 com.zzx.Application -javaagent:E:IDEA2019libidea_rt.jar=10291:E:IDEA2019bin -Dfile.encoding=UTF-8

jstack:查看某个 Java 进程内的线程堆栈信息。使用参数-l可以打印额外的锁信息,发生死锁时可以使用jstack -l pid观察锁持有情况。

jstack -l 4124 | more

输出结果如下:

"http-nio-8001-exec-10" #40 daemon prio=5 os_prio=0 tid=0x000000002542f000 nid=0x4028 waiting on condition [0x000000002cc9e000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000077420d7e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

WAITING (parking)指线程处于挂起中,在等待某个条件发生,来把自己唤醒。

jstat:用于查看虚拟机各种运行状态信息(类装载、内存、垃圾收集等运行数据)。使用参数-gcuitl可以查看垃圾回收的统计信息。

jstat -gcutil 4124
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00  67.21  19.20  96.36  94.96     10    0.084     3    0.191    0.275

参数说明:

jmap:查看堆内存快照。通过jmap命令可以获得运行中的堆内存的快照,从而可以对堆内存进行离线分析。

查询进程4124的堆内存快照,输出结果如下:

>jmap -heap 4124
Attaching to process ID 4124, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 6 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4238344192 (4042.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1412431872 (1347.0MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 327155712 (312.0MB)
   used     = 223702392 (213.33922576904297MB)
   free     = 103453320 (98.66077423095703MB)
   68.37795697725736% used
From Space:
   capacity = 21495808 (20.5MB)
   used     = 0 (0.0MB)
   free     = 21495808 (20.5MB)
   0.0% used
To Space:
   capacity = 23068672 (22.0MB)
   used     = 0 (0.0MB)
   free     = 23068672 (22.0MB)
   0.0% used
PS Old Generation
   capacity = 217579520 (207.5MB)
   used     = 41781472 (39.845916748046875MB)
   free     = 175798048 (167.65408325195312MB)
   19.20285144484187% used

27776 interned Strings occupying 3262336 bytes.

main方法执行过程

以下是示例代码:

public class Application {
    public static void main(String[] args) {
        Person p = new Person("大彬");
        p.getName();
    }
}

class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

执行main方法的过程如下:

  1. 编译Application.java后得到 Application.class 后,执行这个class文件,系统会启动一个 JVM 进程,从类路径中找到一个名为 Application.class 的二进制文件,将 Application 类信息加载到运行时数据区的方法区内,这个过程叫做类的加载。
  2. JVM 找到 Application 的主程序入口,执行main方法。
  3. main方法的第一条语句为 Person p = new Person("大彬"),就是让 JVM 创建一个Person对象,但是这个时候方法区中是没有 Person 类的信息的,所以 JVM 马上加载 Person 类,把 Person 类的信息放到方法区中。
  4. 加载完 Person 类后,JVM 在堆中分配内存给 Person 对象,然后调用构造函数初始化 Person 对象,这个 Person 对象持有指向方法区中的 Person 类的类型信息的引用。
  5. 执行p.getName()时,JVM 根据 p 的引用找到 p 所指向的对象,然后根据此对象持有的引用定位到方法区中 Person 类的类型信息的方法表,获得 getName() 的字节码地址。
  6. 执行getName()方法。

对象创建过程

  1. 类加载检查:当虚拟机遇到一条 new 指令时,首先检查是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那先执行类加载。
  2. 分配内存:在类加载检查通过后,接下来虚拟机将为对象实例分配内存。
  3. 初始化。分配到的内存空间都初始化为零值,通过这个操作保证了对象的字段可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 设置对象头Hotspot 虚拟机的对象头包括:存储对象自身的运行时数据(哈希码、分代年龄、锁标志等等)、类型指针和数据长度(数组对象才有),类型指针就是对象指向它的类信息的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  5. 按照Java代码进行初始化

参考资料

展开阅读全文

页面更新:2024-03-15

标签:常量   新生代   线程   算法   标记   虚拟机   加载   对象   内存   垃圾   年代   参数   时间   方法   用户   科技

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top