JVM对于java程序员来讲既是高级也是基础,刚入行的同窗没必要知道jvm的内存划分、不须要知道类的加载过程、GC的回收过程也能够舒舒服服的写代码,可是这种知其然不知其因此然的态度确定会限制咱们的上升空间,今天这篇文章开始走进jvm,咱们从第一步开始,先搞清楚类是怎么被加载的,这就是今天要分享的内容!java
进入正题以前要先说一下JVM,JVM的组成结构主要是由 类装载子系统、运行时数据区、执行引擎、本地方法接口这4部分组成,而今天的文章主要围绕类状态子系统展开描述!c++
%{JAVA_HOME}\jdk1.8.0_261\jre\lib
目录下的类;sun.misc.Launcher.ExtClassLoader
,主要负责加载%{JAVA_HOME}\jdk1.8.0_261\jre\lib\ext
目录下的类;sun.misc.Launcher.AppClassLoader
,负责加载咱们配置的环境变量classpath
目录下的类;ClassLoader
类能够实现本身定制的类加载方式,这种方式能够用来打破双亲委派模型(双亲委派模型下面会讲到);咱们能够经过一段代码很清晰的看到每一个类加载器的样子:程序员
public class TestClassLoader {
public static void main(String[] args) {
System.out.println(Object.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
System.out.println(TestClassLoader.class.getClassLoader());
}
}
复制代码
输出结果:bootstrap
null
sun.misc.Launcher$ExtClassLoader@77459877
sun.misc.Launcher$AppClassLoader@18b4aac2
复制代码
Object类对应的加载器为何是null嘞? 上面已经说过了,jvm内部会建立一个c++编写的启动类加载器负责去加载%{JAVA_HOME}\jdk1.8.0_261\jre\lib
目录下的类,这个加载器在java里面是获取不到的,因此是null; 下面两个,一个是ExtClassLoader,一个是AppClassLoader,是sun.misc.Launcher
类的静态内部类;而这个Launcher类是由C++调用sun.misc.Launcher#getLauncher
方法得到的;api
能够经过如下代码看一下类加载器之间有什么联系安全
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassloader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassloader.getParent();
System.out.println("the bootstrapLoader : " + bootstrapLoader);
System.out.println("the extClassloader : " + extClassloader);
System.out.println("the appClassLoader : " + appClassLoader);
复制代码
输出结果:markdown
the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@77459877
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
复制代码
能够看到,默认的类加载器是AppClassLoader,往上走是ExtClassLoader,最上层是BootStrapClassLoader; 先来看一下类加载器的类图结构:app
类加载器都是继承的ClassLoader,为每一个子类都维护了一个parent属性:jvm
下面咱们就重点看parent属性作了什么事情,在实例化Launcher的时候 ,构造器里面对app和ext这两个对象作了初始化:ide
那继续点进去看看AppClassLoader是怎么建立的,中间套娃的代码我就跳过了,他是直接调用super(parent)这个构造器,直接看他就行了:
在这个地方维护了类加载器之间的父子关系,因此Ext也是App的父加载器,那么这么作他到底要干什么呢?这里涉及到了一个概念:双亲委派(下面会细说);上面的代码还反映了一个问题:默认的类加载器是AppClassLoader?在JVM内部会默认调用Launcher类的getClassLoader()方法来获取一个默认类加载器进行加载,而这个classLoader恰好就是在实例化Launcher类的时候生成的AppClassLoader:
一个类在被类加载器加载的时候,该类的加载器不会当即去加载,而是经过parent属性找到其父加载器进行加载,一直递归往上找,一直到顶层的BootStrap都没有被加载,就会返回到本类的加载器进行加载:
ClassLoader里面除了维护parent属性外,还维护了一个公共的loadClass方法,这个方法就是双亲委派的实现。咱们来详细分析下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 这里会一直往上调,app的parent是ext,ext的parent是bootstrap
c = parent.loadClass(name, false);
} else {
// 这个方法最终会调用到一个native方法,加载不到类的时候会返回null
// return null if not found
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 父加载器没有加载到类,返回null
if (c == null) {
long t1 = System.nanoTime();
// 调用当前的类加载器去加载
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;
}
}
复制代码
看完上面的代码,咱们的思路就很清晰了,其实就是递归向上调用若是没有加载到就再向下返回;
第一点你们应该改都明白,第二点是什么意思嘞?咱们跑一段代码演示一下:
// 覆盖原有的java.lang包
package java.lang;
// 覆盖原有的Object类
public class Object {
public static void main(String[] args) {
System.out.println("object ....");
}
}
复制代码
上面的代码执行完以后,会是什么结果呢?
错误: 在类 java.lang.Object 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args) 不然 JavaFX 应用程序类必须扩展javafx.application.Application 复制代码
因此,由于有双亲委派的存在,咱们这种恶意破坏原有api的行为就行不通了;
咱们能够经过自定义类加载器,来指定咱们本身要去加载的类;读完以上源码咱们不难发现,双亲委派的逻辑在loadClass方法里,而加载类的逻辑是在findClass方法里,我想要本身实现一个类加载器就应该去继承ClassLoader,重写findClass方法,在findClass方法里面去加载咱们本身指定的类:
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadByte(name);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
// 在这个路径下面放一个User.class文件,由咱们本身的类加载器去加载
Class clazz = classLoader.loadClass("com.maolin.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
复制代码
运行,输出结果:
com.maolin.MyClassLoader
复制代码
User.class的字节码是被MyClassLoader加载器加载的; 经过这种方式还能够打破双亲委派的机制,在重写findClass的基础上,再重写loadClass:
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
/* 咱们把ClassLoader类的loadClass方法里的代码复制出来, 把双亲委派的那段代码去掉,让当前的类加载器直接加载,不向上委托 */
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
复制代码
打破双亲委派机制篇幅太长,后续会单独写一篇文章来描述,想知道结果的能够参考这两篇文章:
类加载器说完了,咱们再来瞅瞅一个类被加载的时候会经历哪些过程:
感谢各位读者朋友耐心看完个人文章,文章中如有错误之处,还请留言指正,或者文章中有哪一个细节描述的不够清晰,也能够在评论区留言,我看到后必定会回复并改正;
不求作的最好,但求作的更好。