类的生命周期
类的声明周期从类的字节码文件被加载到JVM虚拟机中,到使用结束被垃圾回收器回收,整个声明周期分为5个阶段:加载、连接、初始化、使用、卸载。
其中连接阶段有可以细化分为验证、准备和解析三个小阶段。
本文中先只讨论类的加载阶段。
代码调试示例
直接通过一个简单的例子来debug一下,看一下源码是如何进行的。
示例代码中创建一个Animal类,并在主方法中创建实例。
public class ClassLoaderTest {
public static void main(String[] args) {
Animal animal = new Animal();
}
}
class Animal {
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void run() {
System.out.println("Animal is running...");
}
public void eat() {
System.out.println("Animal is eating...");
}
}
loadClass方法
通过断点,我们可以看到在类加载的过程,首先会进入到java.lang.ClassLoader抽象类中的loadClass方法。
下一步,他会进入到sun.misc.Launcher类中的静态内部类AppClassLoader中的loadClass方法。
AppClassLoader继承自URLClassLoader,URLClassLoader继承自ClassLoader。
从调试器上可以看到,当前的类加载器对象为Launcher$AppClassLoader@621。这个对象是当前线程的类加载器,可以通过Thread.currentThread().getContextCalssLoader()查看当前线程所使用的的类加载器。
- AppClassLoader的loadClass方法源码(这里没什么用)
先会对类名经过一个安全管理器的检测,这个不进行配置的话var4一般为null。之后会通过一个URLClassLoader对这个路径的检测,因为URLClassLoader在初始化是会将一个参数设置为false,所以这个方法会返回false。
综上,如果你你的这些类加载器都是使用的默认的话,在AppClassLoader中的loadClass方法会直接进行到调用父类super.loadClass()方法。
因为AppClassLoader的直接父类URLClassLoader实现的loadClass方法为final,因此AppClassLoader中重写的loadClass方法为ClassLoader的,下一步会进入到ClassLoader.loadClass方法。
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
return super.loadClass(var1, var2);
}
}
- ClassLoader的loadClass方法(这里是重点)
通过下面的源码,我们可以发现在进行类加载的过程中有以下步骤:
- 先判断类是否已经加载过了,如果加载过,最后就返回了。
- 判断类加载器的parent是否为空,如果parent不为空,就由父亲的loadClass加载,如果parent为空,就插件引导类加载器加载的类中是否存在该类。
- 如果双亲的loadClass加载失败(返回为null或抛出ClassNotFound异常),则调用自己的findClass方法进行加载。
- resolveClass()方法是用来进行连接阶段的。
这里parent和继承的父类并不是一个,因此将这里称为双亲以做区分。在Java虚拟机中引导类加载器和扩展类加载器的parent为null。
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;
}
}
在调试过程中AppClassLoader的parent是ExtClassLoader不为空,因此会先调用双亲的loadclass,但是结果是在进行ExtClassLoader.findClass时会抛出classNotFound异常,最后还是由AppCalssLoader的findClass进行加载。
因此,下面的源码调试会我们就直接跳过parent的过程,直接到AppClassLoader的findClass方法。下图是ExtClassLoader.findClass抛出异常的调试。
ExtClassLoader同样继承自URLClassLoader,其与AppClassLoader中很多方法都是相同的。
其实上面的过程就是Java类加载的双亲委派机制,具体机制为何,将在下面理论中进行讨论。
findClass方法
因为AppClassLoader中并没有重写findClass方法,因此会调用其父类URLClassLoader中的对应的方法。
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
上面源码中前面后面都不重要,主要的逻辑就在中间的三行:
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
return defineClass(name, res);
第一行,通过全类名得到类的字节码文件的路径;
第二行,通过ucp获取到类对应的Resource,下面的图可以看到Resource主要包含类的路径名、url和file对象。
第三行,通过defineClass()进行字节码文件的加载。
defineClass方法
defineClass方法也为URLClassLoader中重写的方法,其逻辑比较简单,通过下面的源码,可以很清楚的看到,他就是先做了一个判断,然后通过res获取到字节码文件的内容(byte[]字节类型的),也就是类的字节流。最后调用另一个defineClass()方法对类的字节流进行载入。
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
int i = name.lastIndexOf('.');
URL url = res.getCodeSourceURL();
if (i != -1) {
String pkgname = name.substring(0, i);
// Check if package already loaded.
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url);
}
// Now read the class bytes and define the class
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// Use (direct) ByteBuffer:
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, bb, cs);
} else {
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, b, 0, b.length, cs);
}
}
这里又调用的defineClass方法为SecureClassLoader类中的方法,主要就是进行一些校验以及调用native方法。就就没必要继续往下分析了,这里贴了个图。
类的加载阶段原理分析
类加载器
Java中类加载器公分为三种:
- 引导类加载器(bootstrapClassLoader)
这个类加载器主要由Java虚拟机进行控制,用来加载Java运行过程中所必须的类,存放在jre/rt.jar中的所有的类。或者也可以通过 -Xbootclasspath参数,指定引导类加载器需要加载的文件(可指定多个文件夹)。为了能正常运行最好在使用参数指定时,将rt.jar也带上。
需要注意的时,引导类加载器是由C++进行编写的,在Java中是无法获得的。就比如String类是由引导类加载器加载,但是通过String.class.getClassLoader()获得到的结果为null。
- 扩展类加载器(ExtClassLoader)
用于加载Java中扩展功能的jar包,jre提供的功能但并不必要,其在项目启动时不会自动加载所有jar包。主要加载目录为jre/lib/ext路径下的jar包,以及通过参数 -Djava.ext.dirs指定目录下的jar包。
扩展类加载器的双亲(parent)为引导类加载器,因为引导类加载器在Java中不可获得,因此扩展类加载器的parent为null。
- 应用类加载器(AppClassLoader)
除了上面说的两个文件夹中的的其他类都是由应用类加载器进行加载,也就是我们自己写的类,存放在classpath下的类,都是由应用类加载器进行加载的。当然,自己重写了自定义加载器除外。
应用类加载器的双亲为扩展类加载器。当然在项目启动后,可能存在有多个应用类加载器,这些应用类加载之间可能存在一定的双亲parent关系,但最终向上找双亲,总能找到扩展类加载器。
- 自定义类加载器
对于自己有一些特殊的需求,可以自己编写自定义类加载器,需要继承java.lang.ClassLoader。然后通过自定义的类加载器加载指定的类,例如CustomClassLoader.loadClass(“xxx”);xxx为全类名。
需要注意的是,自定义类加载器中需要自己指定一个双亲,以便委托双亲加载一些类。
Java的双亲委派机制
Java双亲委派机制用一句话总结就是:
自底向上委派,自顶向下加载!
在Java的运行过程中,每一个线程都会存在一个上下文类加载器,可以通过Thread.currentThread().getContextClassLoader()获取,一般为一个应用类加载器,也可以指定为自定义类加载器。
- 在需要加载一个类时,会先调用上下文类加载器的loadClass方法进行加载。
- 通过上面的源码可以知道,如果双亲存在,会委托给双亲的类加载器进行加载。如果没有双亲了,则查看类是否被引导类加载器加载。向上委派
因为只有扩展类加载器没有双亲,他的双亲引导类加载器不可获得为null,所以在没有双亲时就查看引导类加载器的。
- 如果双亲加载器加载失败,则由当前的加载器尝试进行加载。向下加载
就比如扩展类加载器只加载jre/lib/ext文件夹下的jar包,我们自定义的类其就无法加载。
下图来自网络,不是自己画的。
- 双亲委派机制的优点
- 避免类的重复加载
当自己程序中定义了一个和Java.lang包同名的类,此时,由于使用的是双亲委派机制,会由启动类加载器去加载JAVA_HOME/lib中的类,而不是加载用户自定义的类。此时,程序可以正常编译,但是自己定义的类无法被加载运行。 - 保护程序安全,防止核心API被随意篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
- 避免类的重复加载
- 双亲委派机制的缺点
- 灵活性降低:由于类加载的过程需要不断地委托给父类加载器,这种机制可能导致实际应用中类加载的灵活性降低。
- 增加了类加载时间:在类加载的过程中,需要不断地查询并委托父类加载器,这意味着类加载所需要的时间可能会增加。在类数量庞大或类加载器层次比较深的情况下,这种时间延迟可能会变得更加明显。
打破双亲委派机制
打破双亲委派机制的方法,其实就是通过手写自定义类加载器的方式,对默认的ClassLoader中的loadClass方法重写,修改其判断双亲存在则由双亲加载的部分逻辑。
下面是一个简单的自定义类加载器的示例:
public class ClassLoaderTest {
public static void main(String[] args) throws Exception{
String path = ClassLoaderTest.class.getClassLoader().getResource("").getPath();
String className = "com.jhx.testdemo.utils.loadDemoClass";
MyClassLoader myClassLoader = new MyClassLoader(path.substring(1),ClassLoaderTest.class.getClassLoader());
Class<?> myClass = myClassLoader.loadClass(className);
Object obj = myClass.newInstance();
Method out = myClass.getDeclaredMethod("out");
out.invoke(obj);
}
]
class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath, ClassLoader parent){
super(parent);
this.classPath = classPath;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null){
try {
loadedClass = findClassInPath(name);
}catch (ClassNotFoundException e) {
loadedClass = super.loadClass(name, resolve);
}
}
if (resolve){
resolveClass(loadedClass);
}
return loadedClass;
}
private Class<?> findClassInPath(String className) throws ClassNotFoundException{
try {
String filePath = className.replace('.','/') + ".class";
byte[] classBytes = Files.readAllBytes(Paths.get(classPath+filePath));
return defineClass(className,classBytes,0,classBytes.length);
} catch (Exception e){
throw new ClassNotFoundException("Class not found in classes path" + classPath);
}
}
}
//下面这个类需要另一个Java文件中,因为在一个文件中只能存在有一个public修饰的类,
//但是不使用public修饰的类,通过反射的newInstance方法获取实例对象时,会出错。
public class loadDemoClass {
String name;
public loadDemoClass(){}
public loadDemoClass(String name){
this.name = name;
}
public void out(){
System.out.println("out...");
}
}
补充说明
- 加载阶段所完成的事情主要有以下
- 第一,将类的字节码文件读取到JVM的方法去中,形成要给Klass对象。
这里是为了用于区分,Class所以官方文档中使用Klass来形容方法去中的字节码文件
- 第二,在堆区中形成一个Class类的实例对象,以供后续使用。
在JVM虚拟机中所有的实例对象都是存储在堆区中的。这里的Class类是java.lang.Class抽象类(由引导类加载器进行加载),并不是我们所需要加载的类,所有加载进虚拟机的类都会产生一个Class类实例对象(俗称类对象)存储在JVM的堆区中,这个对象中包含了类的一些属性和方法信息,在JVM需要调用时,可以直接使用而不用