JVM
类加载步骤
JVM
-
loading
– 装载 -
linking
– 链接-
verification
– 验证 -
preparation
– 准备 -
resolution
– 解析
-
-
intializing
– 初始化
-
装载
1. 通过一个类的 全限定名 来获取定义此类的 二进制字节流 2. 将这个字节流所代表的 静态存储结构 转化为 方法区的 运行时数据结构 3. 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区访问这些数据的入口。
-
验证: 检查载入Class文件数据的正确性;
-
准备: 给类的静态变量分配存储空间 -> 此时会给类的全局变量赋默认值;
int : 0; long : 0L; String : null;
-
解析:将符号引用转成直接引用;
符号引用: 例如String的符号引用 –
Ljava/lang/String
, class 文件的常量池中,
fields
的
descriptor_index
代表了这个属性的类型,以一个符号引用的形式存储在 常量池中。直接引用:指向实际值的地址;
-
初始化: 对变量赋初始值
类的初始化过程是类加载的最后一个过程。虚拟机没有规定一个类何时开始加载,但是有规定几种情形必须对类进行初始化操作:
- 遇到 new、getstatic、putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
ClassLoader
类被加载后
-
将类的二进制内容放进内存(
metaSpace
)中 -
生成了一个
java.lang.Class
类的对象
- MethodArea (方法区) 在 jdk 1.7 及之前是 perm(永久代),在 jdk 1.8 及之后是 metaSpace(元数据区)
双亲委派机制
类加载器有一个层次关系:
注意这里的层次关系并不是继承关系,而加载器的父加载器是通过
ClassLoader
中的一个成员变量
parent
来指定的;
父加载器 – 不是类加载器的加载器,也不是加载器的父类
-
这里的最底层加载器是
BootStrap
加载器,这个加载器是由
C++
实现的,负责加载
lib/rt.jar
,
charset.jar
等的内容;
sys_paths = initializePath("sun.boot.library.path");
-
ExtClassLoader
用于加载
jre/lib/ext/*jar
等扩展包中的类,通过以下配置指定。并且可以通过
-Djava.ext.dirs
配置
String var0 = System.getProperty("java.ext.dirs");
-
AppClassLoader
加载
classPath
的类
final String var1 = System.getProperty("java.class.path");
-
CustomClassLoader
是可以自定义指定的类加载器,也就是说,我们自己写的类加载器,其父加载器都是
AppClassLoader
获取某个类是由哪个加载器加载的
System.out.println(Object.class.getClassLoader());
System.out.println(User.class.getClassLoader().getParent());
/* ---------------------------------------
output:
null
sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@7852e922
*/
1. 由于 Object 是由 BootStrapClassLoader 加载器加载的,但由于这个加载器是由 C++ 实现的,在java层级并没有与之对应的类。所以Object 的 ClassLoader 为null;
2. 程序中的某个类,其加载器是 Launcher 类的子类 -> AppClassLoader
3. AppClassLoader 的父加载器是 ExtClassLoader
类的加载过程:
// java.lang.ClassLoader abstract
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 == null
即为判断其父加载器是不是
BootStrapClassLoader
,调用这个加载器需要进行 native 方法调用。这是个类似递归的调用过程。
父加载器也要在自己的缓存中检测这个类有没有被自己加载过,如果父加载器没有加载成功,就会调用自己的加载逻辑进行加载。
为什么要使用双亲委派机制进行类的加载?
-
主要原因:为了安全
防止类进行重复加载,如果发生了重复加载,则之前加载的类便会被覆盖掉,不仅不能保证类的单例性,而且如果自己写了一个名叫
java.lang.String
的类替换掉了 Java 原来的 String ? 将会造成不可描述的问题。 -
次要原因:为了资源节约
类是单例的,无需进行重复加载。
自己写一个类加载器
什么时候需要加载类?
某些 热部署/热替换 的场景,需要手动编译新的 Class ,并将其替换时。比如 Spring 中大量使用的代理。
-
需要继承
java.lang.ClassLoader
CLassLoader 类中使用
模板模式
定义了 loadClass 方法作为加载类的入口,写死了双亲委派机制的同时,提供了一个具体加载类的模板 ->
findClass
方法。 -
重写
findClass
方法 -
准备好类的 二进制字节流后,使用
defineClass
方法生成
java.lang.Class
对象并返回
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println(name);
File f = new File("/Users/.../java/main/",
name.replaceAll(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b;
while ((b = fis.read()) != 0) {
baos.write(b);
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();
// 转换为 class 对象
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
public static void main(String[] args) throws Exception {
ClassLoader l = new MyClassLoader();
Class clz = l.loadClass("com.improve.jvm.classloader.Hello");
Hello h = (Hello) clz.newInstance();
h.m();
System.out.println(l.getClass().getClassLoader());
System.out.println(l.getParent());
System.out.println(h.getClass().getClassLoader());
}
}
-
自定义的
ClassLoader
由于在项目中,所以它是由
AppClassLoader
加载进来的。
其他
-
Java 使用一种
lazy loading
(懒加载) 的模式,即需要用才加载。在引用一个 jar 包时,并不会将整个 jar 包中都的 Class 都加载进来。
-
通过自定义的 ClassLoader 可以实现对 Class文件的加密,首先对一个 Class 文件进行一定规则的加密,生成另一个 文件 , 在类加载时,对这个类进行解密。
// 一个简单的基于异或运算的加解密样例
public class EncriptionClassLoader extends ClassLoader {
public static int seed = 0B10110110;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File f = new File("c:/test/", name.replace('.', '/').concat(".pbclass"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b=fis.read()) !=0) {
baos.write(b ^ seed);
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();//可以写的更加严谨
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name); //throws ClassNotFoundException
}
public static void main(String[] args) throws Exception {
encFile("com.improve.jvm.hello");
ClassLoader l = new EncriptionClassLoader();
Class clazz = l.loadClass("com.improve.jvm.Hello");
Hello h = (Hello)clazz.newInstance();
h.m();
System.out.println(l.getClass().getClassLoader());
System.out.println(l.getParent());
}
private static void encFile(String name) throws Exception {
File f = new File("c:/test/", name.replace('.', '/').concat(".class"));
FileInputStream fis = new FileInputStream(f);
FileOutputStream fos = new FileOutputStream(new File("c:/test/", name.replaceAll(".", "/").concat(".pbclass")));
int b = 0;
while((b = fis.read()) != -1) {
fos.write(b ^ seed);
}
fis.close();
fos.close();
}
}
- jVM 没有规定一个类什么时候被加载,但是它规定了一个类什么时候必须进行初始化(初始化肯定要先加载了啊,憨憨)
1. 在对类进行 new, getstatic, putstatic, invokestatic 时 (访问 final 的变量除外,因为 final 的变量无需进行初始化已经可以拿来用了。 换句话说,访问 final 的变量,并不会导致这个类被初始化);
2. reflect 反射访问时;
3. 子类初始化,父类必须先初始化;
4. 虚拟机启动时,含有 main 方法的那个主类要初始化;
5. 动态语言支持中,method_handle 方法句柄解析的结果为:
REF_getstatic;
REF_putstatic;
REF_invokestatic;
以及 在 Class.forname() 时;