(二)JVM类加载以及双亲委派

目录

类加载机制

​类加载器

双亲委派机制

打破双亲委派模型案例

案例一:tomcat

案例二:SPI

案例三:OSGi

如何替换 JDK 的类

如何动态加载和卸载类


 

速记秘诀:家宴准姐出(加载,验证,准备,解析,初始化)

类加载机制

(二)JVM类加载以及双亲委派_第1张图片

加载

将外部的 .class 文件(属于二进制数据,保存在jar或war里面),加载到 Java 的方法区内。

验证

并不是所有的 .class 文件都能加载到虚拟机中,容易受到攻击。不符合规范的将抛出 java.lang.VerifyError 错误。像一些低版本的 JVM,是无法加载一些高版本的类库的,就是在这个阶段完成的。

准备

为一些变量分配内存并初始化为默认值。此时,实例对象还没有分配内存,所以这些动作是在方法区上进行的。

code-snippet 1:
  public class A {
     static int a ;
     public static void main(String[] args) {
         System.out.println(a);
     }
  }
code-snippet 2:
  public class A {
      public static void main(String[] args) {
         int a ;
         System.out.println(a);
      }
  }
-------------------------------------------------------------

结果:
code-snippet 1 将会输出 0,
code-snippet 2 将无法通过编译。

原因:
类变量有两次赋初始值的过程,一次在准备阶段,赋予初始值(也可以是指定值);另外一次在初始化阶段,赋予程序员定义的值。
因此,即使程序员没有为类变量赋值也没有关系,它仍然有一个默认的初始值。但局部变量就不一样了,如果没有给它赋初始值,是不能使用的。

解析

解析在类加载中是非常非常重要的一环,是将符号引用替换为直接引用的过程。

  • 符号引用:是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。
  • 直接引用:它的对象都存在于内存中,你可以把通讯录里的女友手机号码,类比为符号引用,把面对面和你吃饭的人,类比为直接引用。

解析阶段负责把整个类激活,串成一个可以找到彼此的网,过程不可谓不重要。那这个阶段都做了哪些工作呢?大体可以分为:

  •     类或接口的解析
  •     类方法解析
  •     接口方法解析
  •     字段解析

我们来看几个经常发生的异常,就与这个阶段有关。

  •     java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。
  •     java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。
  •     java.lang.NoSuchMethodError 找不到相关方法时的错误。

解析过程保证了相互引用的完整性,把继承与组合推进到运行时。

初始化

规则1:static 语句块,只能访问到定义在 static 语句块之前的变量

规则2:JVM 会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕。(也意味着父类中定义的 static 语句块要优先于子类的。)

1.下面的代码,会输出什么?
    public class A {
         static int a = 0 ;
         static {
             a = 1;
             b = 1;
         }
         static int b = 0;
     
         public static void main(String[] args) {
             System.out.println(a);
             System.out.println(b);
         }
     }

结果是 1 0。a 和 b 唯一的区别就是它们的 static 代码块的位置。符合规则1
所以下面的代码是无法通过编译的。
    static {
             b = b + 1;
     }
    static int b = 0;


2. 方法和  方法有什么区别?
主要是为了让你弄明白类的初始化和对象的初始化之间的差别。
    public class A {
         static {
             System.out.println("1");
         }
         public A(){
             System.out.println("2");
         }
    }
     
    public class B extends A {
         static{
             System.out.println("a");
         }
         public B(){
             System.out.println("b");
         }
     
         public static void main(String[] args){
             A ab = new B();
             ab = new B();
         }
     }

先公布下答案:
    1
    a
    2
    b
    2
    b

分析一:其中 static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。类信息会被存放在方法区,在同一个类加载器下,这些信息有一份就够了,所以上面的 static 代码块只会执行一次,它对应的是  方法。
分析二:而对象初始化就不一样了。通常,我们在 new 一个新对象的时候,都会调用它的构造方法,就是 ,用来初始化对象的属性。每次新建对象的时候,都会执行。


结论:所以,上面代码的 static 代码块只会执行一次,对象的构造方法执行两次。再加上继承关系的先后原则,不难分析出正确结果。

(二)JVM类加载以及双亲委派_第2张图片
类加载器

类加载器做的就是上面 5 个步骤的事。

如果你在项目代码里,写一个 java.lang 的包,然后改写 String 类的一些行为,编译后,发现并不能生效。JRE 的类当然不能轻易被覆盖,否则会被别有用心的人利用,这就太危险了。那类加载器是如何保证这个过程的安全性呢?其实,它是有着严格的等级制度的。

(二)JVM类加载以及双亲委派_第3张图片

Bootstrap ClassLoader

这是加载器中的大 Boss,任何类的加载行为,都要经它过问。它的作用是加载核心类库,也就是lib目录下的 rt.jar、resources.jar、charsets.jar 等。当然这些 jar 包的路径是可以指定的,-Xbootclasspath 参数可以完成指定操作。

这个加载器是 C++ 编写的,随着 JVM 启动。

Extention ClassLoader

扩展类加载器,主要用于加载 lib/ext 目录下的 jar 包和 .class 文件。同样的,通过系统变量 java.ext.dirs 可以指定这个目录。

这个加载器是个 Java 类,继承自 URLClassLoader。

App ClassLoader

这是我们写的 Java 类的默认加载器,有时候也叫作 System ClassLoader。一般用来加载 classpath 下的其他所有 jar 包和 .class 文件,我们写的代码,会首先尝试使用这个类加载器进行加载。

Custom ClassLoader

自定义加载器,支持一些个性化的扩展功能。


双亲委派机制

双亲委派机制的意思是除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。

打个比方。有一个家族,都是一些听话的孩子。孙子想要买一块棒棒糖,最终都要经过爷爷过问,如果力所能及,爷爷就直接帮孙子买了。

  • 问题:类加载的双亲委派机制,双亲在哪里?明明都是单亲?
  • 答案:但是由于翻译的问题,这个叫法已经非常普遍了!!!!

我们可以翻阅 JDK 代码的 ClassLoader#loadClass 方法,来看一下具体的加载过程。和我们描述的一样,它首先使用 parent 尝试进行类加载,parent 失败后才轮到自己。同时,我们也注意到,这个方法是可以被覆盖的,也就是双亲委派机制并不一定生效。

(二)JVM类加载以及双亲委派_第4张图片

模型优点:使 Java 类有了一种优先级的层次划分关系。比如 Object 类,这个毫无疑问应该交给最上层的加载器进行加载,即使是你覆盖了它,最终也是由系统默认的加载器进行加载的。如果没有双亲委派模型,就会出现很多个不同的 Object 类,应用程序会一片混乱。

打破双亲委派模型案例

案例一:tomcat

tomcat 通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则的。简单看一下 tomcat 类加载器的层次结构。

(二)JVM类加载以及双亲委派_第5张图片

对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。这个加载器用来隔绝不同应用的 .class 文件,比如你的两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。

如何在同一个 JVM 里,运行着不兼容的两个版本,当然是需要自定义加载器才能完成的事。

那么 tomcat 是怎么打破双亲委派机制的呢?可以看图中的 WebAppClassLoader,它加载自己目录下的 .class 文件,并不会传递给父类的加载器。但是,它却可以使用 SharedClassLoader 所加载的类,实现了共享和分离的功能。

但是你自己写一个 ArrayList,放在应用目录里,tomcat 依然不会加载。它只是自定义的加载器顺序不同,但对于顶层来说,还是一样的。

案例二:SPI

Java 中有一个 SPI 机制,全称是 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。

这个说法可能比较晦涩,但是拿我们常用的数据库驱动加载来说,就比较好理解了。在使用 JDBC 写程序之前,通常会调用下面这行代码,用于加载所需要的驱动类。

    Class.forName("com.mysql.jdbc.Driver")

这只是一种初始化模式,通过 static 代码块显式地声明了驱动对象,然后把这些信息,保存到底层的一个 List 中。这种方式我们不做过多的介绍,因为这明显就是一个接口编程的思路,没什么好奇怪的。

但是你会发现,即使删除了 Class.forName 这一行代码,也能加载到正确的驱动类,什么都不需要做,非常的神奇,它是怎么做到的呢?

我们翻开 MySQL 的驱动代码,发现了一个奇怪的文件。之所以能够发生这样神奇的事情,就是在这里实现的。

路径:   

 mysql-connector-java-8.0.15.jar!/META-INF/services/java.sql.Driver

里面的内容是:    

com.mysql.cj.jdbc.Driver

通过在 META-INF/services 目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载这一种实现,这就是 SPI。

SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,主要使用 java.util.ServiceLoader 类进行动态装载。

(二)JVM类加载以及双亲委派_第6张图片

这种方式,同样打破了双亲委派的机制。

DriverManager 类和 ServiceLoader 类都是属于 rt.jar 的。它们的类加载器是 Bootstrap ClassLoader,也就是最上层的那个。而具体的数据库驱动,却属于业务代码,这个启动类加载器是无法加载的。这就比较尴尬了,虽然凡事都要祖先过问,但祖先没有能力去做这件事情,怎么办?我们可以一步步跟踪代码,来看一下这个过程。

//part1:DriverManager::loadInitialDrivers
//jdk1.8 之后,变成了lazy的ensureDriversInitialized
      ...
      ServiceLoader  loadedDrivers = ServiceLoader.load(Driver.class);
      Iterator driversIterator = loadedDrivers.iterator();
      ...
      //part2:ServiceLoader::load
      public static  ServiceLoader load(Class service) {
          ClassLoader cl = Thread.currentThread().getContextClassLoader();
          return ServiceLoader.load(service, cl);
      }

通过代码你可以发现 Java 玩了个魔术,它把当前的类加载器,设置成了线程的上下文类加载器。那么,对于一个刚刚启动的应用程序来说,它当前的加载器是谁呢?也就是说,启动 main 方法的那个加载器,到底是哪一个?所以我们继续跟踪代码。找到 Launcher 类,就是 jre 中用于启动入口函数 main 的类。我们在 Launcher 中找到以下代码。

 public Launcher() {
     Launcher.ExtClassLoader var1;
     try {
         var1 = Launcher.ExtClassLoader.getExtClassLoader();
     } catch (IOException var10) {
         throw new InternalError("Could not create extension class loader", var10);
     }
     
     try {
         this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
     } catch (IOException var9) {
         throw new InternalError("Could not create application class loader", var9);
     }
     Thread.currentThread().setContextClassLoader(this.loader);
     ...
     }

到此为止,事情就比较明朗了,当前线程上下文的类加载器,是应用程序类加载器。使用它来加载第三方驱动,是没有什么问题的。

我们之所以花大量的篇幅来介绍这个过程,第一,可以让你更好的看到一个打破规则的案例。第二,这个问题面试时出现的几率也是比较高的,你需要好好理解。

案例三:OSGi

OSGi 曾经非常流行,Eclipse 就使用 OSGi 作为插件系统的基础。OSGi 是服务平台的规范,旨在用于需要长运行时间动态更新对运行环境破坏最小的系统。

OSGi 规范定义了很多关于包生命周期,以及基础架构和绑定包的交互方式。这些规则,通过使用特殊 Java 类加载器来强制执行,比较霸道。

比如,在一般 Java 应用程序中,classpath 中的所有类都对所有其他类可见,这是毋庸置疑的。但是,OSGi 类加载器基于 OSGi 规范和每个绑定包的 manifest.mf 文件中指定的选项,来限制这些类的交互,这就让编程风格变得非常的怪异。但我们不难想象,这种与直觉相违背的加载方式,肯定是由专用的类加载器来实现的。

随着 jigsaw 的发展(旨在为 Java SE 平台设计、实现一个标准的模块系统),我个人认为,现在的 OSGi,意义已经不是很大了。OSGi 是一个庞大的话题,你只需要知道,有这么一个复杂的东西,实现了模块化,每个模块可以独立安装、启动、停止、卸载,就可以了。

不过,如果你有机会接触相关方面的工作,也许会不由的发出感叹:原来 Java 的类加载器,可以玩出这么多花样。

如何替换 JDK 的类

让我们回到本课时开始的问题,如何替换 JDK 中的类?比如,我们现在就拿 HashMap为例。

当 Java 的原生 API 不能满足需求时,比如我们要修改 HashMap 类,就必须要使用到 Java 的 endorsed 技术。我们需要将自己的 HashMap 类,打包成一个 jar 包,然后放到 -Djava.endorsed.dirs 指定的目录中。注意类名和包名,应该和 JDK 自带的是一样的。但是,java.lang 包下面的类除外,因为这些都是特殊保护的。

因为我们上面提到的双亲委派机制,是无法直接在应用中替换 JDK 的原生类的。但是,有时候又不得不进行一下增强、替换,比如你想要调试一段代码,或者比 Java 团队早发现了一个 Bug。所以,Java 提供了 endorsed 技术,用于替换这些类。这个目录下的 jar 包,会比 rt.jar 中的文件,优先级更高,可以被最先加载到。

如何动态加载和卸载类

 背景:在Java中,每个类都有相应的Class Loader,同样的,每个实例对象也会有相应的类,当满足如下三个条件时,JVM就会卸载这个类:

1) 该类所有实例对象不可达

2) 该类的Class对象不可达

3) 该类的Class Loader不可达

问:那么实例对象和class对象,还有classLoader之间有什么关系呢?

1)在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。而一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。所以,Class实例和加载它的加载器之间为双向引用关系

2)一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。

扩展:什么样的类可以被卸载?
1)Java虚拟机自带的类加载器(前面介绍的三种类加载器)在JVM运行过程中,会始终存在,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。因此,由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载

2)那么,我们是不是就完全不能在Java程序运行过程中,动态修改我们使用的类了吗?答案是否定的!根据上面的分析,通过Java虚拟机自带的类加载器加载的类无法卸载,我们可以自定义类加载器来加载Java程序,通过自定义类加载器加载的Java类,是可以被卸载的

如上图所示,我们有自定义的类加载器MyClassLoader,用来加载类MyClass,则在JVM中,会存在上面三类引用(上图忽略这三种类型对象对其他的对象的引用)。如果我们将左边的三个引用变量,均设置为null,那么此时,已经加载的MyClass将会被卸载。

动态卸载需要借助于JVM的垃圾收集功能才可以做到

https://zhuanlan.zhihu.com/p/345631770icon-default.png?t=M0H8https://zhuanlan.zhihu.com/p/345631770

你可能感兴趣的:(JVM学习心得,jvm,java,面试)