JVM之类加载子系统
一、类加载的时机
类加载主要有四个时机:
- 遇到 new 、 getstatic 、 putstatic 和 invokestatic 这四条指令时,如果对应的类没有初始
化,则要对对应的类先进行初始化。
public class Student{
private static int age ;
public static void method(){
}
}
//Student.age
//Student.method();
//new Student();
- 使用 java.lang.reflect 包方法时,对类进行反射调用的时候。
Class c = Class.forname("com.hero.Student");
- 初始化一个类的时候发现其父类还没初始化,要先初始化其父类
- 当虚拟机开始启动时,用户需要指定一个主类(main),虚拟机会先执行这个主类的初始化。
二、类加载的过程
类加载主要做三件事:
1、类的全限定名称 ===> 二进制字节流加载class文件
2、字节流的静态数据结构 ===> 方法区的运行时数据结构
3、创建字节码Class对象
一个类的一生:
-
加载:通过全类名获取定义此类的二进制字节流,将字节流所代表的静态存储结构转换为方法区的运行时数据结构,在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口。 -
验证:确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
-
准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。
-
解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
-
初始化:初始化阶段是执行初始化方法 ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
-
使用:
-
卸载:**卸载类即该类的 Class 对象被 GC,**要求:该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象,该类在没有在其他任何地方被引用,该类的类加载器的实例已被GC。
三、类加载器
JVM的类加载是通过ClassLoader及其子类来完成的。
检查顺序是自底向上:加载过程中会先检查类是否被已加载,从Custom ClassLoader到BootStrap
ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。
加载的顺序是自顶向下:也就是由上层来逐层尝试加载此类。
- 启动类加载器(Bootstrap ClassLoader):
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟
机认可(按文件名识别,如rt.jar)的类。由C++实现,不是ClassLoader的子类 - 扩展类加载器(Extension ClassLoader):
负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。 - 应用程序类加载器(Application ClassLoader):
负责加载用户路径classpath上的类库 - 自定义类加载器(User ClassLoader):
作用:JVM自带的三个加载器只能加载指定路径下的类字节码,如果某个情况下,我们需要加
载应用程序之外的类文件呢?就需要用到自定义类加载器,就像是在汽车行驶的时候,为汽车
更换轮子。
比如本地D盘下的,或者去加载网络上的某个类文件,这种情况就可以使用自定义加载器了。
举个栗子:JRebel
自定义类加载器案例:
目标:自定义类加载器,加载指定路径在D盘下的lib文件夹下的类。
步骤:
- 新建一个需要被加载的类Test.jave
- 编译Test.jave到指定lib目录
- 自定义类加载器DreamClassLoader继承ClassLoader:
重写findClass()方法
调用defineClass()方法 - 测试自定义类加载器
实现:
(1)新建一个 Test.java 类,代码如下:
package com.dreamoy.jvm.classLoader;
public class Test {
public void say() {
System.out.println("Hello DreamoyClassLoader");
}
}
(2)使用 javac Test.java 命令,将生成的 Test.class 文件放到D:/lib/com/dreamoy/jvm/classloader 文件夹下。
(3)自定义类加载器,代码如下:
package com.dreamoy.jvm.classLoader;
import java.io.*;
public class DreamClassLoader extends ClassLoader {
private String classpath;
public DreamClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//输入流,通过类的全限定名称加载文件到字节数组
byte[] classData = getData(name);
if (classData != null) {
//defineClass方法将字节数组数据 转为 字节码对象
return defineClass(name, classData, 0, classData.length);
}
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
//加载类的字节码数据
private byte[] getData(String className) throws IOException {
String path = classpath + File.separatorChar +
className.replace('.', File.separatorChar) + ".class";
try (InputStream in = new FileInputStream(path);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[2048];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
(4)测试,代码如下:
package com.dreamoy.jvm.classLoader;
import java.lang.reflect.Method;
public class TestMyClassLoader {
public static void main(String []args) throws Exception{
//自定义类加载器的加载路径
DreamClassLoader dClassLoader=new DreamClassLoader("D:\\lib");
//包名+类名
Class c = dClassLoader.findClass("com.dreamoy.jvm.classloader.Test");
if(c!=null){
Object obj=c.newInstance();
Method method=c.getMethod("say", null);
method.invoke(obj, null);
System.out.println(c.getClassLoader().toString());
}
}
}
输出结果如下:
四、双亲委派模型与打破双亲委派
什么是双亲委派?
当一个类加载器收到类加载任务,会先交给其父类加载器去完成。因此,最终加载任务都会传递到顶层
的启动类加载器,只有当父类加载器无法完成加载任务时,子类才会尝试执行加载任务。
相关源码:
/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/
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;
}
}
为什么需要双亲委派呢?
- 避免原始类被覆盖的问题
- 比如:
- 用户编写了一个java.lang.Object类放入程序中加载,系统中位于rt.jar包中的也有同样的类
java.lang.Object。这不就重复了嘛! - 有了双亲委派,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加
载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
- 用户编写了一个java.lang.Object类放入程序中加载,系统中位于rt.jar包中的也有同样的类
为什么还需要破坏双亲委派?
-
在某些情况下,父类加载器需要加载的class文件受到加载范围的限制,无法加载到需要的文件,这
个时候就需要委托子类加载器进行加载。这种情况就打破了双亲委派模式。 -
与双亲委派冲突:
- 热加载技术
- MySQL驱动
- Tomcat加载多个应用程序