1.1 类加载器子系统的作用
类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。
ClassLoader只负责class文件的加载,至于它是能否可以运行,则由ExecutionEngine决定。
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
1.2 类加载器中ClassLoader的角色
1.class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例,也可以由实例调用getclass获取class
\2. class file 经过classLoader加载到JVM中,被称为DNA元数据模板,放在方法区。
3.在.class文件->JVM->最终成为元数据模板,此过程就要一个运输工具(类装载器class Loader),扮演一个快递员的角色。
1.3 类的加载过程
一个代码示例:
public class HelloLoader {
public static void main(String[ ] args) {
System.out.println("谢谢classLoader加载我....");
System.out.println("你的大恩大德,我下辈子再报!");
}
}
看看其程序框图
可以看到其顺序首先是查看ClassLoader是否装载了我们需要的HelloLoader类,若没有则去装载,失败了会抛出异常,没有失败则会连接,初始化,然后才会来调用main方法执行输出语句。
细分一下类的加载过程。
一、Loading
注意:生成class实例是在Loading这个环节实现的。
加载:
1.通过一个类的全限定名获取定义此类的二进制字节流 (因为我们需要将该类加载到磁盘)
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口(反射)
二、Linking
同样有一小段程序
public class HelloApp {
private static int a = 1;public static long b;l
static{
b = 2;
}
public static void main(String[ ] args) {
system.out.println(a);
system.out.println(b);
}
}
该例子中的b在准备阶段就是0,但是在初始化后就是2了
关于解析部分中的将常量池内的符号引用转换为直接引用的过程。可以去看虚拟机栈,下图
在该阶段会创建一个虚方法表,在类的变量初始化准备完成后,JVM会把虚方法表也准备完毕
三、Initialization
在初始化阶段clinit方法不是我们自己创建的,而是javac编译器为我们收集静态代码块或者类变量赋值操作而自动形成的方法。
public class ClassInitTest {
private static int num = 1;
static{
num = 2;
public static void main(String[] args) {
System.out.println(classInitTest.num);
}
}
上图是clinit方法中的字节码的代码,可以看到先把常量赋值为1,再赋值为2
上面的代码较为简单,我们看一段特殊的代码
public class ClassInitTest {
private static int num = 1;
static{
num = 2;
number = 20;
}
private static int number = 10;
public static void main(string[] args){
System.out.println(ClassInitTest.num);//2
System.out.println(classInitTest.number);
}
}
这一段会经历什么过程呢,我们都知道在linking过程中会将number的值初始化为0,然后才会在initialization的阶段覆盖值,由20----->10
看看字节码文件
值得注意的是上述代码的number可以再静态代码中赋值而不能调用
但是如果没有静态代码块或类变量,是没有clinit方法的
最后一点 静态代码快只会被加载一次
看下面的程序
public class DeadThreadTest {
public static void main(String[ ] args) {
Runnable r = ()->{
system.out.println( Thread.currentThread().getName() +"开始");
DeadThread dead = new DeadThread):l
system.out.println(Thread.currentThread( ).getName() +"结束");};
Thread t1 = new Thread(r, name:"线程1");Thread t2 = new Thread(r, name:"线程2");
t1.start();
t2.start();
}
}
class DeadThread{
static{
if(true){
System.out.println(Thread.currentThread( ) . getName()+“初始化当前类")
while(true){}
}
}
}
一个类只加载一次就可以了,所以再使用这个类是使用缓存中的类,即我们程序只会调用clinit方法一次,所以在多线程中,我么需要保证一个类的clinit方法是被同步加锁的,上面这个while(true)就相当于上了锁,只不过不释放锁。
1.4 类加载器的分类
JVM支持两种类型的类加载器,分别为引导类加载器(BootstrapClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:
用一个代码来感受一下
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.Launder$ExtCLassLoader@1540e19d
//获取其上层:
classLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//获取其上层:
classLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
classLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader) ; / / sun.misc.Launcher$AppCLassLoader@18b4aac
//String类是引导类加载器加载的,java的核心类库都是由引导类加载器加载的
classLoader classLoader1 = string.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
String类是引导类加载器加载的,java的核心类库都是由引导类加载器加载的
对于用户自定义类来说:默认使用系统类加载器进行加载
1.4.1 虚拟机自带的加载器
一、启动类加载器(引导类加载器,Bootstrap classLoader)
①这个类加载使用C/C++语言实现的,嵌套在JVM内部。
②它用来加载Java的核心库(JAVA_HOME/jre/ lib/rt.jar、resources.jar或sun. boot.class.path路径下的内容),用于提供JVM自身需要的类
③并不继承自java.lang.classLoader,没有父加载器。
④加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
⑤出于安全考虑,Bootstrap启动类加载器只加载包名为java、 javax、sun等开头的类
二、扩展类加载器(Extension classLoader)
①Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
②派生于classLoader类
③父类加载器为启动类加载器
④从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/ lib**/ext**子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。(他是来加载扩展的包,看ext就知道了)
三、应用程序类加载器(系统类加载器,AppClassLoader)
①java语言编写,由sun.misc.Launcher$AppclassLoader实现
②派生于classLoader类
③父类加载器为扩展类加载器
④它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
⑤该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
⑥通过classLoader#getsystemclassLoader ()方法可以获取到该类加载器
1.4.2 用户自定义的类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
①隔离加载类
②修改类加载的方式
③扩展加载源
④防止源码泄漏
用户自定义类加载器实现步骤:
1.开发人员可以通过继承抽象类java.lang.classLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
2.在JDK1.2之前,在自定义类加载器时,总会去继承classLoader类并重写loadclass ()方法,从而实现自定义的类加载类,但是在JDK1.2之后己不再建议用户去覆盖loadclass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中
3.在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass ()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
public class CustomClassLoader extends ClassLoader {
@Override
protected class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[ ] result =getclassFromCustomPath(name);if(result == nui1){
throw new FileNotFoundException();}else{
return defineClass(name , result, off: 0,result.length);
}catch (FileNotFoundException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
private byte[] getcClassFromCustomPath(String name){
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
}
}
1.5 ClassLoader的常用方法和获取方法
获取方法
1.6 双亲委派机制 **
引入
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理它是一种任务委派模式。
上面可以用以下的一个代码作出解释,此时在项目路径下存在一个包叫做java.lang,包下有类叫做String
package com.atgu1gu-javal;
public class StringTest {
public static void main(String[ ] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello,atguigu.com" );
}
java.lang.String下自己创建的String类
package java.lang;
public class String {
static{
System.out.println("我是自定义的string类的静态代码块");
}
}
源码解析:
经过上面main函数启动后,并没有出现输出"我是自定义的string类的静态代码块"语句,可以知道用的仍然还是我们的java核心类库的String类,这就是由于**双亲委派机制,**在main方法发现需要加载一个叫做string类的时候,会向上委派,直到最上层的启动类加载器,看看他能不能加载这个类,如果能就直接加载,不能才会委派给下层的加载器,所以我们用的就是顶层的启动类加载器所加载的核心类库下的String类了。
更进一步我们在代码中加上main函数,会报错,找不到main方法,因为此时String类是被启动类加载器所加载的
package java.lang;
public class String {
static{
System.out.println("我是自定义的string类的静态代码块");
}
public static void main(strng[] args) {
System.out.println("hello,String");
}
}
1.6.1 双亲委派机制的优势
优势
①避免类的重复加载
②保护程序安全,防止核心4PI被随意篡改
√自定义类: java . lang.string
√自定义类: java. lang.shkstart
1.6.2 沙箱安全机制
自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java \lang\string.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
1.6.3 其他
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
①类的完整类名必须一致,包括包名。
②加载这个类的classLoader(指classLoader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的classLoader实例对象不同,那么这两个类对象也是不相等的。
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
1.6.3 类的主动使用和被动使用
Java程序对类的使用方式分为:主动使用和被动使用。
主动使用,又分为七种情况:
①创建类的实例
②访问某个类或接口的静态变量,或者对该静态变量赋值
③调用类的静态方法
④反射(比如:Class.forName ( “com.atguigu. Test”) )
⑤初始化一个类的子类
⑥Java虚拟机启动时被标明为启动类的类
⑦JDK 7开始提供的动态语言支持:
java . lang. invoke. MethodHandle实例的解析结果
REF getstatic、REF putstatic、REF_invokestatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,
都不会导致类的初始化。