类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
没有初始化。不代表没有发生加载和链接
通过子类调用父类的静态字段
public class SuperClass {static {System.out.println("SuperClass 被初始化了");}public static int value=123;
}
public class SubClass extends SuperClass {static {System.out.println("SubClass 被初始化了");}}
public class Main {public static void main(String[] args) {System.out.println(cn.itcast.jvm.t3.book.SubClass.value);}
}
//输出结果
SuperClass 被初始化了
123Process finished with exit code 0
通过数组定义
类引用,不会触发此类初始化
package cn.itcast.jvm.t3.book;
public class SuperClass {static {System.out.println("SuperClass 被初始化了");}public static int value=123;
}
public class NotInitialization {public static void main(String[] args) {SuperClass []sca=new SuperClass[10];}
}
//输出结果Process finished with exit code 0
引用常量不会触发类或者接口的初始化
package cn.itcast.jvm.t3.book;public class ConstClass {static {System.out.println("ConstClass 被初始化");}public static final String HELLO_WORLD="heloo world";
}
public class Main {public static void main(String[] args) {System.out.println(ConstClass.HELLO_WORLD);}
}
//输出结果
heloo worldProcess finished with exit code 0
调用ClassLoader类的loadClass()方法
@Test
public void test3(){try {Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.atguigu.java1.Person");} catch (ClassNotFoundException e) {e.printStackTrace(); }}
class Person{static{System.out.println("Person类的初始化");}public static final int NUM = 1;//在链接过程的准备环节就被赋值为1了。public static final int NUM1 = new Random().nextInt(10);//此时的赋值操作需 要在()中执行
}
当一个类在初始化的时候,要求其父类全部已经初始化过,但是在一个接口在初始化的时候,并不要求其父接口都完成了初始化,只有真正使用父接口的时候,如引用接口中定义的常量才会被初始化
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
在初始化一个类时,并不会先初始化它所实现的接口
在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时, 才会导致该接口的初始化
/***示例代码*/
public class HelloLoader {public static void main(String[] args) {System.out.println("Hello World!");}
}
类文件的主要组成
_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,
_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
关于全限定名
首先类的名称=包名+类名
我们的类文件在硬盘上,硬盘上的数据被抽象成文件了(一颗文件系统树),我们的类加载如何去找到类文件的
我们自己写的类和官方停供的类处理方式一样吗,肯定处理方式不可能,所以对于不同的类文件需要不同的类加载器去处理
我们的JVM就是通过类加载器+类的全限名称来唯一确定一个类
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
类的完整类名必须一致,包括包名。
加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
补充:加载class文件的方式
从本地系统中直接加载
通过网络获取,典型场景:Web Applet
从zip压缩包中读取,成为日后jar、war格式的基础
运行时计算生成,使用最多的是:动态代理技术
由其他文件生成,典型场景:JSP应用
从专有数据库中提取.class文件,比较少见
从加密文件中获取,典型的防Class文件被反编译的保护措施
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
关于数组的类的加载
对类加载器的引用
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
验证
格式检查:比如开头是否以魔数开头 主次版本号是否被当前Java虚拟机支持 指向常量池的各种索引值中是否有指向不存在的常量或者不符合类型的常量等等,只有通过这些检查,这段字节流才能进入方法区进行存储
语义检查:1这个类是否有父类(在Java中除了Object,是都会有父类的) 2是否一些被定义成finald的方法或者类被重写或者继承了 3这个类是不是抽象类,是否实现了父类或者接口的所有抽象方法 4类中的字段,方法是否与父类产生矛盾等等 这个阶段的目的主要对元数据信息进行语义校验
字节码验证:这个阶段主要目的是通过分析数据流和控制流分析,确定程序语义是合法的,符合逻辑的,这个阶段也就是对类中的方法体进行校验分析,保证类的方法运行的时候不会危害虚拟机
符号引用验证:符号引用验证的主要目的是确保解析行为能正常执行,也就是检查我们符号引用指向的类或者方法是不是真的存在
准备
为类变量(静态变量)分配内存并且设置该类变量(static)的默认初始值,即零值。
这里不包含用final修饰的static的ConstantValue属性,因为final在编译的时候就会分配了内存和赋零值,准备阶段会显式初始化;
基本类型或者String类型(使用字面量的方式进行赋值)
,就生成ConstantValue属性来进行初始化。没有final修饰或者并非基本类型及String类型,则选择在方法中进行初始化。这里不会为实例变量分配初始化,类变量会分配在"方法区"中,而实例变量是会随着对象一起分配到Java堆中。
在这个阶段并不会像初始化阶段那样会有代码被执行
解析
将常量池内的符号引用转换为直接引用的过程。
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
例子
类的初始化阶段是类加载过程的最后一个步骤
在进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在我们的初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源(到了初始化阶段,才真正开始执行类中定义的Java程序代码——static代码块,和static属性显式赋值)
另一种表达方式,就是我们初始化阶段就是执行类构造器,这个构造器并不是程序员在Java代码中直接编写的方法,它是JavaC编译器的自动生成物
是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。但是对于定义在它之后的静态变量,在前面的静态语句块可以赋值,但是不能访问
构造器方法中指令按语句在源文件中出现的顺序执行。
方法对于类或者接口来说不是必须的,编译器也不是必须生成这个()方法
public class InitializationTest2 {public static int a = 1;//在初始化阶段()中赋值public static final int INT_CONSTANT = 10;//在链接阶段的准备环节赋值public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段()中赋值public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);//在初始化阶段()中赋值public static final String s0 = "helloworld0";//在链接阶段的准备环节赋值public static final String s1 = new String("helloworld1");//在初始化阶段()中赋值public static String s2 = "helloworld2";public static final int NUM1 = new Random().nextInt(10);//在初始化阶段()中赋值
}
()不同于类的构造器。(关联:构造器是虚拟机视角下的())
若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
虚拟机必须保证一个类的()方法在多线程下被同步加锁。
类的初始化发生的时机
发生的时机
概括得说,类初始化是【懒惰的】
main 方法所在的类,总会被首先初始化
首次访问这个类的静态变量或静态方法时
子类初始化,如果父类还没初始化,会引发
子类访问父类的静态变量,只会触发父类的初始化
Class.forName
new 会导致初始化
不会导致类初始化的情况
Java虚拟机设计团队在设计有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流这个动作”
放到Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需的类----实现这个动作的代码被称为类加载器
在类加载器的内部,用一个Java集合来存放所加载类的引用,另一个方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获取这个类的加载器,由此可见,代表某个类的Class实例与其类加载之间为双向关系
类的命名空间
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超于类加载的阶段,对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中唯一性,每一个类加载器,都拥有一个独立的类名称空间
这里的相等包括了类的Class对象的equals方法,isInstance()等方法的返回值
每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。
loader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接引用它。
如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。
当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)
故可以引出一个类什么时候能允许从方法区回收
为什么我的垃圾回收不注重方法区的回收
(1)启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)
(2)被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小。
(3)被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)。
综合以上三点,一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下,来实现系统中的特定功能。
双亲委派模型
。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如JDK内部的ServiceProvider/ServiceLoader机制,用户可以在标准API框架上,提供自己的实现,JDK也需要提供些默认的参考实现。例如,Java中JNDI、JDBC、文件系统、Cipher等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器。
可见性
,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。
单一性
,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。
这里的四者之间的关系是包含关系。不是上层下层,也不是子父类的继承关系。
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
JVM支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)这种是由C++语言实现的,是虚拟机的一部分和自定义类加载器(User-DefinedClassLoader)。这种类加载器都由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
但是站在我们Java开发程序员的角度来看,类加载器的划分应当划分的更细致一些,也是为了实现我们的双亲委派机制
启动类加载器(引导类加载器,Bootstrap ClassLoader)
这个类加载使用C/C++语言实现的,嵌套在JVM内部。
它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
并不继承自ava.lang.ClassLoader,没有父加载器。
加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
package cn.itcast.jvm.t3.load;
public class F {static {System.out.println("bootstrap F init");}
}
package cn.itcast.jvm.t3.load;
public class Load5_1 {public static void main(String[] args) throws ClassNotFoundException {Class> aClass = Class.forName("cn.itcast.jvm.t3.load.F");System.out.println(aClass.getClassLoader());}
}
// 加的命令 E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.cn.itcast.jvm.t3.load.Load5
//输出
bootstrap F init
null
Xbootclasspath 表示设置 bootclasspath
其中 /a:. 表示将当前目录追加至 bootclasspath 之后
可以用这个办法替换核心类
扩展类加载器(Extension ClassLoader)
Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
派生于ClassLoader类
父类加载器为启动类加载器
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用程序类加载器(系统类加载器,AppClassLoader)
java语言编写,由sun.misc.LaunchersAppClassLoader实现
派生于ClassLoader类
父类加载器为扩展类加载器
它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
通过ClassLoader.getSystemclassLoader() 方法可以获取到该类加载器
public class ClassLoaderTest {public static void main(String[] args) {//获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2//获取其上层:扩展类加载器ClassLoader extClassLoader = systemClassLoader.getParent();System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d//获取其上层:获取不到引导类加载器ClassLoader bootstrapClassLoader = extClassLoader.getParent();System.out.println(bootstrapClassLoader);//null//对于用户自定义类来说:默认使用系统类加载器进行加载ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。ClassLoader classLoader1 = String.class.getClassLoader();System.out.println(classLoader1);//null}
}
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?
隔离加载类
修改类加载的方式
扩展加载源
防止源码泄漏
用户自定义类加载器实现步骤:
ClassLoader类是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
获得类加载器的途径
public class ClassLoaderTest2 {public static void main(String[] args) {try {//1.获取当前ClassLoadeClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();System.out.println(classLoader);//2.获取当前线程上下文的ClassLoader ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();System.out.println(classLoader1);//3.获取系统的ClassLoader ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();System.out.println(classLoader2);//方式四:获取调用者的ClassLoader DriverManager.getCallerClassLoader()} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作流程
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
代码支持
双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)接口中体现。该接口的逻辑如下:
(1)先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
(2)判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name,false)接口进行加载。
(3)反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassorNull(name)接口,让引导类加载器进行加载。
(4)如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader接口的defineClass系列的native接口加载目标Java类。
双亲委派的模型就隐藏在这第2和第3步中。
优势
避免类的重复加载
保护程序安全,防止核心API被随意篡改
/*** @author shkstart* @create 2020 上午 11:40*/
public class String {//static{System.out.println("我是自定义的String类的静态代码块");}//错误: 在类 java.lang.String 中找不到 main 方法public static void main(String[] args) {System.out.println("hello,String");}
}
//输出结果 因为我们用启动类加载器加载了我们的核心类库中的String类,导致不能加载我们自己写的String类
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
public class ShkStart {public static void main(String[] args) {System.out.println("hello!");}
}
//输出结果 保护了我们的程序安全,防止修改核心API
java.lang.SecurityException: Prohibited package name: java.lang
弊端
检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。
结论
Java虚拟机规范也没有明确一定要使用双亲委派机制,只是建议使用,比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Serylet规范推荐的一种做法。
第一种——线程上下文类加载器
双亲委派机制虽然能很好的解决各个类加载器协作时的基础类型的一致性问题,基础之所以被称为基础是因为总是被用户代码调用,但是程序不是完美的,如果有基础类又要调回用户的代码,那如何实现(比如我们的SPI接口是启动类加载器加载,但是其具体的实现类应该是由我们的系统类加载器加载)
通过我们的反向委托也就是我们的线程上下文类加载器,通过Thread.setContextClassLoader()方法进行设置,如果当前线程未设置,它会从父线程继承一个,如果在应用程序的全局范围内都没有设置那么默认就是系统类加载器
有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,但也是无可奈何的事情。 ,例如JNDI、JDBC、JCE、JAXB和JBI等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK6时,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。
第二种——重写loadClass方法
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 首先检查这个classsh是否已经加载过了Class> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {// c==null表示没有加载,如果有父类的加载器则让父类加载器加载if (parent != null) {c = parent.loadClass(name, false);} else {//如果父类的加载器为空 则说明递归到bootStrapClassloader了//bootStrapClassloader比较特殊无法通过get获取c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c == null) {//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载classlong t1 = System.nanoTime();c = findClass(name);sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
第三种
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。如:**代码热替换(Hot Swap)、模块热部署(Hot Deployment)**等
IBM公司主导的JSR-291(即OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(osGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bund1e连同类加载器一起换掉以实现代码的热替换。在oSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。
当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将以java.*开头的类,委派给父类加载器加载
2)否则,将委托列表名单内的类,委派给父类加载器加载
3)否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bund1e的类加载器加载。
7)否则,类查找失败。
说明:只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的
小结:这里,我们使用了“被破坏”这个词来形容上述不符合双亲委派模型原则的行为,但这里“被破坏”并不一定是带有贬义的。只要有明确的目的和充分的理由,突破旧有原则无疑是一种创新。
正如:OSGi中的类加载器的设计不符合传统的双亲委派的类加载器架构,且业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议,但对这方面有了解的技术人员基本还是能达成一个共识,认为OSGi中对类加载器的运用是值得学习的,完全弄懂了OSGi的实现,就算是掌握了类加载器的精粹。
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
前最新的安全机制实现,则引入了**域(Domain)**的概念。
虚拟机会把所有代码加载到不同的系统域和应用域。系统域专门负责与关键资源交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示,最新的安全模型(jdk1.6)