JVM虚拟机-面试题

JVM虚拟机

1. JVM运行时数据区有哪些?

包括方法区、堆、虚拟机栈、本地方法栈、程序计数器

 

程序计数器:

是一块较小的内存空间,可以看做是当前线程执行的字节码的行号指示器,每个线程都有一个独立的程序计数器,是线程私有的内存区域。(此内存区域是唯一一个不会抛出OutOfMemoryError的区域)

 

java虚拟机栈:

描述的java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表存放了各种基本数据类型、引用类型等。是线程私有的。

 

本地方法栈:

为虚拟机使用到的native方法服务。(而虚拟机栈为虚拟机执行java方法服务)

 

java堆:

java堆是被所有线程共享的内存区域,在虚拟机启动时创建,用于存放对象实例。又可以称为“GC堆”。可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。java堆被细分为新生代和老年代,新生代又被进一步划分为Eden区、From Survivor、To Survivor区。

 

方法区:

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

 

运行时常量池:

是方法区的一部分,Class文件中除了有类的版本号、字段、方法、接口等描述信息外,还有常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

2. java内存模型

1)主内存与工作内存

内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。

Java内存模型中规定所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。

线程、主内存和工作内存的交互关系如下图所示:

 

 

2)内存间交互操作:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

3)重排序

  在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序。

重排序分成三种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从Java源代码到最终实际执行的指令序列,会经过下面三种重排序:

 

为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。Java内存模型把内存屏障分为LoadLoad、LoadStore、StoreLoad和StoreStore四种:

 

3.JVM的4种引用类型?

包括强引用、软引用、弱引用、虚引用,这4种引用强度依次逐渐减弱。

 

强引用:

类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

 

软引用:

用来描述一些还有用但并非必需的对象。 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。 如果这次回收还没有足够的内存,才会抛出内存溢出异常。

 

弱引用:

也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

 

虚引用:

是最弱的一种引用关系。 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

 

4. 垃圾收集算法包括哪些?

  • 标记——清除算法:

算法分为“标记”、“清除”两个阶段:1、标记所有需要回收的对象 2、清除被标记的对象。其标记的过程就是判断对象有效性,执行可达性分析的过程。

缺点:

一个是效率问题,标记和清除两个过程的效率都不高;

另一个是空间问题,标记清除后会产生大量不连续的内存碎片,可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存,而触发另一次垃圾收集操作。

 

  • 复制算法:

解决了标记-清除算法中效率的问题。

将可用内存按容量分为大小相等的两块,每次只使用其中的一块。当一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间清空。

缺点:

一是将内存缩小为原来的一半,空间利用率降低;

二是在对象存活率较高时,要进行较多的复制操作,效率变低。

 

  • 标记-整理算法:

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

 

  • 分代收集算法:

根据对象存活周期的不同,将内存划分为几块,一般是把java堆分为新生代和老年代,这样可以根据各个年代的特点采用最合适的收集算法。比如新生代的对象在每次垃圾回收时都会有大量的对象死去,只有很少一部分存活,那就可以选择标记-复制算法。另外,在新生代中每次死亡对象约占98%,那么在标记-复制算法中就不需要按照1:1的比例来划分内存区域,而是将新生代细分为了一块较大的Eden和两块较小的Survivor区域,HotSpot中默认这两块区域的大小比例为8:2。每次新生代可用区域为Eden加上其中一块Survivor区域,共90%的内存空间,这样就只有10%的内存空间处在被闲置状态。在进行垃圾回收时,存活的对象被转移到原本处在“空闲的”Survivor区域。如果某次垃圾回收后,存活对象所占空间远大于这10%的内存空间时,也就是Survivor空间不够用时,需要额外的空间来担保,通常是将这些对象转移到老年代。对于老年代来说,对象存活率高、没有额外的空间对它进行分配担保,就必须选用标记-清除或者标记-整理算法来进行垃圾回收了。

 

5.垃圾收集器有哪些?

垃圾收集器是垃圾回收算法的具体实现(HotSpot虚拟机中的垃圾收集器)

 

  • Serial收集器

Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;

       JDK1.3.1前是HotSpot新生代收集的唯一选择;

特点:

       针对新生代;

       采用复制算法;

       单线程收集;

       进行垃圾收集时,必须暂停所有工作线程,直到完成;            

应用场景:

依然是HotSpot在Client模式下默认的新生代收集器;简单高效。

Serial/Serial Old组合收集器运行示意图如下:

 

  • ParNew收集器

ParNew垃圾收集器是Serial收集器的多线程版本。

特点:

       除了多线程外,其余的行为、特点和Serial收集器一样

应用场景:

      ParNew收集器是Server模式下的虚拟机中首选的新生代收集器,除Serial外,目前只有它能与CMS收集器配合工作。

ParNew/Serial Old组合收集器运行示意图如下:

 

  • Parallel Scavenge收集器

 Parallel Scavenge垃圾收集器关注吞吐量,目的是达到一个可控制的吞吐量。

特点:

      新生代收集器;

      采用复制算法;

      多线程收集;

GC自适应的调节策略;

 应用场景:

       当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计 算,而不需要与用户进行太多交互;

  • Serial Old收集器

 Serial Old是 Serial收集器的老年代版本;

特点:

      针对老年代;

      采用"标记-整理"算法;

      单线程收集;

给Client模式下的虚拟机使用。

Serial/Serial Old收集器运行示意图如下:

 

  • Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;

特点:

      针对老年代;

      采用"标记-整理"算法;

      多线程收集;

Parallel Scavenge/Parallel Old收集器运行示意图如下:

 

在注重吞吐量以及CPU资源敏感的场景,优先考虑Parallel Scavenge加Parallel Old收集器的应用组合;

  • CMS收集器

 CMS(并发标记清理)收集器, 以获取最短回收停顿时间为目标。

特点:

      针对老年代;

      基于"标记-清除"算法;            

      以获取最短回收停顿时间为目标;

      并发收集、低停顿;

      需要更多的内存;

 

与用户交互较多的场景,希望系统停顿时间最短, 以给用户带来较好的体验;

CMS收集器3个明显的缺点:

(A)对CPU资源非常敏感

(B)无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败

(C)产生大量内存碎片

 

 

  • G1收集器

G1是一款面向服务端应用的垃圾收集器,针对具有大内存、多处理器的机器。

特点:

(A)、并行与并发

(B)、分代收集,收集范围包括新生代和老年代    

(C)、结合多种垃圾收集算法,空间整合,不产生碎片

(D)、可预测的停顿:低停顿的同时实现高吞吐量

 

6. Minor GC和Full GC的区别?

  • Minor GC

       又称新生代GC,指发生在新生代的垃圾收集动作;

       因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;

  • Full GC

       又称Major GC或老年代GC,指发生在老年代的GC;

       出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);

      Major GC速度一般比Minor GC慢10倍以上;

7. java的类加载机制?

1)什么是类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

 

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)。如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

2)类的生命周期

 

其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉的混合进行,通常在一个阶段执行的过程中调用或激活另一个阶段。

  • 加载:查找并加载类的二进制数据

1、通过一个类的全限定名来获取其定义的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3、在Java堆中生成一个代表这个类的Class对象,作为对方法区中这些数据的访问入口。

  • 验证:确保被加载的类的正确性

验证是连接阶段的第一步,大致会完成4个阶段的检验动作:

文件格式验证、元数据验证、字节码验证、符号引用验证。

  • 准备:为类的静态变量分配内存,并设置类变量初始值。

1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

2、这里所设置的初始值通常情况下是数据类型默认值的零(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

  •  解析:把类中的符号引用转换为直接引用
  • 初始化:为类的静态变量赋予正确的初始值

  在Java中对类变量进行初始值设定有两种方式:

  ①声明类变量是指定初始值

  ②使用静态代码块为类变量指定初始值

JVM初始化步骤:

  1、假如这个类还没有被加载和连接,则程序先加载并连接该类

 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

 3、假如类中有初始化语句,则系统依次执行这些初始化语句

  • 结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期:

– 执行了System.exit()方法

– 程序正常执行结束

– 程序在执行过程中遇到了异常或错误而终止

– 由于操作系统出现错误而导致Java虚拟机进程终止

3)类加载器

 

类加载器可以大致划分为以下三类:

启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。

4)类的加载

类加载有三种方式:

1、命令行启动应用时由JVM初始化加载

2、通过Class.forName()方法动态加载

3、通过ClassLCoader.loadlass()方法动态加载

5)双亲委派模型

工作流程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

 

6)Class.forName()和ClassLoader.loadClass()区别

Class.forName():将类的.class文件加载到jvm中,还会对类进行解释,执行类中的static块;

ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

 

 

 

你可能感兴趣的:(jvm)