java的类加载机制
一 类加载器概述
java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制.JVM中用来完成上述功能的具体实现就是类加载器.类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例.每个实例用来表示一个java类.通过该实例的newInstance()方法可以创建出一个该类的对象.
二 类加载过程
2.1 类加载过程
JVM的类加载过程一共有三个步骤:装载(Load),链接(Link)和初始化(Initialize)三个步骤。过程如下图所示:
2.1.1,加载
加载是类装载的第一步,内存中生成一个代表这个类的java.lang.class对象,通过class文件的路径读取到二进制流,并解析二进制里的元数据(类型,常量等),作为方法区这个类的各种数据量的入口
2.1.2,连接
连接又可分为验证,准备,解析。
1,验证
验证主要是判断class文件的合法性,对版本号进行验证(例如如果使用java1.8编译后的class文件要再java1.6虚拟机上运行),还会对元数据,字节编码等进行验证,确保class文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全。
2,准备
准备主要是分配内存,为变量分配初始值,即在方法区中分配这些变量所使用的内存空间,例如:
public static int i = 1;
在准备阶段i的值会被初始化为0,后面的类的初始化阶段才会赋值为1;
public static final int i = 1;
对应常量(static final)i,在准备阶段就会被赋值1;
3,解析
解析就是把代码中的符号引用替换为直接引用;例如某个类继承了java.lang.Object,原来的符号引用记录的是“java.lang.Object”,并不是java.lang,Object对象,直接引用就是找出对应的java.lang.Object对应的内存地址,建立直接引用关系;
2.1.3,初始化
初始化的过程包括执行类构造器方法,static变量赋值语句,static{}代码块,如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初始化;所以其实在java中初始化一个类,那么必然是先初始化java.lang.Object,因为所有的java类都继承自java.lang.Object。
2.2 类的生命周期
一个类的生命周期如下图所示:
三 类加载器分类
在JVM当中预定义了三种类型的类加载器:启动类加载器,扩展类加载器,系统类加载器。每个加载器其实就是个类的对象。
ExtClassLoader,AppClassLoder继承URLClassLoader,而URLClassLoader继承ClassLoader,BoopStrap ClassLoder不在上图中,因为它是由C/C++编写的,它本身是虚拟机的一部分,并不是一个java类。jvm加载的顺序:BoopStrap ClassLoder-〉ExtClassLoader->AppClassLoder
从源码中我们看到:
(1)Launcher初始化的时候创建了ExtClassLoader以及AppClassLoader,并将ExtClassLoader实例传入到AppClassLoader中。
(2)虽然上一段源码中没见到创建BoopStrap ClassLoader,但是程序一开始就执行了System.getProperty("sun.boot.class.path")。
AppClassLoader的父加载器为ExtClassLoader,ExtClassLoader的父加载器为null,BoopStrap ClassLoader为顶级加载器。
public class Launcher { private static Launcher launcher = new Launcher(); private static String bootClassPath = System.getProperty("sun.boot.class.path"); public static Launcher getLauncher() { return launcher; } private ClassLoader loader; public Launcher() { // Create the extension class loader ClassLoader extcl; try { extcl = ExtClassLoader.getExtClassLoader(); } catch (IOException e) { throw new InternalError( "Could not create extension class loader", e); } // Now create the class loader to use to launch the application try { loader = AppClassLoader.getAppClassLoader(extcl); } catch (IOException e) { throw new InternalError( "Could not create application class loader", e); } Thread.currentThread().setContextClassLoader(loader); } /* * Returns the class loader used to launch the main application. */ public ClassLoader getClassLoader() { return loader; } /* * The class loader used for loading installed extensions. */ static class ExtClassLoader extends URLClassLoader {} /** * The class loader used for loading from java.class.path. * runs in a restricted security context. */ static class AppClassLoader extends URLClassLoader {}
java程序的入口就是sun.misc.Launcher了,这个类里面又继承了ExtClassLoader和AppClassLoader和bootstrap的url路径。
3.1、Launch类初始化
- private static Launcher launcher = new Launcher();
- private static String bootClassPath = System.getProperty("sun.boot.class.path");
- public static Launcher getLauncher() {
- return launcher;
- }
- private ClassLoader loader;
- public Launcher() {
- // Create the extension class loader
- ClassLoader extcl;
- try {
- extcl = ExtClassLoader.getExtClassLoader();
- } catch (IOException e) {
- throw new InternalError(
- "Could not create extension class loader");
- }
- // Now create the class loader to use to launch the application
- try {
- loader = AppClassLoader.getAppClassLoader(extcl);
- } catch (IOException e) {
- throw new InternalError(
- "Could not create application class loader");
- }
- // Also set the context class loader for the primordial thread.
- Thread.currentThread().setContextClassLoader(loader);
- // Finally, install a security manager if requested
- String s = System.getProperty("java.security.manager");
- ......
- }
- /*
- * Returns the class loader used to launch the main application.
- */
- public ClassLoader getClassLoader() {
- return loader;
- }
可以看到Launcher类初始化时,先初始化了个ExtClassLoader,然后又初始化了个AppClassLoader,然后把ExtClassLoader作为AppClassLoader的父loader。
ExtClassLoader没有指定父类,即表明,父类是BootstrapClassLoader。
把初始化的AppClassLoader 作为全局变量保存起来,并设置到当前线程contextClassLoader。
每个线程实例可以设置一个contextClassLoader
3.2、bootClassPath
Launcher 类有个全局的变量private static String bootClassPath = System.getProperty("sun.boot.class.path") 表示bootclassloader去哪里加载类和资源,看以下测试。
- @Test
- public void test3(){
- System.out.println("bootstrap classload----------------------");
- final String s = System.getProperty("sun.boot.class.path");
- System.out.println(s);
- final File[] path = (s == null) ? new File[0] : getClassPath(s);
- for(File f : path){
- System.out.println(f);
- }
- System.out.println();
- sun.misc.Launcher launcher = sun.misc.Launcher.getLauncher();
- System.out.println(launcher.getClass().getClassLoader());
- }
输出:
- bootstrap classload----------------------
- C:\Program Files\Java\jre1.8.0_77\lib\resources.jar;C:\Program Files\Java\jre1.8.0_77\lib\rt.jar;C:\Program Files\Java\jre1.8.0_77\lib\sunrsasign.jar;C:\Program Files\Java\jre1.8.0_77\lib\jsse.jar;C:\Program Files\Java\jre1.8.0_77\lib\jce.jar;C:\Program Files\Java\jre1.8.0_77\lib\charsets.jar;C:\Program Files\Java\jre1.8.0_77\lib\jfr.jar;C:\Program Files\Java\jre1.8.0_77\classes
- C:\Program Files\Java\jre1.8.0_77\lib\resources.jar
- C:\Program Files\Java\jre1.8.0_77\lib\rt.jar
- C:\Program Files\Java\jre1.8.0_77\lib\sunrsasign.jar
- C:\Program Files\Java\jre1.8.0_77\lib\jsse.jar
- C:\Program Files\Java\jre1.8.0_77\lib\jce.jar
- C:\Program Files\Java\jre1.8.0_77\lib\charsets.jar
- C:\Program Files\Java\jre1.8.0_77\lib\jfr.jar
- C:\Program Files\Java\jre1.8.0_77\classes
- null
3.3、ExtClassLoader
extclassloader作为java中间的一个loader,实例化之后就放到classloader链里面了。
- @Test
- public void test4(){
- System.out.println("ext classload----------------------");
- final String s = System.getProperty("java.ext.dirs");//对应路径
- System.out.println(s);
- File[] dirs;
- if (s != null) {
- StringTokenizer st =
- new StringTokenizer(s, File.pathSeparator);
- int count = st.countTokens();
- dirs = new File[count];
- for (int i = 0; i < count; i++) {
- dirs[i] = new File(st.nextToken());
- }
- } else {
- dirs = new File[0];
- }
- for(File f:dirs){
- System.out.println(f.getAbsolutePath());
- }
- }
输出:
- ext classload----------------------
- C:\Program Files\Java\jre1.8.0_77\lib\ext;C:\Windows\Sun\Java\lib\ext
- C:\Program Files\Java\jre1.8.0_77\lib\ext
- C:\Windows\Sun\Java\lib\ext
extclassloader的url有点特殊,System.getProperty("java.ext.dirs")得到的是路径,jvm会把路径下面的所有文件都作为一个单独的url处理(一层路径)。
- private static URL[] getExtURLs(File[] dirs) throws IOException {
- Vector<URL> urls = new Vector<URL>();
- for (int i = 0; i < dirs.length; i++) {
- String[] files = dirs[i].list();
- if (files != null) {
- for (int j = 0; j < files.length; j++) {
- if (!files[j].equals("meta-index")) {
- File f = new File(dirs[i], files[j]);
- urls.add(getFileURL(f));
- }
- }
- }
- }
- URL[] ua = new URL[urls.size()];
- urls.copyInto(ua);
- return ua;
- }
3.4、appclassloader
- @Test
- public void test2(){
- System.out.println("app classload----------------------");
- final String s = System.getProperty("java.class.path");
- System.out.println(s);
- final File[] path = (s == null) ? new File[0] : getClassPath(s);
- for(File f : path){
- System.out.println(f);
- }
- }
输出:
- app classload----------------------
- E:\java\my_workspace\myclassload\bin;E:\program\eclipse4.4-navi-32_2\eclipse4.4-navi-32\plugins\org.junit_4.11.0.v201303080030\junit.jar;E:\program\eclipse4.4-navi-32_2\eclipse4.4-navi-32\plugins\org.hamcrest.core_1.3.0.v201303031735.jar;/E:/program/eclipse4.4-navi-32_2/eclipse4.4-navi-32/configuration/org.eclipse.osgi/362/0/.cp/;/E:/program/eclipse4.4-navi-32_2/eclipse4.4-navi-32/configuration/org.eclipse.osgi/361/0/.cp/
- E:\java\my_workspace\myclassload\bin
- E:\program\eclipse4.4-navi-32_2\eclipse4.4-navi-32\plugins\org.junit_4.11.0.v201303080030\junit.jar
- E:\program\eclipse4.4-navi-32_2\eclipse4.4-navi-32\plugins\org.hamcrest.core_1.3.0.v201303031735.jar
- E:\program\eclipse4.4-navi-32_2\eclipse4.4-navi-32\configuration\org.eclipse.osgi\362\0\.cp
- E:\program\eclipse4.4-navi-32_2\eclipse4.4-navi-32\configuration\org.eclipse.osgi\361\0\.cp
四:类加载过程的几个方法
(1)loadClass (2)findLoadedClass (3)findClass
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查是否已经加载过 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //父加载器不为空,调用父加载器的loadClass c = parent.loadClass(name, false); } else { //父加载器为空则,调用Bootstrap Classloader 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(); //父加载器没有找到,则调用findclass 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() resolveClass(c); } return c; } }
五、双亲委派机制
5.1 定义
JVM加载类时默认采用双亲委派 机制。通俗的讲就是,当某个类加载器收到加载类的请求时,首先会将加载任务任务委托给加载器的父类,然后父类再委托给父类的父类,以此类推。如果父类成功可以成功加载,就返回成功,如果父类加载器无法完成任务时,才会自己加载。
下图展示了双亲委派机制的运行逻辑。
启动类加载器 Bootstrap ClassLoader:加载<JAVA_HOME>\lib目录下核心库
扩展类加载器 Extension ClassLoader:加载<JAVA_HOME>\lib\ext目录下扩展包
应用程序类加载器 Application ClassLoader: 加载用户路径(classpath)上指定的类库
六、类的加载时机和类中内容的加载顺序
6.1、什么时候会加载类?
使用到类中的内容时加载:有三种情况
1.创建对象:new StaticCode();
2.使用类中的静态成员:StaticCode.num=9; StaticCode.show();
3.在命令行中运行:java StaticCodeDemo
6.2、类所有内容加载顺序和内存中的存放位置
利用语句进行分析:
1.Person p=new Person("zhangsan",20);
该句话所做的事情:
1.在栈内存中,开辟main函数的空间,建立main函数的变量 p。
加载类文件时,除了非静态成员变量(对象的特有属性)不会被加载,其它的都会被加载。
记住:加载,是将类文件中的一行行内容存放到了内存当中,并不会执行任何语句。---->加载时期,即使有输出语句也不会执行。
静态成员变量(类变量)----->方法区的静态部分
静态方法 ----->方法区的静态部分
静态代码块 ----->方法区的静态部分
非静态方法(包括构造函数)----->方法区的非静态部分
构造代码块 ----->方法区的静态部分
Java对象初始化顺序(静态变量、静态代码块、变量、动态代码块、构造器)
6.3示例代码
6.3.1.静态变量/静态代码块 > main方法 > 变量/动态代码块 > 构造器
- 示例代码1:
public class Test { static { System.out.println("静态代码块"); } public static void main(String[] args) { System.out.println("main方法"); } }
执行结果:
静态代码块 main方法
- 示例代码2:
public class Test { static { System.out.println("静态代码块"); } { System.out.println("动态代码块"); } public Test() { System.out.println("构造器"); } public static void main(String[] args) { new Test(); } }
执行结果:
静态代码块 动态代码块 构造器
6.3.2 静态变量/静态代码块初始化顺序,代码中先定义的先执行,(变量/动态代码块同理)
- 示例代码:
public class Test { static int i = 1; static { System.out.println("i=" + i); System.out.println("静态代码块1"); } // int i = 1; 写在此处将造成上一段静态代码块报错,变量还未定义 // 也说明执行过程是先定义先执行 static { System.out.println("静态代码块2"); } { System.out.println("动态代码块1"); } { System.out.println("动态代码块2"); } public static void main(String[] args) { new Test(); } }
执行结果:
i=1 静态代码块1 静态代码块2 动态代码块1 动态代码块2
非法向前引用:静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,再前面的静态语句块可以赋值,但不能访问
public class IllegallyReferringVariablesForwardTest {
static {
i = 0;//给变量赋值可以正常编译通过
//System.out.println(i);//这句编译器会提示“非法向前引用”
}
static int i = 1;
}
6.3.3静态代码块只执行一次,节点为该类被使用时
- 类被使用包含以下几种情况
-
- 创建新实例(new、反射、克隆、反序列化)
- 调用静态方法
- 使用静态变量
- 调用某些反射方法
- 初始化其子类
- JVM标记为启动类(包含main方法的类)
- 示例代码:
public class A { static String s = "静态变量"; static { System.out.println("静态代码块"); } } public class Test { public static void main(String[] args) { System.out.println(A.s); } }
执行结果:
静态代码块 静态变量
- 静态代码块并不是在所谓类加载过程中完成的,或者说不是严格意义上的类加载过程中完成的。
- JVM类加载机制:加载过程主要分三部分,加载 > 连接 > 初始化,静态变量的初始化是在第三部分初始化中完成的。
- 加载:根据类全限定名生成二进制流,转化对应数据结构(不是数据),生成类class对象放入方法区
- 链接:将上一步的二进制数据合并入JRE中(此阶段静态变量会被赋初值,如整型赋值0)
- 初始化:静态资源初始化
package ali_test; public class A { static { System.out.println("静态代码块"); } } public class Test { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); classLoader.loadClass("ali_test.A"); System.out.println("-------------"); Class.forName("ali_test.A"); System.out.println("-------------"); new A(); } }
执行结果:
------------- 静态代码块 -------------
- ClassLoader实际调用的是ClassLoader.loadClass(className,false),第二个参数为是否链接,也就表示不进行链接,也不会进行下一步初始化。
- Class.forName实际调用的是Class.forName(className,true,classloader),第二个参数为是否链接,也就表示会进行链接及下一步初始化。
- new对象没有输出静态代码块的打印内容因为在第二步已经进行了初始化。
注意:
在Person.class文件加载时,静态方法和非静态方法都会加载到方法区中,只不过要调用到非静态方法时需要先实例化一个对象,
对象才能调用非静态方法。如果让类中所有的非静态方法都随着对象的实例化而建立一次,那么会大量消耗内存资源,
所以才会让所有对象共享这些非静态方法,然后用this关键字指向调用非静态方法的对象。
3.执行类中的静态代码块:如果有的话,对Person.class类进行初始化。
4.开辟空间:在堆内存中开辟空间,分配内存地址。
5.默认初始化:在堆内存中建立 对象的特有属性,并进行默认初始化。
6.显示初始化:对属性进行显示初始化。
7.构造代码块:执行类中的构造代码块,对对象进行构造代码块初始化。
8.构造函数初始化:对对象进行对应的构造函数初始化。
9.将内存地址赋值给栈内存中的变量p。
2.p.setName("lisi");
1.在栈内存中开辟setName方法的空间,里面有:对象的引用this,临时变量name
2.将p的值赋值给this,this就指向了堆中调用该方法的对象。
3.将"lisi" 赋值给临时变量name。
4.将临时变量的值赋值给this的name。
3.Person.showCountry();
1.在栈内存中,开辟showCountry()方法的空间,里面有:类名的引用Person。
2.Person指向方法区中Person类的静态方法区的地址。
3.调用静态方法区中的country,并输出。
6.3.4类的主动引用(一定会发生类的初始化)
--new一个类的对象
--调用类的静态成员(除了final常量)和静态方法
--使用java.lang.reflect包的方法对类进行反射调用
--当初始化一个类,如果其父类没有被初始化,则先初始化他的父类
--当要执行某个程序时,一定先启动main方法所在的类
6.3.5类的被动引用(不会发生类的初始化)
--当访问一个静态变量时,只有真正生命这个静态变量的类才会被初始化(通过子类引用父类的静态变量,不会导致子类初始化)
--通过数组定义类应用,不会触发此类的初始化 A[] a = new A[10];
--引用常量(final类型)不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取Class对象,不会触发类的初始化。
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
- 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
6.3.6 不同类加载器加载的类本质上不是同一个类:
1.自定义类加载器测试
public class DifferentClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("test.classloader.DifferentClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof test.classloader.DifferentClassLoaderTest);
}
}
输出:class test.classloader.DifferentClassLoaderTest
false
2.spring-boot-devtools 造成 dubbo 调用失败
现象 : dubbo集成到spring-boot, 生产者使用spring-boot-devtools的话,消费者进行调用时发生异常
具体报错信息
Service is not visible from class loader
cause: java.lang.ClassCastException: com.puhuijia.facade.payment.service.impl.PaymentServiceImpl$$EnhancerBySpringCGLIB$$671092bb cannot be cast to com.puhuijia.facade.payment.service.IPaymentService
原因:spring-boot-devtools存在两个classloader
采用spring-boot-devtools会存在两个classloader,一个是应用类加载器AppClassLoader,一个RestartClassLoader,AppClassLoader用于加载第三方jar,而RestartClassLoader用于加载用户目录下的class。
使用rest协议时,会为每个service创建代理类,com.alibaba.dubbo.common.bytecode.Proxy创建代理类时,是用proxy所在classloader为AppClassLoader,去加载用户目录下的class,自然就会报class不可见。
对策
一是弃用 devtools,改用 springloaded 。具体配置方法参看: http://www.cnblogs.com/magicalSam/p/7196355.html
二是配置 devtools 的properties ,标记出 dubbo 服务的类,不进行热部署
参看:https://github.com/dangdangdotcom/dubbox/issues/218
在项目里 resources\META-INF\spring-devtools.properties 里添加
restart.include.dubbo=/dubbo-[\\d\\.]+\\.jar
另一个详细的说明:
两个maven模块,a模块依赖b模块,在a模块中,使用main方式启动,爆 xx is not visible from class loader,
解决办法在a模块中新建META-INF/spring-devtools.properties,文件中定义restart.exclude.dependency=/b/target/classes/,这样restart class loader不再加载b中的类,这个类,只有appclassloader加载,就不会出现异常了。