JAVA虚拟机类加载机制详解

JVM类加载机制详解

  • 在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的。
  • 提供了更大的灵活性,增加了更多的可能性(加载时稍微增加了一些性能开销)。

类在虚拟机中的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。

生命周期

加载

查找并加载类的二进制数据。类的加载的最终产品是位于内存找那个的class对象

验证

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

准备

为类的静态变量分配内存,并将其初始化为默认值。但时在到达初始化之前,类变量都没有初始化为真正的初始值。

数据类型零值数据类型零值
int0booleanfalse
long0Lfloat0.0f
short(short)0double0.0d
char‘\u000’referencenull
byte(byte)0

解析

在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用转换为直接引用

初始化

在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予正确的初始值。静态变量的初始化有两种途径:

  1. 在静态变量的声明处进行初始化
  2. 在静态代码块中进行初始化。
//初始化后,a = 3, b = 0;
public class Sample{
   static int a = 1;
   static int b;
   static{
      a = 3;
   }
}

类的初始化步骤:

  1. 假如这个类还没有被加载和连接,那就先进行加载和连接。
  2. 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类。
  3. 假如类中存在初始化语句,那就依次执行这些初始化语句。

类的初始化时机

Java程序对类的使用方式分为两种:

  • 主动使用
  • 被动使用

所有的Java虚拟机实现必须在每个类或接口被Java程序 首次主动使用 时才初始化他们。

主动使用(七种)有且只有

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
    • 以上三种在jvm中表示为 使用 new、getstatic、putstatic或invokestatic这四个字节码指令时,如果类没进行初始化,则需要先触发初始化
  4. 反射(如Class.forName(“com.test.Test”))
  5. 初始化一个类的子类
  6. java虚拟机启动时被标明为启动类的类
  7. JDK1.7开始提供的动态语言支持

使用

实例化

  • 为对象分配内存
  • 实例变量赋默认值
  • 为实例变量赋正确的值

卸载

当类被加载、连接和初始化后,他的生命周期就开始了。当类的Class对象不再被应用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载,从而结束类的生命周期。

  • 一个类何时结束生命周期,取决于代表他的Class对象何时结束生命周期。

  • java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。

  • 由用户自定义的类加载器所加载的类是可以被卸载的。

类加载器

有两种类型的类加载器

  1. java虚拟机自带的加载器
    • 根类加载器(Bootstrap)
    • 扩展类加载器(Extension)
    • 系统(应用)类加载器(System)
  2. 用户自定义的类加载器
    • java.lang.ClassLoader的子类
    • 用户可以定制类的加载方式

当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器和系统类加载器,这块特殊的机器码叫做启动类加载器(Bootstrap)。启动类加载器并不是Java类,是特定于平台的机器指令,它负责开启整个加载过程,加载其他类加载器(都是Java类),并负责加载供JRE正常运行所需要的基本组件,包括java.util和java.lang包中的类等等。

类加载器在预料到某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件确实或存在错误,类加载器必须在程序首次主动使用该类是才报告错误(LinkageError)

jvm启动参数中有关类加载路径的的参数

BootstrapClassLoader的加载路径:-Dsun.boot.class.path=加载路径;修改则报错

ExtClassLoader的加载路径: -Djava.ext.dirs=加载路径

AppClassLoader的加载路径: -Djava.class.path=加载路径

获取ClassLoader的途径

  • 获得当前类的ClassLoader
    clazz.getClassLoader();

  • 获得当前线程上线文的ClassLoader
    Thread.currentThread().getContextClassLoader();

  • 获得系统的ClassLoader
    ClassLoader.getSystemClassLoader()

  • 获得调用者的ClassLoader
    Reflection.getCallerClass()

自定义ClassLoader类加载器

  • 所有自定义ClassLoader均要继承java.lang.ClassLoader
  • 自定义ClassLoader中编写loadClassData方法,将文件转换为byte[]写入内存,使用defineClass方法,将byte[]转换为Class<?>

以下引用自JDK8 java doc文档

Normally, the Java virtual machine loads classes from the local file system in a platform-dependent manner. For example, on UNIX systems, the virtual machine loads classes from the directory defined by the CLASSPATH environment variable.
However, some classes may not originate from a file; they may originate from other sources, such as the network, or they could be constructed by an application. The method defineClass converts an array of bytes into an instance of class Class. Instances of this newly defined class can be created using Class.newInstance.
The methods and constructors of objects created by a class loader may reference other classes. To determine the class(es) referred to, the Java virtual machine invokes the loadClass method of the class loader that originally created the class.

For example, an application could create a network class loader to download class files from a server. Sample code might look like:

ClassLoader loader = new NetworkClassLoader(host,port);
Object main = loader.loadClass("Main", true).newInstance();
. . .

The network class loader subclass must define the methods findClass and loadClassData to load a class from the network. Once it has downloaded the bytes that make up the class, it should use the method defineClass to create a class instance. A sample implementation is:

class NetworkClassLoader extends ClassLoader {
String host;
int port;


   public Class findClass(String name) {
       byte[] b = loadClassData(name);
       return defineClass(name, b, 0, b.length);
   }

   private byte[] loadClassData(String name) {
       // load the class data from the connection
        . . .
   }
}

类加载器的命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。

  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类

  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

原理:

  1. 因为双亲委派机制,类加载器加载Class时,总是先由其父类加载器加载
  2. 每次加载时都会调用findLoadedClass方法,查找该Class是否被本类加载器加载。

Returns the class with the given binary name if this loader has been recorded by the Java virtual machine as an initiating loader of a class with that binary name. Otherwise null is returned.

在运行期,一个Java类是由该类的完全限定名(binary name 二进制名)和用于加载该类的定义类加载器所共同决定的。如果同样名字的类是由两个不同的类加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载亦如此。

不同类加载器的命名空间关系
  • 同一个命名空间内的类是相互可见的

  • 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。

  • 由父加载器加载的类不能看见子加载器加载的类。

  • 如果两个类加载器之间没有直接或间接的父子关系,那么他们各自加载的类相互不可见。

类加载器的双亲委托模型的好处
  1. 可以确保Java核心库的类型安全:所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到Java虚拟机中;如果这个加载过程是由Java应用自己的类加载器所完成的,那么很有可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类还不兼容,不想不可见借助于双亲委托模式,Java核心类库的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的的都是同一个版本的Java核心库,他们之间是互相兼容的
  2. 可以确保java核心类库所提供的类不会被自定义的类所替代
  3. 不同的类加载器可以为相同名称的类创建额外的命名空间。相同名称的类可以并存于JVM中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的。这就相当于在Java虚拟机中创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用。

线程上下文类加载器

上下文类加载器是从JDK1.2开始引入的,类Thread中getContextClassLoader()setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器,如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文累加载器。Java应用运行时的初始线程的上下文的类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源。

线程上下文类加载器的重要性

SPI(Service Provider Interface)

  • 父ClassLoader可以使用当前线程Thread.currentThread().getContextLoader()所指定的classloader加载的类。这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型。
    线程上下文类加载器就是当前线程的Current ClassLoader。
  • 在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的Jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以有设置的上下文类加载器来实现对于接口实现类的加载。

线程类加载器的一般使用模式:获取 - 使用 - 还原

ClassLoader classLoader = Thread.currentThread.getContextClassLoader();
try{
   Thread.currentThread.setContextClassLoader(targetTccl);
   myMethod();
}finally{
   Thread.currentThread().setContextClassLoader(classLoader);
}
  • myMethod 里面则调用了Thread.currentThread().getContextClassLoader(), 获取当前线程的上下文类加载器做某些事情。 如果一个类加载器由类加载器A加载。 那么这类的依赖类也是由相同的类加载器加载的。(如果该依赖类之前没有被加载过的话)

  • ContextClassLoader 的作用就是为了破坏Java的类加载委托机制。

  • 当前高层提供统一接口让低层去实现。同时又要在高层加载(或实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader来找到并加载该类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值