您的位置:首页 > 编程语言 > Java开发

java 的 ClassLoader 类加载机制详解

2017-03-21 11:28 549 查看
一个程序要运行,需要经过一个编译执行的过程:

Java的编译程序就是将Java源程序 .java 文件 编译为JVM可执行代码的字节码文件 .calss 。Java编译器不将对变量和方法的引用编译为数值引用,也不确定程序执行过程中的内存布局,而是将这些符号引用信息保留在字节码中,由解释器在运行过程中创立内存布局,然后再通过查表来确定一个方法所在的地址。这样就有效的保证了Java的可移植性和安全性。

编译完之后就需要加载进内存,然后执行

类的加载就是将类的.class文件中的二进制数据读进内存之中,将其放进JVM运行时内存的方法区中, 然后在堆中创建一个java.lang.Class对象,用于封装在方法区中类的数据结构,然后 根据这个Class对象,我们可以创建这个类的对象,对象可以有很多个,但是对应的Class对象只有这一个。

备注:关于方法区概念,不懂的,可以看看JVM 的内存机制

这个Class对象就像是一面镜子一样,可以反射一个类的内存结构,因此Class对象是整个反射的入口, 可以通过 (对象.getClass(), 类.Class, 或者Class.forName( 类名的全路径:比如java.lang.String) 三种方式获取到, 通过class对象,我们可以反射的获取某个类的数据结构,访问对应数据结构中的数据,这也是反射机制的基本实现原理。

加载 .class 文件有几种途径

1. 可以 从本地直接加载(加载本地硬盘中的.class文件)

2. 通过网络下载 .class 文件,(java.net.URLClassLoader 加载网络上的某个 .class文件)

3. 从zip,jar 等文件中加载 .class 文件

4. 将java源文件动态的编译为 .class 文件(动态代理)

java的ClassLoader 可以分为两大类,

一: Java 自带的 类加载器

二: 用户自定义的类加载器 (用户可以自己写一个类加载器,但是必须继承自java.lang.ClassLoader类)

类加载的最终产品 创建一个位于堆中方法区的Class对象,每一个class对象都包含有一个对应的classLoader,可以通过class.getClassLoader() 获取到 ,

但是注意上面讲的例子另外较为特殊的是:

数组类的类加载器也可以由这个方法返回,但是 该加载器与里面的元素的类加载器的信息相同,如果里面的元素为基本类型,String , void型(jdk 1.5之后将void 纳入基本数据类型),那么返回的类加载器也是null

Java 自带的类加载器可以分为三类:

一: 根加载器(Bootstrap): 主要用来加载java的核心API,根加载器加载出来的Class对象,我们是无法访问到的,底层

* 是C++实现的,JVM也没有暴露根类加载器

二: 扩展类加载器(Extension): 主要加载java 中扩展的一些jar包中的文件,比如你的项目中放在System Library 里的jar包

* 或者你导入的其他jar包

三:应用类加载器(AppClassloader): 也叫系统类加载器(system)主要用来加载一些用户自己写的类的.class文件

类加载器加载的过程可以分为三步

第一: 加载 这个阶段就是将class文件从文件或者本地存储中读取到内存中的过程

第二: 连接主要实现的就是将已经读入到内存中的二进制class数据合并到JVM的运行时环境中去。在没有实现这一步之前,class 都是一个个单独的存放在内存中的二进制文件,他们之间没有任何联系,只有JVM把他们连接起来,才能将每个类之间的关系有机的结合起来。

* 连接又分为三小步

1. 验证: 验证加载类的正确性。 有人说class文件不是JVM编译成的字节码吗,还需要验证吗? 答案是肯定的,在上一步读取class文件的时候,是不做内容检查的,有可能你把一个文件的后缀名修改为.class, 它也会被读进内存,但是在验证就是要检验你的.class文件是不是定义的一个正确的类,是不是通过JVM编译而成的,当然因为java是开源的,一些第三方(如CGlib)也可以生成符合加载的字节码文件, 为了安全起见,重新在进行一次编译检查

验证实现功能: (类文件的结构检查,字节码验证,二进制兼容性验证,语义检查)

2. 准备: 为类的静态变量分配内存空间,并初始化这些变量为一个默认的值。 一定要看清这里是为类的静态变量分配的内存空间而且这里也有一个初始化的工作(初始化静态变量为默认值,int 型的为 0, boolean 型的为 false, 对象为null),与下面的初始化是有区别的。

3. 解析: 将类中的符号引用解析为直接引用。 在java语言里,我们说是没有指针的, 实现通过引用去访问对象,一般两种实现方式,(句柄和直接引用)。 在下面的例子中,Woker 类中调用了Car类中的方法, 因此在运行的时候,JVM把这里的一个符号引用替换为一个指针(这里是指替换成真正由C++实现的指针),我们称之为直接引用

class Worker{
public void dirveCar(){
Car.run();
}
}


第三: 初始化: 哎,你可能会产生疑问,上面不是有一次初始化了,这次的初始化是干什么的? 是为类的静态变量赋予正确的初始值 ,注意到,我这里加了一个正确的初始值,在上面的连接那部分里,也有一次初始化工作的,当时设置的是默认值,由此引发的不同,在下面的代码示例中讲解了由于两种初始化工作所造成的一个很惊讶的结果,详细看下面。

静态变量的初始化一般有两种方式:

第一种 在静态变量的声明处进行初始化赋值

private static int a = 2;


第二种 在静态代码块中赋值

private static int a;
static{
a = 2;
}


类的初始化时机

请千万注意,一般来讲当类加载进JVM中的时候执行到上面的一步 连接 就结束了,这次的初始化工作是要经过一定条件的触发才会执行的, 触发的条件是什么呢? 是当程序主动使用该类的时候才会进行为静态变量赋予正确初始值的初始化工作

* **程序主动使用类(六种情况)**:
* 1. 创建类的实例
* 2. 访问某个类或者接口的静态变量,或者对该静态变量赋值
* 3. 调用类的静态方法(一定是调用当前类的静态方法或者静态变量, 比如如果调用子类的一个方法,但是静态方法或者变量实际上是在父类中的,因此只会初始化父类,调用的这个子类反而不会进行初始化)
* 4. 反射   Class.forName()
* 5. 初始化一个类的子类(继承关系是会先初始化父类,但是如果实现的是接口,并不会先初始化它的接口,只有当程序首次使用接口特定的静态变量时才会初始化接口)
* 6. java虚拟机启动时被标明为启动类的类(含有main 方法,并且是启动方法的类 )


类的加载时机

类的加载并不像类的初始化工作一样,必须要等到程序主动调用的时候。

JVM 允许类加载器预料到某个类将要被使用的时候就提前加载它,如果在加载的过程中 遇到了class文件缺失或者错误,那么 在程序主动调用的时候要报告这个linkageError,如果程序一直没有使用到这个类,那么类加载器就不会报告错误,这个错误一般是版本的不兼容型错误,比如你在jdk 1.6 编译下的class文件 加载到 jdk 1.5的环境中就有可能出错。

类加载器的父亲委托机制

这种机制能够更好的保证java平台的安全,在此委托机制中,除了虚拟机自带的根加载器(bootstrap Classloader)之外,其他所有的类加载器有且只有一个父加载器当 java程序请求类加载器 loader1 加载某一个类时,首先委托其父类的加载器进行加载,如果能够加载,则由父类加载器进行加载,如果不能加载,则由自己进行加载

类加载器的调用顺序, 根加载器—-扩展类加载器—–系统(应用)类加载器 —- 自定义的类加载器

父类委托机制 和 类的调用顺序 这一切的原因都是出于安全性的考虑, 这样用户自定义的不可靠的类加载器无法加载本该由父类加载器加载的可靠的类, 采用这些就避免了不可靠甚至恶意的代码加载那些java的核心类库,从而保证了类加载的安全性。

举例子: java.lang.Object 由根类加载器加载,如果没有父类委托机制,那么你自定义的类加载器就可以加载这个类,那么程序就会变得极其的不安全,不稳定。

根加载器 : 无父类加载器,并没有继承java.lang.ClassLoader类,加载java的核心库 ,比如java.lang

扩展加载器: 其父类加载器是根加载器,加载jre/lib/ext中的类或者系统属性中指定的目录 java.ext.dirs

应用类加载器(系统类加载器) : 其父类加载器是扩展类加载器; 从环境变量path路径中或者 java.class.path 指定目录中加载类, 同时他也是用户自定义的类加载器的默认父类加载器

父类加载器机制并不意味着各个子类加载器继承了父类的加载器,他们其实是一种组合关系,一种包装关系。

Classloder Loader1 = new myClassloader();
// 将loader1 作为 loader2 的父亲加载器 (由此可以看出两者并不是继承关系), 如果你自定义了一个加载器,但是没有写下面的这句,给它指定父类加载器,那么默认的父类加载器是系统类加载器
ClassLoader loader2 = new myClassloader(loader1)
// 其实是在ClossLoader类中都拥有着一个 protected 变量parent,代表着它的父亲加载器, 将上面一句实际执行的是  parent = loader1;


运行时包: 同一个运行包是指类在同一个包下,并且由同一种类加载器实际加载。 安全考虑,原因在下面



两个由不同类加载器加载的类(这两个类之间无父亲委托关系)相互之间是不可见的,只能通过反射访问 但是子加载器加载的类能够看见父加载器加载的类,我们平常编程的时候,基本上都是SystemcloassLoader 加载的,在同一个命名空间内,而且其他的类都是由 系统加载器的父类加载的,我们也可以访问 java.lang.String 或者其他的类。



此外类加载还采用了****cache机制,也就是如果 cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但是必须重新启动JVM才能生效的原因。

类加载器的卸载: java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期内,是不可能被卸载的,因为有JVM始终指向他们,但是用户自定义的类加载器是可以被卸载的。卸载的方式是 无引用指向它们

loader1 = null;
clazz = null; // 一个class对象始终引用它的类加载器
object = null; // 一个实例对象始终引用它的class对象




package ClassLoder;

public class ClassLoderTest {
public static void main(String[] args) {
/*
* 8种基本数据类型 及其 对象型, String类型, void 类型的 他们的类加载器是boot(根)加载器
* boot类加载器,是无法被我们所获得的,因此java 给我们返还给一个null
*/
ClassLoader stringLoader = String.class.getClassLoader();
ClassLoader intLoader = int.class.getClassLoader();
ClassLoader IntegerLoader = Integer.class.getClassLoader();
System.out.println(stringLoader); // null
System.out.println(intLoader); // null
System.out.println(IntegerLoader); // null

// 是不是觉得结果很诡异,这就是因为是两个不同的初始化工作所造成的不同结果,
// 当调用了 LoaderTest 的静态方法, 就会对该类的静态变量进行初始化的工作,首先将 a =0, b = 5,
//           然后在为loader1赋值 执行了 +1 操作, a = 1, b = 6
// 当调用了LoaderTest2 的静态方法,顺序的为静态变量赋值,那么首先赋值的就是 loader2 对象,
//          但是注意的是此时的 a 和 b 还没有赋予真正的初始值,他们的值仍旧为默认的 0, 执行了加1 ,a和b都成了 1
//          然后执行下面的语句,由对a 与 b 的值进行了覆盖,a = 0, b = 5
LoaderTest.getLoderTest();
LoaderTest.getA();  // 输出结果 1, 6
LoaderTest2.getLoderTest2();
LoaderTest2.getA();  // 输出结果 0, 5

// 加上静态代码块之后 结果分别为 2, 7
//                          1,  6
// 这是因为类在加载的时候顺序是 静态变量初始化的工作和静态代码块的工作就是按照在代码中出现的顺序依次执行的

}
}
// 测试加载的过程顺序
class LoaderTest{
static int a;
static int b = 5;
private static LoaderTest  loader1 = new LoaderTest();
public LoaderTest(){
a++;
b++;
}
public static void getA(){
System.out.println(a +"     " + b);
}
public static LoaderTest getLoderTest(){
return  loader1;
}
// 静态代码快是在值被初始化之后才开始执行的,  但是优先于构造函数和其他方法执行
//  static{
//      a++;
//      b++;
//      System.out.println("static 执行了");
//      System.out.println(a +"     " + b);
//  }
}

class LoaderTest2{
private static LoaderTest2  loader2 = new LoaderTest2() ;
private  LoaderTest2(){
a++;
b++;
}
private static int a = 0;
private static int b = 5;
public static void getA(){
System.out.println(a +"     " + b);
}
public static LoaderTest2 getLoderTest2(){
return  loader2;
}
//  static{
//      a++;
//      b++;
//      System.out.println("static2 执行了");
//      System.out.println(a +"     " + b);
//  }
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: