三个大阶段
一:装载(三个小阶段)
1.装载:就是读取class文件的字节流,放到jvm中
二:连接(分动态和静态,静态很少使用,下面按顺序三步骤)
1.验证:校验字节的正确性
2.常量池解析:把符号引用替换为指针引用,就是将方法名和类名替换成Class对象的内存地址(类A里有类B,使用类A的时候,先判断A的常量池中是B的class对象,还是B的字符串表示,如果是字符串,则将其替换成Class对象的引用)
3.准备:给静态变量分配内存并且根据相应类型赋默认值(注意此处是默认值)
三:初始化:对静态变量赋指定值,同时执行静态代码块
下面将用代码阐述上述逐个过程
public class MyClass {public static final int a = 1;
public int b = 2;
public void m1() {
m2();
}
public void m2() {
}
}
一.1:将MyClass.class文件通过字节数组读到jvm中
一.2:校验这些字节是不是和jvm定义的一样,假设int在jvm中的机器码是8B,那么此时要校验int a是不是8B a
一.3:使int a=0;
二:在jvm中m1方法肯定不能直接调用m2方法,因为jvm不知道m2是个什么东西,m1调用的应该是m2方法开始的内存地址
三:使int a=1;
关于双亲委派模型
双亲委派存在的意义
1.java的沙箱安全机制,我们自己不能创建java.lang开头的java类
2.避免重复加载类
什么时候需要打破双亲委派
当需要同一个类,并且不同版本的时候
典型的例子就是tomcat的各种类加载器,tomcat实现了许多类加载器,主要目的就是为了实现使用相同名称但是版本又不相同的类
如何自定义类加载器
1.通常实现findclass,本质该方法也会调用loadClass
2.但是如果想打破双亲模型,则覆盖loadClass方法
Java的命名空间(对程序员是透明的,所以我们自己无法定义命名空间名称)
现在有自定义类加载器CL,有一个类A,类A中有一个属性类B,现new两个CL,一个叫CL1,一个叫CL2,它俩都加载类A,返回Class A对象叫Class1和Class2,那么此时Class1!=Class2,而Class1中的B.class也!=Class2中的B.class,由上述结论可知:不同的自定义类加载器,加载的类是相互隔离的,切这些加载的类中引用的类,也是相互隔离的,下面是本结论的代码验证
public B b = new B();
}public class B {
public void m() {
System.out.println(this.getClass().getClassLoader());
}
}
将A和B编译成class文件,放到桌面上,下面自定义类加载器
import java.nio.file.Files;import java.nio.file.Path;
import java.nio.file.Paths;
public class CL extends ClassLoader {
@Override
protected Class<?> findClass(String name) {
byte[] data = null;
// 本自定义类加载器只加载桌面上的class文件
// 对name进行处理,如果name是com.aa.A,则nm=A
// 如果name是com.abc.MyClass,则nm=MyClass
String nm = 对name字符串处理;
Path path = Paths.get("C:\\Users\\admin\\Desktop\\" + nm + ".class");
try {
data = Files.readAllBytes(path);
} catch (Exception e) {
e.printStackTrace();
}
return defineClass(name, data, 0, data.length);
}
}
最后一个main方法
public static void main(String[] args) throws Exception {CL c1 = new CL();
CL c2 = new CL();
Class<?> clazz1 = c1.loadClass("A的全限定名");
Class<?> clazz2 = c2.loadClass("A的全限定名");
System.out.println(clazz1 == clazz2);//false
Field field1 = clazz1.getField("b");
Field field2 = clazz2.getField("b");
Method m1 = field1.getType().getMethod("m");
Object o1 = field1.get(clazz1.getConstructor().newInstance());
Method m2 = field2.getType().getMethod("m");
Object o2 = field2.get(clazz2.getConstructor().newInstance());
m1.invoke(o1);// 值与下面不一样
m2.invoke(o2);// 值与上面不一样
}
下面这行代码会打印出null
System.out.println(String.class.getClassLoader());因为String属于rt包下,由启动类加载器加载(bootstrap classloader),而启动类加载器由c++实现,所以这行代码打印null,因为java中没有启动类加载器
方法区会存放以下类型信息
0.Class对象的引用
1.类型的全限定名
2.直接父类的全限定名(这样就可以找到父类,根据父类可以找到爷爷类)
3.类型标识(用来标识当前类型是类还是接口)
4.访问修饰符(public,abstract,final)
5.直接接口的全限定名列表(就是实现了哪些接口)
6.该类的常量池(字段信息+方法信息),可参考常量池的表现方式 字段修饰符
字段类型
字段名
方法修饰符
方法返回值
方法名
方法参数的类型集合(按顺序)
方法的字节码
操作数栈和局部变量去大小
常量(其他类如果使用该常量,则其他类会将该常量复制到自己的常量池)
7.ClassLoader的引用(表示哪个类加载器加载的该类)
class对象被回收的条件
class对象被回收的条件必须要同时满足下面3个
1.该class对象不存在实例对象
2.加载该class对象的classloader被回收
3.没有其他地方使用该class对象(通过反射)