类加载子系统
阶段
- 加载阶段(Loader)
- 链接阶段(Linking)
- 验证(Verify)
- 准备(Prepare)
- 解析(Resolve)
- 初始化(Initialization)
结构图
类加载子系统作用
- 类加载子系统负责从文件系统或者从网络中加载class文件,class文件在文件开头有特定的文件标识。
- ClassLoader 只负责class文件的加载,至于是否可以运行,则由Execution Engine(执行引擎)决定。
- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量
类加载子系统扮演的角色
在.class文件 -> JVM ->最终成为元数据模板,在这个过程,就需要一个运输工具,而类加载子系统(Class Loader SubSystem) 就扮演了这样的一个角色,说白了就是个工具人:)
类加载过程
加载(Loader)
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
链接(Linking)
验证(Verify)
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
-
文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
-
元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
-
字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
-
符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
-
准备(Prepare)
- 为类的静态变量分配内存并设置该类的默认初始值,即零值。
- 但是final修饰的static除外,因为final 在编译的时候就会分配了,准备阶段会显式初始化。
- 这里不会为非静态变量分配并初始化,静态变量会分配在方法区中,而非静态变量会随着对象一起被分配到java堆中。
解析(Resolve)
- 将常量池中的符号引用转为直接引用。
- 解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。
- 直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
- 解析动作主要针对类或者接口、字段、类方法、接口方法、方法类型等。对应常量池中间的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
初始化(Initialization)
- 初始化阶段就是执行类构造器
<clinit>()
的过程 - 此方法不需要定义,是javac编译器自动收集类中所有静态变量和静态代码块中的语句合并而来的。
<clinit>()
方法中指令按语句在源文件中的出现顺序执行。<clinit>()
不同于类的构造器(<init>()
)。- 若该类具有父类,JVM保证子类的
<clinit>()
执行前,父类的<clinit>()
已经执行完毕。 - 虚拟机必须保证一个类的
<clinit>()
方法在多线程下被同步加锁,否则会出现重复加载问题。 <clinit>()
不是必须有的,当类及其父类中没有静态变量或者静态代码块时,是没有这个<clinit>()
方法的。
通过实例查看字节码
- 安装插件
- 如果使用idea的编译器的话,可以直接安装一个叫jclasslib的插件就行,然后重启。
- 如果不是,则直接安装这个软件就行,名字一样。
- 测试代码
public class ClassLoaderTest {
static int j = 0;
static int k = 0;
}
-
查看反编译后的类的内容。
-
idea点击build->Recompile ‘xxx.java’
-
然后点击菜单栏中的View-> 'Show ByteCode With Jclasslib’然后就会出现这样的内容
-
查看方法,里面就会出现
<clinit>()
方法,如下图
-
当代码改为如下时,即添加静态代码块时,然后查看ByteCode,如下图:
public class ClassLoaderTest { static int j = 0; static int k = 0; static { j = 10; k = 20; System.out.println("hello world"); } }
- 如果这个类及父类都没有静态变量或者静态代码块时,是没有()方法的。
public class ClassLoaderTest { int j = 0; int k = 0; // static { // j = 10; // k = 20; // System.out.println("hello world"); // } }
-
-
更改代码
public class ClassLoaderTest {
static {
j = 10;
k = 20;
}
static int j = 0;
static int k = 0;
public static void main(String[] args) {
System.out.println(j);
System.out.println(k);
}
}
这样会不会报编译错误呢?
答案是不会的。
为什么呢?
因为在类加载的Linking中的准备(Prepare)阶段,此时JVM会给类的静态静态变量分配内存并赋初始值,即零值;而在静态代码块执行的时候,已经到了初始化阶段中的<clinit>()
,所以不会报编译错误。
那么执行结果是什么呢?
答案:j k 全为0。
为什么呢?
这就要说到这个<clinit>()
的收集规则了,HotSpot 虚拟机中,初始化(Initialization)阶段中<clinit>()
方法中指令按语句在源文件中的出现顺序执行。所以先执行静态代码块中的赋值,然后再执行后面的赋值。所以j k 全部为0。
- 再次更改代码
public class ClassLoaderTest {
static int k = 0;
static {
j = 10;
k = 20;
System.out.println(j);
System.out.println(k);
}
static int j = 0;
}
这里会报编译时错误嘛?
答案是的,在句输出j语句那里会报一个编译时错误:Illegal forward reference
,翻译过来就是非法的前向引用。
那为啥前面的赋值就没啥问题呢,我就输出了一下,就编译错误了?
我先说说别人的观点:
- 此时 j 静态变量,只是一个符号引用,还没初始化完成,没有到达解析阶段,所以不能直接调用。
- 编译器所决定的。
首先说明,下面的解读是个人理解;再来说说第一个观点,虽然他说的多,但是不一定对啊,前半句说的没毛病,j此时确实是一个符号引用,如果他说的是对的,那么按道理来说,System.out.println(k);这一句也会报错,因为如果j是一个符号引用,那么k也一定是一个符号引用,因为这个时候没有到初始化阶段,j没被解析成直接引用,那么k肯定也没有被解析成直接引用,所以应该和这个符号引用没啥关系。
再来看看第二种解释,好嘛,直接甩锅给编译器,但是这个在笔者来看,反而是合理的,原因就是在oracle官网关于java的文档中找到了这样几行字(文档官网传送门):
8.3.2.3. Restrictions on the use of Fields during Initialization //初始化期间对字段使用的限制
The declaration of a member needs to appear textually before it is used only if the member is an instance (respectively static) field of a class or interface C and all of the following conditions hold: //只有当成员是类或接口C的实例(分别是静态的)字段,并且具备以下条件时,成员的声明才需要在使用之前以文本形式出现:
The usage occurs in an instance (respectively static) variable initializer of C or in an instance (respectively static) initializer of C. //这种用法发生在C的实例(分别是静态的)变量初始化器或C的实例(分别是静态的)初始化器中。
The usage is not on the left hand side of an assignment. //用法不在赋值表达式的左边。
The usage is via a simple name.//使用方法是通过一个简单的名称。
C is the innermost class or interface enclosing the usage. //C是包含使用的最内部的类或接口。
然后我们来解析这几句话,只有全部满足这四点要求的,那么这个静态变量就必须在调用前声明,注意这里的调用不包括赋值,因为在第二点就排除了赋值,即用法不在赋值表达式的左边,即右值引用。首先必须是在类的静态变量初始化器或者类初始化器中使用,上面代码中在静态代码块中,满足第一个点,第二页满足,并不是赋值,而是调用,也满足,第三点,使用方法是通过一个简单的名称,而不是通过ClassLoaderTest.j
这种形式去调用,也满足,第四点,C(ClassLoaderTest) 是包含使用的最内部的类或者接口,j静变量本来就是在ClassLoaderTest类中,所以也满足,所以,满足这四点的,那么(静态变量)就必须在调用前声明。
类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
分类
按照HotSpot JVM规定(HotSpot JVM规范中说明:直接或者间接派生于java.lang.ClassLoader
类的加载器都属于用户自定义加载器,这里可能会被误解,所以特意做此说明。)来说,一共是分为俩类。
-
BootStrapClassLoader(引导类加载器)
- 它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自
java.lang.ClassLoader
(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是java.lang.ClassLoader
子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作 - 查看BootStrapClassLoader负责加载哪些类
public class ClassLoaderTest { // static int k = 0; // static { // j = 10; // k = 20; // k = ClassLoaderTest.j; // } // static int j = 0; public static void main(String[] args) { URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for(URL url : urls){ System.out.println(url.toExternalForm()); } } }
- 它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自
-
User-Defined-ClassLoader(用户自定义类加载器)
- 扩展类加载器(
sun.misc.Launcher.ExtClassLoader
)
它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null(这里的父类不是指继承的关系)。 - 系统类加载器(
sun.misc.Launcher.AppClassLoader
)
也称为应用类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为sun.misc.Launcher.ExtClassLoader
。
- 扩展类加载器(
-
源码解析用户自定义类加载器
下面就通过源码来分析为啥扩展类加载器的父类是null,而系统类加载器的父类是扩展类加载器。
查看sun.misc.Launcher
类的构造方法,这个类是JVM在应用程序上的入口。public Launcher() { Launcher.ExtClassLoader var1; try { //这里初始化了扩展类加载器 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); if (var2 != null) { SecurityManager var3 = null; if (!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) { } catch (InstantiationException var6) { } catch (ClassNotFoundException var7) { } catch (ClassCastException var8) { } } else { var3 = new SecurityManager(); } if (var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); } System.setSecurityManager(var3); } }
我们通过源代码知道,扩展类加载器就是通过
Launcher.ExtClassLoader.getExtClassLoader();
语句实现的,然我我们来看看Launcher.ExtClassLoader.getExtClassLoader();
方法的具体实现。//Launcher.ExtClassLoader.getExtClassLoader()方法,ExtClassLoader是Launcher的一个内部类,把主要的方法写出来,其他的方法就直接省略掉了 static class ExtClassLoader extends URLClassLoader { //这里定义了一个扩展加载器的单例,通过getExtClassLoader 进行初始化; private static volatile Launcher.ExtClassLoader instance; public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { if (instance == null) { Class var0 = Launcher.ExtClassLoader.class; synchronized(Launcher.ExtClassLoader.class) { if (instance == null) { instance = createExtClassLoader(); } } } return instance; } //创建一个扩展类实例 private static Launcher.ExtClassLoader createExtClassLoader() throws IOException { try { return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() { public Launcher.ExtClassLoader run() throws IOException { File[] var1 = Launcher.ExtClassLoader.getExtDirs(); int var2 = var1.length; for(int var3 = 0; var3 < var2; ++var3) { MetaIndex.registerDirectory(var1[var3]); } return new Launcher.ExtClassLoader(var1); } }); } catch (PrivilegedActionException var1) { throw (IOException)var1.getException(); } } void addExtURL(URL var1) { super.addURL(var1); } public ExtClassLoader(File[] var1) throws IOException { super(getExtURLs(var1), (ClassLoader)null, Launcher.factory); SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this); } //这里就是去获取扩展类加载器需要加载那些类。 private static File[] getExtDirs() { String var0 = System.getProperty("java.ext.dirs"); File[] var1; if (var0 != null) { StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator); int var3 = var2.countTokens(); var1 = new File[var3]; for(int var4 = 0; var4 < var3; ++var4) { var1[var4] = new File(var2.nextToken()); } } else { var1 = new File[0]; } return var1; } }
由上面知道,ExtClassLoader 扩展类加载就是一个很普通的DCL单例,而设置父类,则是通过ExtClassLoader(File[] var1)这个构造方法中,调用父类构造方法,其中第二个参数就是父类加载器对象,这里传入的就是一个null。
然后系统类加载器就是在Launcher下的loader变量,则是通过this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);来设置系统类加载器,传入的参数就是扩展类加载器对象。然后跟进代码看一下是怎么实现的。
static class AppClassLoader extends URLClassLoader { final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this); public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { final String var1 = System.getProperty("java.class.path"); final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1); return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() { public Launcher.AppClassLoader run() { URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2); //这里就通过构造方法返回一个系统类加载器的实例。 return new Launcher.AppClassLoader(var1x, var0); } }); } AppClassLoader(URL[] var1, ClassLoader var2) { //这里调用父类构造方法,将生成的扩展类加载器设置成父类加载器。 super(var1, var2, Launcher.factory); this.ucp.initLookupCache(this); } 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); } } protected PermissionCollection getPermissions(CodeSource var1) { PermissionCollection var2 = super.getPermissions(var1); var2.add(new RuntimePermission("exitVM")); return var2; } private void appendToClassPathForInstrumentation(String var1) { assert Thread.holdsLock(this); super.addURL(Launcher.getFileURL(new File(var1))); } private static AccessControlContext getContext(File[] var0) throws MalformedURLException { PathPermissions var1 = new PathPermissions(var0); ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1); AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2}); return var3; } static { ClassLoader.registerAsParallelCapable(); } }
由上面源代码知道,getAppClassLoader方法最后返回的是一个含有俩个参数的构造方法创建的实例对象。和扩展类加载器一样,都是调用父类构造方法,不同的是,扩展类加载器调用父类构造方法时,第二个参数也就父类加载器实例,传的是null,而系统类加载器传的是扩展类加载器。
-
验证源码解读是否正确
public class ClassLoaderTest {
// static int k = 0;
// static {
// j = 10;
// k = 20;
// k = ClassLoaderTest.j;
// }
// static int j = 0;
public static void main(String[] args) {
//获取自定义类的加载器,即系统类加载器
ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(appClassLoader);
//获取扩展类加载,即系统类加载器的父类加载器
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println(extClassLoader);
//获取引导类加载器(BootStrapClassLoader)
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println(bootStrapClassLoader);
}
}
由上面的结果来看,我们对源码的解读是没有错的,确实如此。
-
双亲委派机制
- 说明:这个是高频面试题,其实这个不难,首先了解一下HotSpot虚拟机加载类的方式是怎么样的,它对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会去将对应的class文件加载到内存中生成Class对象,而加载某个类的class文件时,虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。
- 用一个小实例来说明这个机制。
//创建一个包名为java.lang,类名为String类,然后添加一个静态代码块,然后另外一个类再测试一下。 package java.lang; public class String { static { System.out.println("自定义java.lang.String类"); } } public class ClassLoaderTest { // static int k = 0; // static { // j = 10; // k = 20; // k = ClassLoaderTest.j; // } // static int j = 0; public static void main(String[] args) { String str = new java.lang.String(); } }
- 工作原理
-
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
-
如果父类加载还存在其父类加载器,则进一步向上委托,一次递归,请求最终到达顶层的引导类加载器。
-
如果父类加载器可以完成类加载,就成功返回,若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
-
图例
-
结论:java.lang包下的类是由BootStrapClassLoader进行加载的,那么new出来的对象,是java核心api下的java.lang.String,而不是自定义的java.lang.String类。
-
继续更改自定义String类代码
-
package java.lang; public class String { public static void main(String[] args) { System.out.println("自定义java.lang.String类"); } }
运行发现报错了,错误如下:
添加一个自定义类比如java.lang.Zeroable
package java.lang; public class Zeroable { public static void main(String[] args) { System.out.println("zeroable!"); } }
运行如下:
报了一个安全错误。
这样就更加解释了这种双亲委派机制的原理了- 双亲委派机制的优势
- 避免类的重复加载
- 保护程序安全,避免核心API被篡改(沙箱安全机制)。
- 类的主动使用和被动使用
- 主动使用分为七种情况
- 创建类的实例
- 访问某个类或者接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如Class.forName(“com.zeroable.Test”);)
- 初始化一个子类
- java虚拟机启动时被表名为启动的类
- JDK7开始提供的动态语言支持。
- 除了上面的七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
- 主动使用分为七种情况