Java开发面试指南系列-JVM (二)

该篇为Java JVM相关知识点

1.HotSpot为什么要分为新⽣代和⽼年代?

主要是为了提升GC效率。上篇末提到的分代收集算法已经很好的解释了这个问题。 (JVM 一)

2.常⻅的垃圾回收器有那些?

2024010607040291

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

虽然我们对各个收集器进⾏⽐᫾,但并⾮要挑选出⼀个最好的收集器。因为知道现在为⽌还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应⽤场景选择适合⾃⼰的垃圾收集器。试想⼀下:如果有⼀种四海之内、任何场景下都适⽤的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同的垃圾收集器了。

Serial收集器

Serial(串⾏)收集器收集器是最基本、历史最悠久的垃圾收集器了。⼤家看名字就知道这个收集器是⼀个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使⽤⼀条垃圾收集线程去完成垃圾收集⼯作,更重要的是它在进⾏垃圾收集⼯作的时候必须暂停其他所有的⼯作线程( “Stop The World”),直到它收集结束。

新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

2024010607054161

虚拟机的设计者们当然知道Stop The World带来的不良⽤户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。但是Serial收集器有没有优于其他垃圾收集器的地⽅呢?当然有,它简单⽽⾼效(与其他收集器的单线程相⽐)。Serial收集器由于没有线程交互的开销,⾃然可以获得很⾼的单线程收集效率。Serial收集器对于运⾏在Client模式下的虚拟机来说是个不错的选择。

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余⾏为(控制参数、收集算法、回收策略等等)和Serial收集器完全⼀样。

新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

2024010607072345

它是许多运⾏在Server模式下的虚拟机的⾸要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后⾯会介绍到)配合⼯作。

并⾏和并发概念补充:

Parallel Scavenge 收集器类似于ParNew 收集器。 那么它有什么特别之处呢?

-XXé+UseParallelGC
    使⽤Parallel收集器+ ⽼年代串⾏
-XXé+UseParallelOldGC
    使⽤Parallel收集器+ ⽼年代并⾏

Parallel Scavenge收集器关注点是吞吐量(⾼效率的利⽤CPU)。CMS等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验)。所谓吞吐量就是CPU中⽤于运⾏⽤户代码的时间与CPU总消耗时间的⽐值。 Parallel Scavenge收集器提供了很多参数供⽤户找到最合适的停顿时间或最⼤吞吐量,如果对于收集器运作不太了解的话,⼿⼯优化存在的话可以选择把内存管理优化交给虚拟机去完成也是⼀个不错的选择。

新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

2024010607090571

Serial Old收集器

Serial收集器的⽼年代版本,它同样是⼀个单线程收集器。它主要有两⼤⽤途:⼀种⽤途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使⽤,另⼀种⽤途是作为CMS收集器的后备⽅案。

Parallel Old收集器

Parallel Scavenge收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器。它⽽⾮常符合在注重⽤户体验的应⽤上使⽤。

CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第⼀款真正意义上的并发收集器,它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是⼀种 “标记-清除”算法实现的,它的运作过程相⽐于前⾯⼏种垃圾收集器来说更加复杂⼀些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快 ;
  • 并发标记: 同时开启GC和⽤户线程,⽤⼀个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为⽤户线程可能会不断的更新引⽤域,所以GC线程⽆法保证可达性分析的实时性。所以这个算法⾥会跟踪记录这些发⽣引⽤更新的地⽅。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短
  • 并发清除: 开启⽤户线程,同时GC线程开始对为标记的区域做清扫。

2024010607120642

从它的名字就可以看出它是⼀款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下⾯三个明显的缺点:

  • 对CPU资源敏感;
  • ⽆法处理浮动垃圾;
  • 它使⽤的回收算法-“标记-清除”算法会导致收集结束时会有⼤量空间碎⽚产⽣。

G1收集器

G1 (Garbage-First)是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征.被视为JDK1.7中HotSpot虚拟机的⼀个重要进化特征。它具备⼀下特点:

  • 并⾏与并发:G1能充分利⽤CPU、多核环境下的硬件优势,使⽤多个CPU(CPU或者CPU核⼼)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执⾏的GC动作,G1收集器仍然可以通过并发的⽅式让java程序继续执⾏。
  • 分代收集:虽然G1可以不需要其他收集器配合就能独⽴管理整个GC堆,但是还是保留了分代的概念。
  • 空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿:这是G1相对于CMS的另⼀个⼤优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为M毫秒的时间⽚段内。

G1收集器的运作⼤致分为以下⼏个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

G1收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的Region(这也就是它的名字Garbage-First的由来)。这种使⽤Region划分内存空间以及有优先级的区域回收⽅式,保证了GF收集器在有限时间内可以尽可能⾼的收集效率(把内存化整为零)。

3.类⽂件结构

介绍⼀下类⽂件结构,根据 Java 虚拟机规范,类⽂件由单个 ClassFile 结构组成:

ClassFile {
  u4              magic; //Class ⽂件的标志
  u2              minor_version;//Class 的⼩版本号
  u2              major_version;//Class 的⼤版本号
  u2              constant_pool_count;//常量池的数量
  cp_info         constant_pool[constant_pool_count-1];//常量池
  u2              access_flags;//Class 的访问标记
  u2              this_class;//当前类
  u2              super_class;//⽗类
  u2              interfaces_count;//接⼝
  u2              interfaces[interfaces_count];//⼀个类可以实现多个接⼝
  u2              fields_count;//Class ⽂件的字段属性
  field_info      fields[fields_count];//⼀个类会可以有个字段
  u2              methods_count;//Class ⽂件的⽅法数量
  method_info     methods[methods_count];//⼀个类可以有个多个⽅法
  u2              attributes_count;//此类的属性表中的属性数
  attribute_info  attributes[attributes_count];//属性表集合
}

Class⽂件字节码结构组织示意图 (之前在⽹上保存的,⾮常不错,原出处不明):

2024010607190728

下⾯会按照上图结构按顺序详细介绍⼀下 Class ⽂件结构涉及到的⼀些组件。

  1. 魔数: 确定这个⽂件是否为⼀个能被虚拟机接收的 Class ⽂件。
  2. Class ⽂件版本 :Class ⽂件的版本号,保证编译正常执⾏。
  3. 常量池 :常量池主要存放两⼤常量:字⾯量和符号引⽤。
  4. 访问标志 :标志⽤于识别⼀些类或者接⼝层次的访问信息,包括:这个 Class 是类还是接⼝,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。
  5. 当前类索引,⽗类索引 :类索引⽤于确定这个类的全限定名,⽗类索引⽤于确定这个类的⽗类的全限定名,由于 Java 语⾔的单继承,所以⽗类索引只有⼀个,除了 java.lang.Object 之外,所有的 java 类都有⽗类,因此除了 java.lang.Object 外,所有 Java 类的⽗类索引都不为 0。
  6. 接⼝索引集合 :接⼝索引集合⽤来描述这个类实现了那些接⼝,这些被实现的接⼝将按 implents (如果这个类本身是接⼝的话则是 extends ) 后的接⼝顺序从左到右排列在接⼝索引集合中。
  7. 字段表集合 :描述接⼝或类中声明的变量。字段包括类级变量以及实例变量,但不包括在⽅法内部声明的局部变量。
  8. ⽅法表集合 :类中的⽅法。
  9. 属性表集合 : 在 Class ⽂件,字段表,⽅法表中都可以携带⾃⼰的属性表集合。

4.类加载过程

知道类加载的过程吗?

类加载过程:加载->连接->初始化。连接过程⼜可分为三步: 验证->准备->解析

2024010607233321

那加载这⼀步做了什么?

类加载过程的第⼀步,主要完成下⾯3件事情:

  1. 通过全类名获取定义此类的⼆进制字节流
  2. 将字节流所代表的静态存储结构转换为⽅法区的运⾏时数据结构
  3. 在内存中⽣成⼀个代表该类的 Class 对象,作为⽅法区这些数据的访问⼊⼝

虚拟机规范多上⾯这3点并不具体,因此是⾮常灵活的。⽐如:”通过全类名获取定义此类的⼆进制字节流” 并没有指明具体从哪⾥获取、怎样获取。⽐如:⽐᫾常⻅的就是从 ZIP 包中读取(⽇后出现的JAR、EAR、WAR格式的基础)、其他⽂件⽣成(典型应⽤就是JSP)等等。

⼀个⾮数组类的加载阶段(加载阶段获取类的⼆进制字节流的动作)是可控性最强的阶段,这⼀步我们可以去完成还可以⾃定义类加载器去控制字节流的获取⽅式(重写⼀个类加载器的 loadClass() ⽅法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。类加载器、双亲委派模型也是⾮常重要的知识点,这部分内容会在后⾯的问题中单独介绍到。加载阶段和连接阶段的部分内容是交叉进⾏的,加载阶段尚未结束,连接阶段可能就已经开始了。

知道哪些类加载器?

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承⾃ java.lang.ClassLoader :

  • BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib ⽬录下的jar包和类或者或被 -Xbootclasspath 参数指定的路径中的所有类。
  • ExtensionClassLoader(扩展类加载器) :主要负责加载⽬录 %JRE_HOME%/lib/ext ⽬录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
  • AppClassLoader(应⽤程序类加载器) :⾯向我们⽤户的加载器,负责加载当前应⽤classpath下的所有jar包和类。

双亲委派模型知道吗?能介绍⼀下吗?

双亲委派模型介绍

每⼀个类都有⼀个对应它的类加载器。系统中的 ClassLoder 在协同⼯作的时候会默认使⽤ 双亲委派模型 。即在类加载的时候,系统会⾸先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,⾸先会把该请求委派该⽗类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当⽗类加载器⽆法处理时,才由⾃⼰来处理。当⽗类加载器为null时,会使⽤启动类加载器 BootstrapClassLoader 作为⽗类加载器。

2024010607273598

每个类加载都有⼀个⽗类加载器,我们通过下⾯的程序来验证。

public class ClassLoaderDemo {
   public static void main(String[] args) {
     System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
     System.out.println("The Parent of ClassLodarDemo's ClassLoaderis " + ClassLoaderDemo.class.getClassLoader().getParent());
     System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
   }
}

Output

ClassLodarDemo's ClassLoader is
sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is
sun.misc.Launcher$ExtClassLoader@1b6d3586
The GrandParent of ClassLodarDemo's ClassLoader is null

AppClassLoader 的⽗类加载器为 ExtClassLoader ExtClassLoader 的⽗类加载器为null,null并不代表 ExtClassLoader 没有⽗类加载器,⽽是 Bootstrap ClassLoader 。

其实这个双亲翻译的容易让别⼈误解,我们⼀般理解的双亲都是⽗⺟,这⾥的双亲更多地表达的是“⽗⺟这⼀辈”的⼈⽽已,并不是说真的有⼀个 Mather ClassLoader 和⼀个 Father ClassLoader 。另外,类加载器之间的“⽗⼦”关系也不是通过继承来体现的,是由“优先级”来决定。官⽅API⽂档对这部分的描述如下:

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.

双亲委派模型实现源码分析

双亲委派模型的实现代码⾮常简单,逻辑⾮常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中,相关代码如下所示。

private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException{
    synchronized (getClassLoadingLock(name)) {
    // ⾸先,检查请求的类是否已经被加载过
    Class<?> c = findLoadedClass(name);
    if (c == null) {
    long t0 = System.nanoTime();
    try {
      if (parent != null) {//⽗加载器不为空,调⽤⽗加载器loadClass()⽅法处理
        c = parent.loadClass(name, false);
      } else {//⽗加载器为空,使⽤启动类加载器BootstrapClassLoader 加载
        c = findBootstrapClassOrNull(name);
      }
    } catch (ClassNotFoundException e) {
      //抛出异常说明⽗类加载器⽆法完成加载请求
    }
    if (c == null) {
        long t1 = System.nanoTime();
        //⾃⼰尝试加载
        c = findClass(name);
        // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
               }
          }
          if (resolve) {
             resolveClass(c);
          }
          return c;
      }
}

双亲委派模型带来了什么好处呢?

双亲委派模型保证了Java程序的稳定运⾏,可以避免类的重复加载(JVM 区分不同类的⽅式不仅仅根据类名,相同的类⽂件被不同的类加载器加载产⽣的是两个不同的类),也保证了 Java 的核⼼ API 不被篡改。如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的Object 类。

如果我们不想⽤双亲委派模型怎么办?

为了避免双亲委托机制,我们可以⾃⼰定义⼀个类加载器,然后重载 loadClass() 即可。

如何⾃定义类加载器?

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承⾃java.lang.ClassLoader 。如果我们要⾃定义⾃⼰的类加载器,很明显需要继承 ClassLoader 。

 

作者:Hardy

链接:https://bbyycc.com/stne/4220.html

声明:如无特别声明本文即为原创文章仅代表个人观点,版权归《屿川博客》作者所有,欢迎转载,转载请保留原文链接。

Like (0)
Donate 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
Previous 2024年1月6日 下午2:50
Next 2024年3月25日 下午1:23

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

TOP