JVM性能优化之类加载机制
文章目录
一、类加载子系统
类加载子系统简介
我们写的Java代码都是编译为.class文件后再交给虚拟机运行的,类的加载就是虚拟机运行的第一步,类加载器负责从文件系统或者网络中加载class文件,加载后的类的信息我们在前面也说了是放在方法区的(JDK8之后的元空间),而具体的执行则交给了执行引擎去操作的。
类加载器的执行过程
我们的class文件从加载到JVM到卸载的过程总共分为:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和卸载(Unloading)七个步骤。其中前五步操作是类加载器执行的,使用是在执行引擎,而卸载则是GC的垃圾回收。
加载: 虚拟机的加载分为预加载和运行时加载,其中预加载指的是初始默认加载一些JDK中常用到的类,例如IO操作,基础对象类型等;运行时加载指的是虚拟机在运行过程中加载class文件,首先先去方法区找类的信息,如果找不到则加载该类。在类的加载信息过程中虚拟机主要做了三件事:1.获取class的二进制流;2.将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中;3.在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证: 验证主要对.class文件的字节流中文件格式、元数据、字节码、符号引用信息的验证。为了防止程序代码中有有害代码导致虚拟机运行崩溃。
准备: 当.class验证通过后就可以进入了准备阶段,这时候虚拟机为类中的变量分配内存并设置一个默认值,这里的变量指的是static类型的变量,我们知道static修饰的信息是在类加载的时候初始化的,准备完成后的类信息还是在方法区中。
解析: 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,简单来说就是我们前面把信息都准备好了,解析阶段负责把准备好的信息解析并且串联起来,让其成为一个可以找到的完整的类的信息。
初始化: 初始化阶段就是执行类构造方法的过程,这里的类构造方法并不是我们写的,而是javac编译过程中自动手机类中的变量和静态代码块合并生成的,这时候虚拟机才真正执行类中定义的JAVA代码。与init()不同的是,cinit()是在类的初始化阶段调用的,只会执行一次,无法手动的去调用,在类的初始化的过程中如果发现父类没有初始化会先触发其父类的初始化,虚拟机会保证cinit()方法在多线程环境中被正确加锁和同步;而init()则是在对象的初始化阶段执行的。
类加载器的分类
类加载器分为引导类加载器、扩展类加载器和系统类加载器。
- 引导类加载器: 引导类加载器是c/c++实现的,用来加载Java的核心类库,用于提供JVM自身需要的类,不需要继承java.lang.ClassLoader,没有父加载器。
- 扩展类加载器: 扩展类加载器是我们自己用Java语言实现的,父类是引导类加载器,加载Java扩展(java.ext.dirs)下面的类。
- 系统类加载器: 系统类加载器是程序中默认的类加载器,负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库,父类是扩展类加载器,可以通过ClassLoader.getSystemClassLoader() 来获取它。
- 自定义类加载器: 我们还可以通过继承 java.lang.ClassLoader 类的方式实现自己的类加载器,例如Tomcat中就有很多自定义类加载器。
二、双亲委派原则
双亲委派原则指的是如果一个类加载器需要加载一个类,它不会自己去直接加载这个类,而是把这个类委派给父加载器去加载,当父加载器在所属范围内查找不到该类的时候,子加载器才会尝试自己去加载该类,如果加载不到就是我们常见的 ClassNotFoundException。
双亲委派原则的优点在于:
- 不会重复加载某些类,如果有的类已经被父加载器加载过了,那么我们子类去加载的时候就重复加载了。
- 保证了核心类不被篡改,例如我们写个String类加载器,那我们后面用String的时候就会出现意想不到的BUG,如果有人发坏在你代码里面加了这种操作,在你不知道的情况下肯定找不到代码哪里有问题。双亲委派之后String被引导类加载器加载了,后面自己实现了也不会去加载,因为父加载器已经加载了。
三、如何自定义类加载器
我们可以先点开ClassLoader的代码看一下,我们可以看到其内部有三个个方法,loadClass()、findClass()以及defineClass(),其中loadClass()是用来加载类的,这个代码不建议修改,因为其内部已经实现了双亲委派原则,如果我们自行实现该代码有可能会破坏双亲委派原则;而是使用findClass()去寻找类加载,我们可以看到ClassLoader中没有写findClass的实现代码,这个代码就是官方提供给用户去实现自己的类加载器的;defineClass()方法的功能是将字节码转换加载为虚拟机中的类。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
类加载器的执行流程是这样的:
下面写一个demo代码:
public class MyClassLoader extends ClassLoader {
//这是我们寻找class的路径
private String path;
public MyClassLoader(ClassLoader parent, String path) {
super(parent);
this.path = path;
}
public MyClassLoader(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
BufferedInputStream bis = null;
ByteArrayOutputStream bos = null;
try {
//.class文件的路径
String fileName = path + name + ".class";
// 下面是纯IO流操作
bis = new BufferedInputStream(new FileInputStream(fileName));
bos = new ByteArrayOutputStream();
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
bos.write(data, 0, len);
}
//获取字节数组
byte[] byteCode = bos.toByteArray();
//调用defineClass 将字节数组转成Class对象
Class<?> defineClass = defineClass(null, byteCode, 0, byteCode.length);
return defineClass;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
我们就可以直接使用该类加载器调用我们自己想要加载的类了:
public static void main(String[] args) {
//加载的路径
MyClassLoader classLoader = new MyClassLoader("C:/myClass");
try {
//加载的class名,我们自己写的类加载在内部补充了.class
Class<?> clazz = classLoader.loadClass("HelloWorld");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}