类的加载
1、类的生命周期总共有7个,分别是:
-
加载:找到class文件,查找并加载类的二进制数据。
-
校验:验证格式、依赖,确保被加载类的正确性。
-
准备:静态字段、方法表,为类的静态变量分配内存,并初始化他们。
-
解析:符号解析为引用,将常量池的符号引用转为直接引用。
-
初始化:构造器、静态代码块、静态你变量赋初值。
-
使用
-
卸载
2、类加载要完成的功能 -
通过类的全限定名来获取该类的二进制字节流
-
把二进制字节流转换为方法区的运行时数据结构。
-
在堆上创建java.lang.Class对象,用来封装方法区的数据结构,向外提供了访问方法区的内数据结构的接口。
3、加载类的方式 -
最常见的方式:本地文件系统中加载、从jar等归档文件中加载。
-
动态方式:将java源文件动态编译成class
-
其他方式:通过网络下载。
4、类的加载器
加载器的种类: -
启动类加载器BootstrapClassLoader:用于启动加载基础模块的类比如:java.base java.management java.xml等等 。
-
扩展类加载器(ExtClassloader)
-
应用类加载器(AppClassLoader)
-
用户自定义加载,是java.lang.classLoader的子类,用户可以定制类的加载方式,只不过自定义的类加载器其加载顺序是所有系统的系统类的加载器的最后。
-
*类的加载器的说明: -
java程序不能直接引用启动类加载器。
-
类加载器并不需要等到某个类首次主动使用时候才加载他,jvm规范允许类加载器在预料到某个类将要使用的时候预先加载他。
-
如果在加载的时候.class文件缺少,会在该类主动使用的时候报告LinkageError错误,如果一直没有使用就不报错。
public class JvmClassLoaderPrintPath {
public static void main(String[] args) {
URL[] urls= Launcher.getBootstrapClassPath().getURLs();
System.out.println("启动类加载器");
Arrays.stream(urls).forEach(s->{
System.out.println("=>"+s.toExternalForm());
});
//扩展类加载器
printClassLoader("扩展类加载器",JvmClassLoaderPrintPath.class.getClassLoader().getParent());
//应用类加载器
printClassLoader("应用类加载器", JvmClassLoaderPrintPath.class.getClassLoader());
}
public static void printClassLoader(String name,ClassLoader classLoader){
if (classLoader!=null){
System.out.println(name+"=>classLoader:"+classLoader.toString());
printUrlForClassLoader(name, classLoader);
}else{
System.out.println(name+"=>classLoader:"+"null");
}
}
public static void printUrlForClassLoader(String name, ClassLoader classLoader){
Object ucp=insightField(classLoader, "ucp");
Object path=insightField(ucp, "path");
//System.out.println(ucp);
ArrayList list=(ArrayList)path;
list.stream().forEach(s->{
System.out.println("=>"+s.toString());
});
}
public static Object insightField(Object obj,String fname){
Field field=null;
try {
if (obj instanceof ClassLoader){
field= URLClassLoader.class.getDeclaredField(fname);
}else{
field=obj.getClass().getDeclaredField(fname);
}
field.setAccessible(true);
return field.get(obj);
}catch (NoSuchFieldException | IllegalAccessException e){
e.printStackTrace();
return null;
}
}
}
*加载器的特点:双亲委派、负责依赖、缓存加载
- Jvm中的classloader通常采用双亲委派模型,要求除了启动类加载器之外,其余的类加载器都应该有自己的父类,其余的类加载器都应该有自己的父级加载器,这里的父子关系是组合而不是继承。
- 一个类加载器收到类的加载请求后,首先搜索他的内建加载器定义的所有具有具名模块。
- 如果找到了合适的模块定义,将会使用该模块来加载。
- 如果class没有在加载器定义的具名模块中找到,那么他将委托给父级加载器,直到启动类加载器。
- 如果父级加载器反馈它不能完成加载请求,比如它的搜索路径下找不到这个类,那子的类加载器才自己来加载。
- 在类路径下找到的类将成为这些加载器的无名模块。
双亲委派模型说明 - 双亲委派模型对于保证java程序的运行稳定很重要。
- 实现双清委派的的代码在java.lang.classloader的loadclass方法中,如果自定义类加载器的话推荐实现findclass方法。
- 如果有一个类加载器能加载某个类,称为定义类加载器,所有能成功返回该类的Class类加载器都被称为初始类加载器。
- 如果没有指定父加载器,默认就是启动加载器。
- 每个类加载器都有命名空间,命名空间由该加载器及其所有的父加载器所加载的类构成,不同的命名空间可以出现类路径名相同的情况。
- 自定义classloader。
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* @author lxg
* @Description: 自定义classloader
* @date 2020/10/2121:58
*/
public class MyClassLoader extends ClassLoader {
private String myName;
public MyClassLoader(String myName) {
this.myName = myName;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data=loadClassData(name);
return this.defineClass(name, data,0,data.length);
}
private byte[] loadClassData(String clsName){
byte[] data=null;
InputStream in=null;
ByteArrayOutputStream out=new ByteArrayOutputStream();
clsName=clsName.replace(".","/");
try {
in=new FileInputStream(new File("H:\\wonders-code\\java-advanced\\classes\\"+clsName+".class"));
int a=0;
while ((a=in.read())!=-1){
out.write(a);
}
out.flush();
data=out.toByteArray();
out.close();
in.close();
}catch (Exception e){
e.printStackTrace();
}
return data;
}
}
类的初始化:就是为类的静态变量赋值,或者是执行类的构造器的方法的过程
-
如果类还没有加载和连接,就先加载和连接。
-
如果存在父类,且父类没有初始化,就先初始化父类。
-
如果类中存在初始化语句,就依次执行这些初始化语句。
-
如果是接口的话
-
A、初始化一个类的时候,并不会初始化它实现的接口。
-
B、初始化一个接口的时候,并不会初始化它的父接口。
-
C、只有当程序首次使用接口里面的变量或者是调用接口方法的时候,才会导致接口的初始化。
-
调用classloader类的loadclass方法来装载一个类,并不会初始化这个类,不是对类的主动使用。
类的加载时机 -
当虚拟机启动时,初始化用户指定的主类也就是启动执行main所在的类。
-
当遇到以新建目标实例的new指令,初始化new指令的目标类,就是new一个类的时候需要初始化。
-
当调用静态方法的指令时,初始化该静态方法所在的类。
-
当遇到静态字段时,初始化该静态字段所在的类。
-
子类的初始化会触发父类的初始化。
-
如果一个接口定义了default方法,那么一个类直接或者间接实现该接口都会触发该接口的初始化。
-
使用反射api对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有了实例,要么是静态方法,都需要初始化。
-
当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的 类。
不会初始化,但是可能会加载
1、通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2、定义对象数组,不会触发该类的初始化。
3、常量在常量在编译期会存入调用类的常量池,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
4、通过类名获取class对象,不会触发类的初始化,Hello.class不会让hello初始化。
5、通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触 发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName (“jvm.Hello”)默认会加载Hello类
6、通过ClassLoader默认的loadClass方法,也不会触发初始化动作(加载了,但是 不初始化)。
类的卸载
通过ClassLoader默认的loadClass方法,也不会触发初始化动作(加载了,但是 不初始化)。