1 类加载器命名空间
每一个类加载器实例都有各自的命名空间,命名空间是由该加载器及其所有父加载器所构成的,因此在每个类加载器中同一个class都是独一无二的
public class NameSpaceTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader appClassLoader = NameSpaceTest.class.getClassLoader();
// 加载两次
Class<?> aClass = appClassLoader.loadClass("study.wyy.thread.jvm.Demo1");
Class<?> bClass = appClassLoader.loadClass("study.wyy.thread.jvm.Demo1");
System.out.println(aClass.hashCode());
System.out.println(bClass.hashCode());
System.out.println(aClass == bClass);
}
}
1975012498
1975012498
true
load多少次Test,你都将会发现他们始终是同一份class对象(在同一个运行时包下,一个Class只会被初始化一次)
但是,使用不同的类加载器,或者同一个类加载器的不同实例,去加载同一个class,则会在堆内存和方法区产生多个class的对象。
这里就是用之前自己写的两个类加载器加载当时测试的类:
地址:
MyClassLoader
BrokerDelegateClassLoader
public static void main(String[] args) throws ClassNotFoundException {
String classDir = System.getProperty("user.home") + "/MyClassLoader";
MyClassLoader myClassLoader = new MyClassLoader(classDir,null);
// MyClassLoader classLoader = new MyClassLoader(classDir,null); 同一个类加载器的不同实例
BrokerDelegateClassLoader classLoader = new BrokerDelegateClassLoader(classDir,null); // 不同的类加载器
// 加载两次
Class<?> aClass = myClassLoader.loadClass("study.wyy.thread.jvm.HelloWorld");
Class<?> bClass = classLoader.loadClass("study.wyy.thread.jvm.HelloWorld");
System.out.println(aClass.hashCode());
System.out.println(bClass.hashCode());
System.out.println(aClass == bClass);
}
460141958
1163157884
false
程序的输出结果显示,aClass和bClass不是同一个class实例。
在类加载器进行类加载的时候,首先会到加载记录表也就是缓存中,查看该类是否已经被加载过了,如果已经被加载过了,就不会重复加载,否则将会认为其是首次加载,下图是同一个class被不同类加载器加载之后的内存情况:
同一个class实例只能在JVM中存在一份这样的说法是不够严谨的,更准确的说法应该是同一个class实例在同一个类加载器命名空间之下是唯一的。
2 运行时包
编写代码的时候通常会给一个类指定一个包名,包的作用是为了组织类,防止不同包下同样名称的class引起冲突,还能起到封装的作用,包名和类名构成了类的全限定名称。在JVM运行时class会有一个运行时包,运行时的包是由类加载器的命名空间和类的全限定名称共同组成的。
这样做的好处同样是出于安全和封装的考虑,在java.lang.String中存在仅包可见的方法void getChars(char[]var1,int var2),java.lang包以外的class是无法直接对其访问的。假设用户想自己定义一个类java.lang.HackString,并且由自定义的类加载器进行加载,尝试访问getChars方法,由于java.lang.HackString和java.lang.String是由不同的类加载器进行加载的,它们拥有各自不同的运行时包,因此HackString是无法访问java.lang.String的包可见方法以及成员变量的。
3 初始类加载器
由于运行时包的存在,JVM规定了不同的运行时包下的类彼此之间是不可以进行访问的,那么问题来了,为什么我们在开发的程序中可以访问java.lang包下的类呢?根据前面所学的知识,我们知道java.lang包是由根加载器进行加载的,而我们开发的程序或者第三方类库一般是由系统类加载器进行加载的,为什么我们在程序中能够new Object()或者new String()等任意的java.lang包下的类呢
package study.wyy.thread.jvm;
import java.util.ArrayList;
import java.util.List;
public class SimpleClass {
// /在SimpleClass中使用byte[]
private static byte[] buffer = new byte[8];
// SimpleClass中使用String
private static String str = "";
// SimpleClass中使用List
private static List<String> list = new ArrayList<>();
static {
buffer[0] = (byte) 1;
str = "Simple";
list.add("element");
System.out.println(buffer[0]);
System.out.println(str);
System.out.println(list.get(0));
}
public static void main(String[] args) {
System.out.println(buffer.getClass());
}
}
将这个类javac之后,放到之前BrokerDelegateClassLoader指定的默认的加载目录下(注意包名)
BrokerDelegateClassLoader之前自定义的一个classLoader
使用这个类加载上面的SimpleClass
public static void main(String[] args) throws ClassNotFoundException,
IllegalAccessException, InstantiationException {
BrokerDelegateClassLoader classLoader = new BrokerDelegateClassLoader();
Class<?> aClass = classLoader.loadClass("study.wyy.thread.jvm.SimpleClass");
System.out.println("SimpleClass的加载器:" + aClass.getClassLoader());
aClass.newInstance();
}
在SimpleClass中,我们访问了java.lang.String、java.utils.List以及java.lang.Object等类,这些类都存在于rt.jar包下,在JVM启动的时候这些类是由根加载器进行加载的
在上面的程序中,SimpleClass是由我们自定义的ClassLoader加载的,但是其能够访问不同的运行时包下的类,比如String,这是为啥呢
原因很简单,由于存在双亲委派机制,在加载String等类的时候,会先从自定义加载器BrokerDelegateClassLoader加载,而自定义是无法加载的,就会交给其父加载器App类加载器,而APP类加载器也是无加载String,有逐步交给扩展类加载器,根加载器,最终由根加载器加载。
JVM规范的规定,在类的加载过程中,所有参与的类加载器,即使没有亲自加载过该类,也都会被标识为该类的初始类加载器,所以加载String的时候经过了自定义加载器,系统记载器,扩展类加载器,根加载器,这些加载器都会是String的初始类加载器,在每一个类加载器维护的列表中添加该class类型
到这也就知道双亲委派加载机制存在的原因了
4 类的卸载
我们知道某个对象在堆内存中如果没有其他地方引用则会在垃圾回收器线程进行GC的时候被回收掉,那么该对象在堆内存中的Class对象以及Class在方法区中的数据结构何时被回收呢?
JVM规定了一个Class只有在满足下面三个条件的时候才会被GC回收,也就是类被卸载:
- 该类所有的实例都已经被GC
- 加载该类的ClassLoader实例被回收。
- 该类的class实例没有在其他地方被引用。
5 测试: 自定义一个java.lang.String
测试: 自定义一个java.lang.String, 交给自定义加载器加载:
package java.lang;
/**
* @author wyaoyao
* @date 2021/4/4 17:50
*/
public class String {
static {
System.out.println("自定义java.lang.String");
}
}
为了能够加载这个类,使用之前实现的一个自定义加载器去加载BrokerDelegateClassLoader BrokerDelegateClassLoader
但是需要去掉对java和javax前缀的判断
测试:编译这个String之后,丢到BrokerDelegateClassLoader 指定的class加载目录
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
BrokerDelegateClassLoader classLoader = new BrokerDelegateClassLoader();
Class<?> aClass = classLoader.loadClass("java.lang.String");
Object o = aClass.newInstance();
}
会报错:Exception in thread “main” java.lang.SecurityException: Prohibited package name: java.lang
Prohibited 禁止的意思
打开ClassLoader源码会发现JVM在defineClass的时候做了安全性检查
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}