虚拟机类加载器

上篇博客我大概讲了下《java虚拟机分析以及GC 》,讲的不好之处请指出《java虚拟机分析以及GC 》


今天看《深入理解Java虚拟机》有点小体会,特写这篇博客
首先话不多说,看图
这里写图片描述
从图中我们很容易看出:一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接


类加载的规则
1、首先加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类加载过程必须按照这种顺序按部就班的开始(注意不是按部就班的进行或者完成,因为这些阶段通常是互相交叉混合‘进行’的)
2、解析阶段则不一定;它在某些情况下可以在初始化的阶段之后再开始,这是为了支持java语言的运行时绑定


我将会按照上图的顺序介绍类加载机制


加载

  1. 加载需要完成的功能有:

     1.通过一个类的全限定名来获取定义此类的二进制节流
     2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
     3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
    
  2. 如何获取一个二进制字节流,也就是通常我们说的.class文件

    1. 从zip包中读取,这很常见,我们开发中经常用到的jar、ear、war等格式中获取class文件
    2. 从网络中获取,已经基本不用的applet就是这种方式获取
    3. 运行时计算机生成,动态代理技术,这里有自己写的采用配置文件实现的动态代理
    4. 由其他文件生成,典型场景是jsp应用
    5. 从数据库中读取,很少用到
  3. 注意一点就是数组类本身不通过类加载器创建,它是由java虚拟机直接创建,但是数组类与类加载器仍然有密切的关系,其中最重要的是数组类的元素类型最终还是要类加载器创建,详细介绍请参见《深入理解java虚拟机–类加载的过程》这一章


验证

  1. 验证需要实现的功能

    1. 是否以魔数0xCAFEBABE开头,关于魔数可以理解成为是一个公司的logo, cafebabe也可以看出java原本的标记是一个cofe
    2. 主、次版本号是否在当前虚拟机处理范围之内:高版本虚拟机兼容低版本的字节码文件、低版本的虚拟机不能兼容高的字节码文件
    3. 常量池的常量中是否有不被支持的常量类型
    4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
    5. CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
    6. 是否Class文件本省有没有被修改
  2. 元数据验证

    1. 这个类是否有父类(所有的类都有父类,除了Object类以外)
    2. 这个类的父类是否继承了不允许被继承的类
    3. 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的方法
    4. 类中的字段、方法是否与父类产生矛盾(出现了不和规则的重载)
  3. 字节码验证:主要目的是通过数据流和控制流分析确定程序语义是否合法、符合逻辑

    1. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
    2. 保证跳转指令不会跳转到方法体以外的字节码指令上
    3. 保证方法体中的类型转换是有效的
  4. 符号引用验证

    1. 符号引用中通过字符串描述的全限定是否能找到对应的类
    2. 在指定类中是否存在符合方法描述符以简单名称所描述的方法和字段
    3. 符号引用中的类、方法的访问性是否符合标准

准备:正式为类变量分配内存并设置类变量的初始值(是类型的初始值,而不是代码阶段的初始值:int a = 10,这个阶段的a = 0)的阶段
下图是基本类型变量的初始值
这里写图片描述
但是有特殊情况,如:public static final int value = 123;这里的value的初始值直接是123,而不是0


解析:虚拟机将常量池内的符号引用替换为直接引用的过程,在这里我先描述下什么叫符号引用,什么叫直接引用

符号引用(Symbolic References):符号引用以一组符号来描述所引用的对象,符号可以是任何形式的字面量(其实就是User user = new User()中user就是符号引用)
直接引用:可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。如上文讲的user符号引用直接转化为指向User这个对象的指针

解析动作主要是针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。具体的进行的解析请自行查阅《深入理解java虚拟机—关于7.3.4解析》一章。我这里就不做具体的解说


初始化:

  1. java程序对类的使用方式分为两种:主动引用、被动引用
  2. 主动引用
    1.创建类的实例
    2.访问某个类或接口的静态变量,或者对静态变量赋值
    3.调用类的静态方法
    4.反射技术
    5.初始化一个类的子类
    6.java虚拟机启动时被标明为启动类的类
  3. 被动引用
    1.通过子类引用父类的静态字段、不会导致子类的初始化
package com.xingyao.ConstClass;
//父类
public class FatherClass {
    //静态代码块,类初始化被调用
    static {
        System.out.println("fatherClass init");
    }
    //静态变量
    public static int value = 123;
}
//子类
public class SubClass extends ConstClass {
    //静态代码块,类初始化被调用
    static {
        System.out.println("sonClass init");
    }
}
//调用
public class InitClass {

    public static void main(String[] args) {

        System.out.println(SubClass.value);

    }
}

运行结果如下
这里写图片描述

2. 通过数组定义类引用类、不会触发此类的初始化
    1.这里要提的事:String [] strs = new String[10],输出的类为“class [Ljava.lang.String;” 对于用户代码来说,这并不是一个合法的类名称,它是由虚拟机自动生成的,直接继承与java.lang.Object的子类,创建动作由字节码指定newarray触发

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

使用:执行程序,这里没有什么可以讲的


卸载:我理解成为一个程序的死亡,一个程序的死亡有这几种方法
1. 执行了System.exit()方法
2. 程序正常执行结束
3. 程序在执行过程中遇到了异常情况
4. 操作系统的错误而导致java虚拟机的进程的终止


好了,讲完了一个类从创建到死亡的过程,下文开始讲类加载器的机制:双亲委托机制及自定义类加载器

  1. 双亲委托机

描述:某个特定的类加载器在接到加载类的请求时,首先将加载任务托给父类加载器,依次递归。如果父类加载器可以完成加载任务,就返回成功;如果父类加载器无法完成任务时,才自己加载,最终无法加载成功就抛出ClassNotFoundException;所以用系统的类加载器无法定义一个String的类

2.java从顶层到底层有三个类加载器

  • 启动(Bootstrap)类加载器:是用本地代码实现的类加载器,他负责< java_Runtime_Home>/lib下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地的代码实现,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
  • 标准扩展(Extension)类加载器:是由sun的ExtClassLoader实现的,它负责将< java_Runtime_Home>/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器
  • 系统(System)类加载器:是由Sun的AppClassLoader实现的。他负责将系统类路径中指定的类库加载到内存中。开发者可以直接使用系统类加载器

3.自定义一个类加载器

1、首先要继承ClassLoader类,然后重写findClass方法,抛出ClassNotFoundException异常
再写一个方法,使用字节流加载class文件

代码如下

package com.xingyao.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;


/**
 * @author xingyao
 * @version 1.0
 * @since 2016.06.8
 */
public class MYClasssLoader extends ClassLoader {

    private String name;            //类加载器的名字

    private String path = "d:\\";       //加载类的路径

    private final String fileType = ".class";       //class文件的扩展名

    public MYClasssLoader(String name){

        super();            //让系统类加载器成为该类加载器的父加载器

        this.name = name;

    }

    public MYClasssLoader(ClassLoader parent,String name) {

        super(parent);      //显示指定该类加载器的父加载器

        this.name = name;
    }

    @Override
    public String toString() {
        return this.name;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }


    /**
     * 通过类名找到Class对象
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Class findClass(String name)throws ClassNotFoundException{

        byte[] data = this.loadClassData(name);

        return this.defineClass(name, data, 0, data.length);

    }

    /**
     * 同过类名,得到一个byte类型的对象
     * 就是将XXX.class变成二进制流
     * @param name
     * @return
     */
    private byte[] loadClassData(String name){

        InputStream is = null;
        byte [] data = null;
        ByteArrayOutputStream bos = null;
        try{
             this.name = this.name.replace(".", "\\");

             is = new FileInputStream(new File(path+name+fileType));

             bos = new ByteArrayOutputStream();

             int ch = 0;

             while(-1 != (ch = is.read())) {
                 bos.write(ch);
             }

             data = bos.toByteArray();

        }catch(Exception ex) {
            ex.printStackTrace();
        }finally {
            if(bos != null) {
                try {
                    bos.close();
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return data;
    }
}

需要加载的类
package com.xingyao.classloader;

public class Sample {

    public int v1 = 1;

    public Sample() {
        System.out.println("sample is loaded by :"+Sample.class.getClassLoader());
    }

}

测试代码如下
package com.xingyao.classloader;

public class Test_MYClassLoader {

    public static void main(String[] args) {
        MYClasssLoader loader1 = new MYClasssLoader("loader1");

        loader1.setPath("d:\\myapp\\serverlib\\");

        MYClasssLoader loader2 = new MYClasssLoader(loader1, "loader2");

        loader2.setPath("d:\\myapp\\clientlib\\");

        MYClasssLoader loader3 = new MYClasssLoader(null,"loader3\\");

        test(loader1);
    }

    public static void test(ClassLoader loader) {
        try {
            Class clazz = loader.loadClass("com.xingyao.classloader.Sample");
            try {
                Object obj = clazz.newInstance();
            } catch (InstantiationException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

运行结果如下
这里写图片描述

用如果你实现用cmd命令行编译Sample.java文件,然后用myEclipse运行Test_MYClassLoader可能会遇到版本问题:cmd的运行版本高于myEclipse的jre版本,则需要配置下MyEclipse的jre版本
这里写图片描述

下一篇讲集合类的底层代码实现的差别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值