JVM之类加载机制和类加载器 (二)

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

类加载的全过程

类从被加载到虚拟机内存开始,到卸载。生命周期包括七个阶段,如下图:

注意:加载--->验证--->准备--->初始化--->卸载这5个阶段的顺序是确定的,而解析可能为了支持Java语言的运行时绑定会在初始化后才开始。通常上述阶段都是互相交叉地混合进行的。另外,类加载无需等到程序中“首次使用”的时候才开始,JVM预先加载某些类也是被允许的。(类加载的时机)

什么情况下需要开始类加载过程的第一个阶段:加载,这点交给虚拟机的具体实现来自由把握。  加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

1. 从本地文件系统加载class文件,这是绝大部分程序的类加载方式。

2. 从JAR包加载class文件,这种方式也是很常见的,如JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。

3. 通过网络加载class文件。

4. 把一个Java源文件动态编译,并执行加载
类的连接

类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接分为三个阶段:

1、验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。

2、准备:为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。如static int a = 100;静态变量a就   会在准备阶段被赋默认值0。  对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。

另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int a = 888;  静态常量a就会在准备阶段被直接赋值为888,对于静态变量,这个操作是在初始化阶段进行的。

3、解析:将类的二进制数据中的符号引用换为直接引用。

类的初始化

类初始化是类加载的最后一步,除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行Java代码。

类的初始化的主要是为静态变量赋程序设定的初值。

如何才会进行类的初始化呢?

类的初始化(类加载时机)

1. 创建类的实例,也就是new一个对象

2. 访问某个类或接口的静态变量,或者对该静态变量赋值

3. 调用类的静态方法

4. 对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。

5. 初始化一个类的子类(会首先初始化子类的父类)

6. JVM启动时标明的启动类,即文件名和类名相同的那个类  

类加载器:

类加载器负责加载所有的类,它为所有被载入内存中的类生成一个java.lang.Class实例对象。一但一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。

 JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

1. 根类加载器(bootstrap class loder):它是加载java的核心类(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

2. 扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

3. 系统类加载器(system class loader):被称为系统(也称为应用)类加载器,负责加载用户类路径上所指定的类库。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。如果以上类加载器不能满足需求,可以自定义类加载器。

public class MyLoaderTest {
    public static void main(String[] args) {
        //获得系统类加载器,即应用程序类加载器
        System.out.println(ClassLoader.getSystemClassLoader());
        //应用程序类加载器的父类是扩展类加载器
        System.out.println(ClassLoader.getSystemClassLoader().getParent());
        //扩展类加载器的父类是引导类加载器,但它是由c++实现的,java获取不到
        System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());

        //系统类加载器的处理路径
        System.out.println(System.getProperty("java.class.path"));

        String a="meme";
        System.out.println(a.getClass().getClassLoader()); //加载的是jdk提供的lang.String,而不是自己定义的
        System.out.println(a); //不会打印自己定义的toString

    }

类加载机制

JVM的类加载机制主要分为3种:

1. 双亲委派机制:双亲委派机制先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

2. 全盘负责机制:全盘负责机制就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

3. 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。


此处着重说明双亲委派机制:

双亲委派机制的优势:

1. 可以防止核心API库被随意篡改(如自己写的String类不会被加载)。

2. 可以避免类的重复加载,简单理解为当父类已经加载了该类,子类加载器就没必要再加载一次,沙箱安全机制

如有不同见解,欢迎留言指正,望不吝赐教!!!

发布了91 篇原创文章 · 获赞 1 · 访问量 3199

猜你喜欢

转载自blog.csdn.net/duan196_118/article/details/104215262