Class Load
load
我们编译后的.class文件是如何被JVM加载进入内存的呢,中间经过了哪些个步骤呢?如下图:
主要是经过这么几个步骤:
- loading加载:从磁盘读取xxx.class文件进入内存
-
linking
– verifiy验证阶段:验证class文件是否已“cafababe”开头
– prepare准备阶段:对类的成员变量开辟空间,将class中的所有成员变量(静态和非静态)赋值为默认值,不执行任何初始化操作
– resolute解析阶段:为类、接口、方法、成员变量的符号引用定位直接引用,也即与外部其他class建立引用关系 - init 初始化:将静态成员变量的默认值替换成初始值
如现有A.class文件,内部定义了一个静态成员变量B=1,其成员变量B加载过程如下:在prepare阶段,先给B赋默认值0,在init阶段,再给B赋初始值1。
那么,当JVM将class文件读取进内存之后,内存中有哪些东西呢?
- 将Class元数据信息保存在matespace上(1.8之后,1.8之前是保存在老年代)
类型 | 保存位置 |
---|---|
静态成员变量 | 方法区的静态部分 |
静态方法 | 方法区的静态部分 |
非静态方法 | 方法区的非静态部分 |
静态代码块 | 方法区的静态部分 |
构造代码块 | 方法区的静态部分 |
- 创建当前class文件的Class对象,无论我们后面在代码中new出了多少个实例,JVM中对应的Class对象,始终只有一个。
ClassLoader
Java中有4种ClassLoader,自上而下依次为Bootstrap CLassloder、ExtClassLoader、AppClassLoader、CustomClassLoader(自定义类加载器)
那这4中加载器,它们之间的关系是什么,各自的作用域又是什么呢?要搞清楚这个问题,我们得分析下源码:
Launcher.class部分源码如下:
private static String bootClassPath = System.getProperty("sun.boot.class.path");
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
...
}
}
static class ExtClassLoader extends URLClassLoader {
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
...
}
}
通过源码,我们可以看到有三处定义了uri,这实际便是几个加载器的作用域,即
- Bootstrap CLassloder, 负责加载sun.boot.class.path路径下的类,如lib/rt.jar,charset.jar等核心类库,此部分功能由c++实现,我们代码中这部分调用getClassLoader()方法,其返回的是个null,是个跟加载器
- ExtClassLoader ,负责加载java.ext.dirs路径下的类,负责加载扩展包
- AppClassLoader,负责加载java.class.path路径下的类
- CustomClassLoader,可自定义加载某个路径下的类以及加载方式。
双亲委派
JVM没有规定一个Class文件具体啥时候需要被加载,具体时间由各自的虚拟机决定。常用的Hotspot采用懒加载方式,即当需要用到这个Class的时候才将该类加载进内存,加载流程是什么样的呢?这里面就会涉及类加载的双亲委派机制了,我们以最全流程为例讲解:
现有一个Class需要加载进内存,使用CustomClassLoader。
- CustomClassLoader查看自身是否有加载过这个Class,如果有则不再加载,直接返回,如果没有加载,转2
- CustomClassLoader询问父加载器AppClassLoader是否有加载过这个class,如果父加载器有加载过,直接返回,如果父加载器也没有加载过,转3
- AppClassLoader向父加载器ExtClassLoader询问是否有加载过,同理,如果没有加载过,或者加载不成功,转4
- ExtClassLoader询问父加载器Bootstrap,结果还是一样没有加载过,且自身不负责加载该类,Bootstrap通知子加载器ExtClassLoader,要求ExtClassLoader自己尝试加载
- ExtClassLoader尝试加载该类,发现该类作用域不在自己的管辖内,不能加载,于是再通知自己的子加载器AppClassLoader,要求AppClassLoader自己尝试加载
- AppClassLoader加载失败,通知CustomClassLoader自己加载
- CustomClassLoader负责加载该class进内存
通过上面的步骤可以得知,所谓双亲委派,就是在需要加载一个新的Class的时候,需要向父加载器一层层的问询是否已经加载过,如果都没有加载过,则由父加载器一层层通知子加载器,由子加载器尝试加载class。
Java为什么要这么设计呢?主要是为了安全,设想这样一种场景,我们自定义了一个加载器,用来加载String类,如果直接走我们自定义的加载器,我们在加载器内部收集所有string数据上传,如果这些数据正好是用户的用户名和密码,那我们就可以轻松拿到每个人的用户名和密码了。
还有一点,为了避免重复加载,如果一个类已经被父加载器加载过了,内存中已经存在了一个Class对象,这时候直接用就可以了,不需要再次加载。
Java是如何实现双亲委派的呢?源码如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
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
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
其实很简单,就是通过递归的方式,先在 parent.loadClass中问询是否有加载过该类,如过parent没有加载过,且尝试加载失败,则当前的子加载器自己尝试加载该class。