JVM类加载机制原理及用法解析
Java虚拟机是Java语言实现"Write Once, Run Anywhere"程序设计理念的一个关键组成部分,而Java虚拟机中最重要的一个子系统就是类加载子系统。该子系统负责对字节码文件(.class文件)中的类进行加载、验证、准备、解析、初始化等操作,从而在程序的运行中实现类的动态加载和管理。那么,下面我们就来详细讲解JVM类加载机制原理及用法解析的完整攻略。
类装载的五个阶段
Java虚拟机类加载机制分为以下五个阶段:
1. 加载
类的加载是指将类的.class文件中的二进制数据读入到内存中,将其放入运行时数据区的方法区内(堆区存放对象实例)。同时在内存中生成一个表示该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2. 验证
这个步骤主要目的是确保Class文件的字节流中包含的信息是符合JVM规范,并且不会危害JVM的各种安全特性。
3. 准备
准备阶段是为类的静态变量分配内存,并为其赋上默认值。这里需要注意的是,这里赋值的是默认值,而不是程序中代码指定的值。
4. 解析
解析阶段是将常量池中的符号引用替换为直接引用的过程。也就是说,在这个阶段会把类、接口、方法和字段的符号引用解析为直接引用。
5. 初始化
初始化阶段是执行类构造器
可以看到,初始化阶段是整个类加载过程中最后一个阶段,这个阶段非常重要,只有在这个阶段,class才真正开始执行。
类的加载时机
类的加载时机有以下三种:
- 当虚拟机启动时,初始化用户指定的主类,即main方法所在的类。
- 当遇到new指令时,初始化new指令后的对象所属的类。
- 当遇到调用静态方法或读取/设置一个静态字段时,初始化该静态方法所在的类。
在这三种情况中,第一种是最常见的,也是最直观的。第二种和第三种只有在当程序中使用到该类时会进行加载。
举个例子:下面有两个类,一个是主类,一个是Car类,代码如下:
public class Main {
public static void main(String[] args) {
new Car();
}
}
public class Car {
public Car() {
System.out.println("Class Car init!");
}
}
在代码执行时,当我们执行Main类中的main方法时,就会通过new关键字创建一个Car对象。这时就会触发对Car类的加载。由于我们只是用到了该类的构造方法,因此并不会触发该类的初始化。
类的加载机制
类的加载机制分为以下两种:
1. 多个类加载器的情况
-
启动类加载器(Bootstrap):也叫根加载器。这个类是由C++实现的,是虚拟机自带的类加载器,它负责加载虚拟机自身的类(位于
<JAVA_HOME>/lib
下的jar包,如rt.jar、charsets.jar等)。 -
扩展类加载器(Extension):也叫扩展加载器。它用来加载Java的扩展类库,默认加载
<JAVA_HOME>/jre/lib/ext/
目下的jar包。 -
应用程序类加载器(Application):也叫系统加载器。它负责加载用户自定义的类。Java应用通常是由它来进行类加载的。可以通过Thread.currentThread().getContextClassLoader()来获取当前线程的类加载器。
除以上三种以外,还有一种叫做“自定义加载器”。如果需要实现特定的需求时,可以通过继承ClassLoader类,让Java程序拥有自己定制化的类加载器。
2. 双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器(子加载器)收到了一个类加载请求,它首先不会自己去加载这个请求,而是把请求委派给父类加载器(如果有的话)去执行加载。
- 如果父类加载器可以完成类加载任务,就成功返回,如果不能完成,则子加载器尝试自己去加载该类。
- 如果还不成功,则回到跟加载器(BootStrap ClassLoader)来加载。
这种方式可以保证Java核心类库的类型安全,即Java核心类库都是由Bootstrap加载器加载的,而不是由用户自定义的类加载器来加载的。这种方式可以保证Java的核心类库不被篡改,从而提高了Java应用的稳定性和安全性。
代码示例
下面我们通过一个简单的示例来进一步说明类加载机制及双亲委派模型的使用。
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println("ClassLoaderTest类的加载器是:" + ClassLoaderTest.class.getClassLoader());
System.out.println("String类的加载器是:" + String.class.getClassLoader());
}
}
运行上述代码,可以看到结果如下:
ClassLoaderTest类的加载器是:sun.misc.Launcher$AppClassLoader@18b4aac2
String类的加载器是:null
从上面的输出结果可以看出:
- ClassLoaderTest的类加载器是AppClassLoader(即应用程序类加载器)。
- String类的类加载器是null,这是因为String类是由Bootstrap加载器(启动类加载器)加载的。
下面,我们再通过代码示例来进一步说明恰当使用自定义类加载器的方法:
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
public Class<?> findClass(String name) {
byte[] data = getClassData(name);
return defineClass(name, data, 0, data.length);
}
private byte[] getClassData(String name) {
String path = classPath + "/" + name.replaceAll("\\.", "/") + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader classLoader = new MyClassLoader("/Users/zhangshanming/Desktop");
Class clazz = classLoader.loadClass("User");
System.out.println(clazz.getClassLoader());
}
}
public class User {
static {
System.out.println("User被加载");
}
}
运行上述代码,可以得到输出结果:
User被加载
MyClassLoader@18b4aac2
从上面的输出结果可以看出:
- MyClassLoader这个类是继承自ClassLoader,用于实现自定义的类加载器。
- MyClassLoader的findClass方法被重写,用于实现从指定路径中加载指定类名的类的字节码文件,并返回其Class对象。
- 接下来根据加载class的名字去加载这个Class,会发现在加载User类的时候,先是使用了MyClassLoader这个类来进行加载,最终生成并返回了User的Class对象。而这个Class对象是属于MyClassLoader实例的,它的Classloader属性得到的值正是实例MyClassLoader本身,也就说明了该类是由MyClassLoader来执行加载请求的。
通过这个示例,我们可以看出,通过自己的类加载器来加载类,可以实现自己的定制化需求。当然,切换加载器实例,也使加载成本加大,因此,我们一般使用Java提供的框架和第三方工具类库来尽量避免此类问题的发生。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:JVM类加载机制原理及用法解析 - Python技术站