常识
1.JVM是一种规范,有很多具体的实现,比如常用的hotspot,阿里的TaoBaoVM。
2.范围从小到大 JVM-> JRE->JDK
3.JVM运行只看字节码文件,不看是谁生产的字节码。
类的加载流程
很简单,直接看图,正常的java文件通过javac命令变编译成.class文件,会被ClassLoader直接加载到内存,这个时候会在内存中创建两块内容,第一块是把class文件原封不同的加载进内存,第二块内容是生成跟class文件对应的对象,在访问的时候,通过对象访问加载到内存的class文件。JVM会根据代码执行的热度不同选择用解析器解析代码或者是编译被执行引擎执行。
初识class文件字节码
查看字节码要安装两款插件
1.BinEd ->用于把class文件翻译成 十六进制 文件
2.jclasslib -> 用于分析二进制文件,生成视图
package mytest;
/**
* @ClassName T
* @Description 测试二进制码
* @Author zxj
* @Date 2020/5/11 22:43
* @Version 1.0
**/
public class T {
}
一个什么都没有的类,生成的class文件如下,用BinEd打开
CA FE BA BE :表示class文件开头的固定写法
00 00 00 34 :前面四个0表示JDK的小版本号,后面0034表示JDK1.8的版本
00 10 : 表示常量池中有16-1的长度
再往后的一个字节0A是一个符号坐标,需要去查JVM规范手册
使用jclasslib插件打开class文件相当于对之前的16进制文件做了一个解析,直接看图
双亲委派机制
首先说明的是父子Classloader并不是继承关系,而是聚合关系。
比如我们自定义一个类T,加载的时候,如上图,先从自定义ClassLoader自身维护的缓存中检查T是否被加载,如果没有被加载,会让它的父ClassLoader(AppClassLoader)检查自身维护的缓存中T类是否被加载,如果AppClassLoader也没有加载T类,AppClassLoader会让ExtClassLoader检查T类是否加载,如果ExtClassLoader也没有加载,就会传递到最顶级的父加载器(BootStratClassLoader)检查是否加载,如果BootStratClassLoader还没有加载,就会尝试加载T类,但是由于BootStratClassLoader只负责加载核心类库,所以会向下传递给ExtClassLoader尝试加载,由于ExtClassLoader只负责加载扩展类库,会继续传递给AppClassLoader尝试加载,AppClassLoader负责加载ClassPath路径下的类,如果T类存在,就会被AppClassLoader加载,如果不存在,会传递给我们自定义的ClassLoader加载我们自定义Path的T类。
简单的讲,就是检查T类是否被加载是从自定义ClassLoader —> BootStratClassLoader,中途如果发现类已经被加载会直接return,保证了class只会被加载一次。尝试加载过程顺序是 BootStratClassLoader—>自定义ClassLoader,保证了类的加载顺序。 总结就是保证了安全性。 当然也节省了资源的开销。
如何证明ClassLoader的加载范围?
阅读Launcher源码会发现
Bootstart
ExtClassLoader
AppClassLoader
源码解析双亲委派机制
手动加载一个类需要使用ClassLoader的loadClass()方法
public class T {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(T.class.getClassLoader());
//加载person类
Class<?> aClass = T.class.getClassLoader().loadClass("mytest.Person");
System.out.println(aClass.getName());
}
}
输出结果
sun.misc.Launcher$AppClassLoader@18b4aac2
mytest.Person
然后查看loadClass()源码
java.lang.ClassLoader#loadClass(java.lang.String)
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//检查需要被加载的类是否已经被加载,如果已经被加载直接return
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//调用父加载器的loadClass(name, false),父加载器的load也会先检查是否被加载
//如果没有被加载,会继续往上传递,直到传递到bootstrat,类似于递归
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
/**
* 如果父类都没有找到,c还是null的情况下
*/
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//自己去加载,这里有点类似Servlet的filter的方式
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
所以我们如果要自定义ClassLoader,只需要继承ClassLoader类重写findClass(name)方法就行了。
public class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file=new File("D:\\MyObject\\Person.class");
try {
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true)
{
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
byte[] bytes = baos.toByteArray();
//------------------------------------------------------
//调用defineClass方法
Class<?> aClass = this.defineClass(name, bytes, 0, bytes.length);
return aClass;
} catch (Exception e) {
e.printStackTrace();
}
//返回异常
return super.findClass(name);
}
public static void main(String[] args) {
MyClassLoader classLoader=new MyClassLoader();
try {
Class<?> person = classLoader.loadClass("Person");
Object o = person.newInstance();
System.out.println(o);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
输出:
Person{name='null', age=null}
类加载器::mytest.MyClassLoader@1b6d3586
父加载器::sun.misc.Launcher$AppClassLoader@18b4aac2
这里踩了很多的坑总结一下:
1.要把idea生成的class文件手动删掉,不然双亲委派机制会直接让AppClassLoader加载ClassPath下的class文件。
2.findClass方法参数name要 等于 class文件里面的包名+类名。
问题又来了,parentClassLoader是哪里指定的?
看源码
java.lang.ClassLoader#ClassLoader()
ClassLoader的默认构造方法
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
可以看到getSystemClassLoader()可以获取到父加载器,所以在我们实例化自定义加载器的时候,就已经指定好了父加载器了。
如何破坏双亲委派机制?
通过之前的源码分析,其实只要重写loadClass()方法把向父加载器查询过程干掉就可以了。
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
File f = new File("D:\\MyObject\\Person.class");
if(!f.exists()) return super.loadClass(name);
try {
InputStream is = new FileInputStream(f);
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.loadClass(name);
}
戏入人生
如果我有理解不正确的地方请留言指正,谢谢