JVM类加载机制

一、JVM类加载机制

 虚拟机把描述类的数据从 .class 文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的 java 类型。 

 

 JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。

 

 1.1 加载

  加载是类加载过程中的一个阶段,这个阶段会把 .class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行数据结构,在堆内存中生成一个代表这个类的 java.lang.Class 对象作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。

  即该阶段主要完成以下三件事:

   1.1.1 通过一个类的全限定名获取该类的二进制流。

   1.1.2 将该二进制流中的静态存储结构转化为方法去运行时数据结构。

   1.1.3 在内存中生成该类的 Class 对象,作为该类的数据访问入口。

 1.2 验证

  这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 

  在该阶段主要完成以下四钟验证:

   1.2.1 文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

   1.2.2 元数据验证:  对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

   1.2.3 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。

   1.2.4 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

 1.3 准备 

  准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为: 

  public static int v = 8080;

  实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080即这里的初始值是默认值而不是自己赋的值,将 v 赋值为 8080 的 put static 指令是程序被编译后,存放于类构造器<client>方法之中。

  但是注意如果声明为:  
  public static final int v = 8080;

  在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v赋值为 8080。 

 1.4 解析

  解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的: CONSTANT_Class_info ; CONSTANT_Field_info  ; CONSTANT_Method_info   等类型的常量。

  1.4.1 符号引用 

   符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。 

  1.4.2 直接引用 

   直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。 

 1.5 初始化

  初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。 

  初始化阶段是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量(静态变量)的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子<client>方法执行之前,父类的<client>方法已经执行完毕;如果一个类中没有对类变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()方法;虚拟机会保证一个类的<client> 方法在多线程环境下被正确的加锁和同步;当访问一个类的静态域时,只有真正声明这个域的类才会被加载。

  1.5.1 注意以下几种情况不会执行类初始化,类的被动引用不会触发此类的初始化: 

    1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

    2. 定义对象数组即通过数组定义类引用,不会触发该类的初始化。

    3. 引用常量不会触发此类的初始化,常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

    4. 通过类名获取 Class 对象,不会触发类的初始化。

    5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。

    6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。 

  1.5.2 类的主动引用(一定会发生类的初始化)

    1. new一个类的对象

    2. 调用类的静态成员(除了final常量)和静态方法

    3. 使用java.lang.reflect包的方法对类进行反射调用(参数 initialize 为 true )

    4. 当虚拟机启动,java Hello,则一定会初始化Hello类。说白了就是先启动main方法所在的类

    5. 当初始化一个类,如果其父类没有被初始化,则先会初始化他的父类

二、类加载器

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

 类缓存: 标准的Java SE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过,JVM垃圾收集器可以回收这些Class对象

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

  

  2.1.1 启动类加载器(Bootstrap ClassLoader)

   负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。用来加载 java 核心类库,无法被 java 程序直接引用。是用原生代码来实现的,并不继承自 java.lang.ClassLoader

   加载扩展类和应用程序类加载器。并指定他们的父类加载器。

  2.1.2 扩展类加载器(Extension ClassLoader)

   负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

  2.1.3 应用程序类加载器(Application ClassLoader)

   负责加载用户路径(classpath)上的类库。一般来说,Java 应用的类都是由它来完成加载的。JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。

  2.1.4 自定义类加载器

   开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

  

 2.2 java.class.ClassLoader类

  2.2.1 作用

   java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java 类,即 java.lang.Class类的一个实例。

  2.2.2 相关方法

   getParent() :返回该类加载器的父类加载器。

   loadClass(String name) :加载名称为 name的类,返回的结果是 java.lang.Class类的实例。

   findClass(String name) :查找名称为 name的类,返回的结果是 java.lang.Class类的实例。

   findLoadedClass(String name) :查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。

   defineClass(String name, byte[] b, int off, int len) :把字节数组 b中的内容转换成 Java 类,返回的结果是java.lang.Class类的实例。这个方法被声明为 final的。

   resolveClass(Class<?> c) :链接指定的 Java 类。

  对于以上给出的方法,表示类名称的 name参数的值是类的二进制名称。需要注意的是内部类的表示,如com.example.Sample$1和com.example.Sample$Inner等表示方式。

 

 2.3 类加载器的代理模式------双亲委派机制

  2.3.1 代理模式:交给其他加载器来加载指定的类

  2.3.2 双亲委托机制

   就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次追溯,直到最高的爷爷辈的,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

   双亲委托机制是为了保证 Java 核心库的类型安全。这种机制就保证不会出现用户自己能定义java.lang.Object类的情况。

   采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

   类加载器除了用于加载类,也是安全的最基本的屏障。

  2.3.3 双亲委托机制是代理模式的一种,并不是所有的类加载器都采用双亲委托机制tomcat服务器类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。

与一般类加载器的顺序是相反的。

 

 2.4 自定义类加载器的流程

  1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2

  2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真个虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3

  3、调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转抛异常,终止加载过程(注意:这里的异常种类不止一种)。

   注意:被两个类加载器加载的同一个类,JVM不认为是相同的类。

 

 2.5 线程上下文类加载器

  2.5.1 双亲委托机制以及默认类加载器的问题

   一般情况下, 保证同一个类中所关联的其他类都是由当前类的类加载器所加载的.。比如,ClassA本身在Ext下找到,那么他里面new出来的一些类也就只能用Ext去查找了(不会低一个级别),所以有些明明App可以找到的,却找不到了。

   JDBC API,他有实现的driven部分(mysql/sql server),我们的JDBC API都是由Boot或者Ext来载入的,但是JDBC driver却是由Ext或者App来载入,那么就有可能找不到driver了。在Java领域中,其实只要分成这种Api+SPI(Service Provide Interface,特定厂商提供)的,都会遇到此问题。

   常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers 包中。SPI 的接口是 Java 核心库的一部分,是由启动类加载器来加载的;SPI 实现的Java 类一般是由应用程序类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。

  2.5.2 通常当你需要动态加载资源的时候 , 你至少有三个 ClassLoader 可以选择 :

   1.系统类加载器或叫作应用类加载器 (system classloader or application classloader)

   2.当前类加载器

   3.当前线程类加载器

  2.5.3 • Thread.currentThread().getContextClassLoader()

   当前线程类加载器是为了抛弃双亲委派加载链模式。每个线程都有一个关联的上下文类加载器。如果你使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文类加载器。如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上下文类加载器。

 

 2.6 TOMCAT服务器的类加载机制

  一切都是为了安全!TOMCAT不能使用系统默认的类加载器。

      

    • 如果TOMCAT跑你的WEB项目使用系统的类加载器那是相当危险的,你可以直接是无忌惮是操作系统的各个目录了。

    • 对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。

    • 每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式(不同于前面说的双亲委托机制),所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。但也是为了保证安全,这样核心库就不在查询范围之内。

    • 为了安全TOMCAT需要实现自己的类加载器。

    • 我可以限制你只能把类写在指定的地方,否则我不给你加载!

 

三、OSGI(动态模型系统)

 OSGI (Open Service Gateway Initiative),是面向 Java 的动态模型系统,是 Java 动态化模块化系统的一系列规范

 它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGI 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse就是基于 OSGI 技术来构建的。

 3.1 原理

  OSGI 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGI 特有的类加载器机制来实现的。OSGI 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。

 3.2 特点

  3.2.1 动态改变构造

   OSGI 服务平台提供在多种网络设备上无需重启的动态改变构造的功能。为了最小化耦合度和促使这些耦合度可管理,OSGI 技术提供一种面向服务的架构,它能使这些组件动态地发现对方。

  3.2.2 模块化编程与热插拔

   OSGI 旨在为实现 Java 程序的模块化编程提供基础条件,基于 OSGI 的程序很可能可以实现模块级的热插拔功能,当程序升级更新时,可以只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说是非常具有诱惑力的特性。

   OSGI 描绘了一个很美好的模块化开发目标,而且定义了实现这个目标的所需要服务与架构,同时也有成熟的框架进行实现支持。但并非所有的应用都适合采用 OSGI 作为基础架构,它在提供强大功能同时,也引入了额外的复杂度,因为它不遵守了类加载的双亲委托模型。

 

posted @ 2020-07-15 08:42  FireCode  阅读(369)  评论(0编辑  收藏  举报