1、类加载子系统 在介绍类的加载过程之前,先看看类加载子系统的组成。
类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。
2、类的加载过程 2.1 类的加载过程一:Loading
1.首先通过一个类的全限定名来获取此类的二进制字节流。
2.其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.最后在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.2 类的加载过程二:Linking
2.3 类的加载过程三:Initialization
为类的静态变量赋予正确的初始值 。
初始化阶段就是执行类构造器方法()的过程。 ()方法不需要定义,是javac编译器自动收集类中的所有类变量 的赋值动作和静态代码块 中的语句合并而来。
1 2 3 4 5 6 7 8 9 10 11 public class ClassInitTest { private static int num = 1 ; static { num = 2 ; } public static void main (String[] args) { System.out.println(ClassInitTest.num); } }
查看()方法内容如下:
1 2 3 4 5 0 iconst_1 1 putstatic #3 <com/company/java01/ClassInitTest.num> 4 iconst_2 5 putstatic #3 <com/company/java01/ClassInitTest.num> 8 return
1 2 3 4 5 6 7 8 9 10 11 12 13 public class InitTest { private static int a = 1 ; static { a = 2 ; b = 20 ; System.out.println(b); } private static int b = 10 ; public static void main (String[] args) { System.out.println(InitTest.a); System.out.println(InitTest.b); } }
从字节码文件中的()可看到static变量的赋值过程:
1 2 3 4 5 6 7 8 9 0 iconst_1 1 putstatic #3 <com/jvm/demo/InitTest.a> 4 iconst_2 5 putstatic #3 <com/jvm/demo/InitTest.a> 8 bipush 20 10 putstatic #5 <com/jvm/demo/InitTest.b> 13 bipush 10 15 putstatic #5 <com/jvm/demo/InitTest.b> 18 return
()不同于类的构造函数(构造器是虚拟机视角的())。当没在类中写构造函数时,则使用的是默认的构造器,从字节码中可看到只调用了父类Object的构造器:
1 2 3 0 aload_0 1 invokespecial #1 <java/lang/Object.<init>> 4 return
如果写了自定义的构造器,那么在中会有相应的初始化:
1 2 3 4 5 6 7 8 9 public class InitTest2 { private int a = 10 ; public InitTest2 () { a = 20 ; int b = 30 ; } public static void main (String[] args) { } }
对应的:
1 2 3 4 5 6 7 8 9 10 11 0 aload_0 1 invokespecial #1 <java/lang/Object.<init>> 4 aload_0 5 bipush 10 7 putfield #2 <com/jvm/demo/InitTest2.a> 10 aload_0 11 bipush 20 13 putfield #2 <com/jvm/demo/InitTest2.a> 16 bipush 30 18 istore_1 19 return
因为此类中没有静态变量,所以字节码文件中自然也不会有()。
若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ClinitTest1 { static class Father { public static int A = 1 ; static { A = 2 ; } } static class Son extends Father { public static int B = A; } public static void main (String[] args) { System.out.println(Son.B); } }
son类的字节码文件中的()方法如下:
1 2 3 0 getstatic #2 <com/company/java01/ClinitTest1$Son.A>//父类已经加载过 3 putstatic #3 <com/company/java01/ClinitTest1$Son.B> 6 return
虚拟机必须保证一个类的()方法在多线程下被同步加锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class ThreadTest { public static void main (String[] args) { Runnable r = () -> { System.out.println(Thread.currentThread().getName() + "开始" ); DeadThread deadThread = new DeadThread (); System.out.println(Thread.currentThread().getName() + "结束" ); }; Thread r1 = new Thread (r,"线程一" ); Thread r2 = new Thread (r,"线程二" ); r1.start(); r2.start(); } } class DeadThread { static { if (true ){ System.out.println(Thread.currentThread().getName() + "初始化当前类" ); while (true ){ } } } }
输出结果为:
可见线程一首先抢到DeadThread的调用,因为DeadThread类中有死循环导致线程一出不来,此时线程二也无法调用DeadThread了。一个类只会被加载一次,加载后会被放在方法区缓存起来,即类在加载时只会调用一次()方法。
3、加载器
从上面可以知道,类加载器基本职责就是根据类的二进制名(binary name)读取java编译器编译好的字节码文件(.class文件),并且转化生成一个java.lang.Class类的一个实例。这样的每个实例用来表示一个Java类,jvm就是用这些实例来生成java对象的。基本上所有的类加载器都是java.lang.ClassLoader 类的一个实例。
总的来说,jvm支持两种类型的类加载器,分别为引导类加载器 和自定义类加载器 (jvm规范中将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ClassLoaderTest { public static void main (String[] args) { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader); ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader); ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1); } }
3.1 引导类加载器(启动类加载器):BootstrapClassLoader
这个类加载器是使用C/C++语言实现的,嵌套在JVM内部。
它用来加载java的核心类库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容等),用于提供JVM自身需要的类。
引导类加载器具体加载哪些核心代码可以通过获取值为 “sun.boot.class.path” 的系统属性获得。
它不是java原生代码编写的,所以其也不是java.lang.ClassLoader类的实例,其没有getParent方法。
它加载拓展类和应用程序类加载器,并指定为它们的父类加载器。
出于安全考虑,引导类加载器只加载包名为java、javax、sun等开头下的类。
通过运行System.out.println(System.getProperty(“sun.boot.class.path”))可得到如下信息:
1 2 3 4 5 6 7 8 /opt/jdk1.8.0_202/jre/lib/resources.jar; /opt/jdk1.8.0_202/jre/lib/rt.jar; /opt/jdk1.8.0_202/jre/lib/sunrsasign.jar; /opt/jdk1.8.0_202/jre/lib/jsse.jar; /opt/jdk1.8.0_202/jre/lib/jce.jar; /opt/jdk1.8.0_202/jre/lib/charsets.jar; /opt/jdk1.8.0_202/jre/lib/jfr.jar; /opt/jdk1.8.0_202/jre/classes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ClassLoaderTest { public static void main (String[] args) { System.out.println("**********启动类加载器**************" ); URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (URL element : urLs) { System.out.println(element.toExternalForm()); } ClassLoader classLoader = Provider.class.getClassLoader(); System.out.println(classLoader); } }
3.2 拓展类加载器:ExtensionClassLoader
拓展类加载器用来加载jvm实现的一个拓展目录,该目录下的所有java类都由此类加载器加载。此路径可以通过获取”java.ext.dirs”的系统属性获得。
Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
派生于java.lang.ClassLoader,即为ClassLoader类的一个实例。
父类加载器为启动类加载器(引导类加载器)。
如果用户创建的jar文件在/jre/lib/ext目录(拓展目录)下,也会自动由拓展类加载器加载。
其有关类继承关系如下所示:
通过运行System.out.println(System.getProperty(“java.ext.dirs”))可得到如下信息:
1 2 /opt/jdk1.8.0_202/jre/lib/ext; /usr/java/packages/lib/ext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class ClassLoaderTest { public static void main (String[] args) { System.out.println("***********扩展类加载器*************" ); String extDirs = System.getProperty("java.ext.dirs" ); for (String path : extDirs.split(":" )) { System.out.println(path); } ClassLoader classLoader1 = CurveDB.class.getClassLoader(); System.out.println(classLoader1); } }
3.2 应用类加载器(系统类加载器):AppClassLoader
应用类加载器又称为系统类加载器,开发者可用通过 java.lang.ClassLoader.getSystemClassLoader()方法获得此类加载器的实例,系统类加载器也因此得名。
Java语言编写,由sun.misc.Launcher$AppClassLoader实现。
派生于java.lang.ClassLoader,即为ClassLoader类的一个实例。
它负责加载环境变量classpath或系统属性”java.class.path”指定路径下的类库。
它是程序中默认的类加载器,一般来说,java应用都是用此类加载器完成加载的。
父类加载器为拓展类加载器。
其有关类继承关系如下所示:
通过运行System.out.println(System.getProperty(“java.class.path”))可得到如下信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /opt/jdk1.8.0_202/jre/lib/charsets.jar /opt/jdk1.8.0_202/jre/lib/deploy.jar /opt/jdk1.8.0_202/jre/lib/ext/cldrdata.jar /opt/jdk1.8.0_202/jre/lib/ext/dnsns.jar /opt/jdk1.8.0_202/jre/lib/ext/jaccess.jar /opt/jdk1.8.0_202/jre/lib/ext/jfxrt.jar /opt/jdk1.8.0_202/jre/lib/ext/localedata.jar /opt/jdk1.8.0_202/jre/lib/ext/nashorn.jar /opt/jdk1.8.0_202/jre/lib/ext/sunec.jar /opt/jdk1.8.0_202/jre/lib/ext/sunjce_provider.jar /opt/jdk1.8.0_202/jre/lib/ext/sunpkcs11.jar /opt/jdk1.8.0_202/jre/lib/ext/zipfs.jar /opt/jdk1.8.0_202/jre/lib/javaws.jar /opt/jdk1.8.0_202/jre/lib/jce.jar /opt/jdk1.8.0_202/jre/lib/jfr.jar /opt/jdk1.8.0_202/jre/lib/jfxswt.jar /opt/jdk1.8.0_202/jre/lib/jsse.jar /opt/jdk1.8.0_202/jre/lib/management-agent.jar /opt/jdk1.8.0_202/jre/lib/plugin.jar /opt/jdk1.8.0_202/jre/lib/resources.jar /opt/jdk1.8.0_202/jre/lib/rt.jar /home/zhu/Desktop/jvmdemo/out/production/jvmdemo //自己开发的应用 /home/zhu/Downloads/idea-IU-192.7142.36/lib/idea_rt.jar
3.3 用户自定义类加载器
在Java的日常开发中,类几乎是由上面三种加载器配合加载的,但在必要时还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
隔离加载类
修改类的加载方式
扩展加载源
防止源码泄露(对字节码文件进行加密后要运行时需解密,可以自定义类加载器解密)
用户自定义类加载器实现步骤:
继承抽象类ClassLoader。
在jdk1.2之前需要继承抽象类ClassLoader并重写loadClass()方法,但在jdk1.2后不再建议此方法,而是建议把自定义类加载逻辑写在findClass()方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte [] result = getClassFromCustomPath(name); if (result == null ){ throw new FileNotFoundException (); }else { return defineClass(name,result,0 ,result.length); } } catch (FileNotFoundException e) { e.printStackTrace(); } throw new ClassNotFoundException (name); } private byte [] getClassFromCustomPath(String name){ return null ; } public static void main (String[] args) { CustomClassLoader customClassLoader = new CustomClassLoader (); try { Class<?> clazz = Class.forName("One" ,true ,customClassLoader); Object obj = clazz.newInstance(); System.out.println(obj.getClass().getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } }
如果自定义类加载器没有过多复杂的需求,可以直接继承URLClassLoader类,这样可以避免重写findClass()方法以及获取字节码流的方式,使自定义类加载器编写更加简洁。
3.4 关于ClassLoader
所有的类加载器(除了启动类加载器)都继承于抽象类ClassLoader。
这个类加载器的一些核心方法:
方法名
说明
getParent()
返回该类加载器的父类加载器
loadClass(String name)
加载名为name的类,返回java.lang.Class类的实例
findClass(String name)
查找名字为name的类,返回的结果是java.lang.Class类的实例
findLoadedClass(String name)
查找名字为name的已经被加载过的类,返回的结果是java.lang.Class类的实例
defineClass(String name,byte[] b,int off,int len)
根据字节数组b中的数据转化成Java类,返回的结果是java.lang.Class类的实例
获取类加载器途径:
1 2 3 4 5 6 7 8 public static void main (String[] args) { try { ClassLoader classLoader = Class.forName("java.lang.String" ).getClassLoader(); System.out.println(classLoader); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
1 2 3 4 5 6 7 8 9 10 public class ClassLoaderTest2 { public static void main (String[] args) { try { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); System.out.println(contextClassLoader); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
1 2 3 4 5 6 7 8 9 10 public class ClassLoaderTest2 { public static void main (String[] args) { try { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader().getParent(); System.out.println(systemClassLoader); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
获取调用者的ClassLoader(DriverManager.getCallerClassLoader())。
3.5 双亲委派机制
1 2 3 4 5 6 7 package java.lang;public class String { static { System.out.println("我是自定义的String类" ); } }
然后进行引用,进行类加载:
1 2 3 4 5 6 7 package com.jvm.demo;public class StringTest { public static void main (String[] args) { java.lang.String str = new java .lang.String(); } }
运行后发现自定义类中的static代码块并没有执行,原因是双亲委派机制,前面说过引导类加载器负责加载java、javax、sun开头的包下的类,由于该自定义类在java包下,在向上委托的过程中交给了引导类加载器加载,所以实际加载的是java核心类库内的String类。
而通过打印下方StringTest类的加载器类型,可以知道是预期的sun.misc.Launcher$AppClassLoader,是因为它在com.jvm.demo包下,并不归应用类加载器上层的拓展类加载器和引导类加载器加载。
1 2 3 4 5 6 7 8 package com.jvm.demo;public class StringTest { public static void main (String[] args) { StringTest stringTest = new StringTest (); System.out.println(stringTest.getClass().getClassLoader()); } }
如果在自定义String类中定义main方法并运行:
1 2 3 4 5 6 7 package java.lang;public class String { public static void main (String[] args) { System.out.println("我是自定义的String类" ); } }
会发现报错如下:
1 2 3 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application
更加确切地说明加载的String类是核心代码库里的String类,而里面并没有main()方法,因而报错,从而保证了无法随意篡改类定义。
如果在java.lang包下自定义一个核心api没有的类:
1 2 3 4 5 6 7 package java.lang;public class MyTest { public static void main (String[] args) { System.out.println("我是java.lang包下自定义的类" ); } }
运行后会发现报错如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662) at java.lang.ClassLoader.defineClass(ClassLoader.java:761) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:468) at java.net.URLClassLoader.access$100(URLClassLoader.java:74) at java.net.URLClassLoader$1.run(URLClassLoader.java:369) at java.net.URLClassLoader$1.run(URLClassLoader.java:363) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:362) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
原因是虽然此类在双亲委派机制中交给了引导类加载器加载,但是出于安全考虑,访问java.lang包下的类是需要权限的,它阻止我们用此包名去定义自定义类。试想如果加载这个自定义类成功了,可能会对引导类加载器造成破坏,保证了安全。
双亲委派机制的好处:
避免类的重复加载。
保护程序安全,防止核心API被随意篡改,即沙箱安全机制。
3.6 类的主动和被动使用
类的主动使用分为以下几种情况,其它情况均视为被动使用:
创建类的实例。
访问某个类或接口的静态变量,或者对该静态变量赋值。
调用类的静态方法。
反射(如Class.forName(“com.bunny.Test”))。
初始化一个类的子类。
Java虚拟机启动时被表明为启动类的类(JavaTest)。
Jdk7开始提供的动态语言支持。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class ClassUsed { public static int a = 0 ; public static void main (String[] args) throws Exception{ ClassUsed classUsed = new ClassUsed (); int b = ClassUsed.a; ClassUsed.test(); Class.forName("com.bunny.Test" ); ChildClass.c = 10 ; } public static void test () { } }