一文教你读懂JVM的类加载机制
Java运行程序又被称为WORA(WriteOnceRunAnywhere , 在仍何地方运行只需写入一次) , 意味着我们程序员小哥哥可以在任何一个系统上开发Java程序 , 但是却可以在所有系统上畅通运行 , 无需任何调整 , 大家都知道这是JVM的功劳 , 但具体是JVM的哪个模块或者什么机制实现这一功能呢?
JVM(JavaVirtualMachine,Java虚拟机)作为运行java程序的运行时引擎 , 也是JRE(JavaRuntimeEnvironment,Java运行时环境)的一部分 。
说起它想必不少小伙伴任处于似懂非懂的状态吧 , 说实话 , 着实是块难啃的骨头 。 但古语有云:千里之行 , 始于足下 。 我们今天主要谈谈 , 为什么JVM无需了解底层文件或者文件系统即可运行Java程序?
--这主要是类加载机制在运行时将Java类动态加载到JVM的缘故 。
加载连接(验证 , 准备 , 解析)初始化
根据类的全局限定名找到.class文件 , 生成对应的二进制字节流 。 将静态存储结构转换为运行时数据结构 , 保存运行时数据结构到JVM内存方法区中 。 JVM创建java.lang.Class类型的对象 , 保存于堆(Heap)中 。 利用该对象 , 可以获取保存于方法区中的类信息 , 例如:类名称 , 父类名称 , 方法和变量等信息 。ForExample:
packagecom.demo;importjava.lang.reflect.Field;importjava.lang.reflect.Method;publicclassClassLoaderExample{publicstaticvoidmain(String[]args){StringOpstringOp=newStringOp();System.out.println("ClassName:"+stringOp.getClass().getName());for(Methodmethod:stringOp.getClass().getMethods()){System.out.println("MethodName:"+method.getName());}for(Fieldfield:stringOp.getClass().getDeclaredFields()){System.out.println("FieldName:"+field.getName());}}}StringOp.class
packagecom.demo;publicclassStringOp{privateStringdisplayName;privateStringaddress;publicStringgetDisplayName(){returndisplayName;}publicStringgetAddress(){returnaddress;}}output:
ClassName:com.demo.StringOpMethodName:getAddressMethodName:getDisplayNameFieldName:displayNameFieldName:address注意:对于每个加载的.class文件 , 仅会创建一个java.lang.Class对象.
StringOpstringOp1=newStringOp();StringOpstringOp2=newStringOp();System.out.println(stringOp1.getClass()==stringOp2.getClass());//output:true2.连接2.1验证验证:主要是确保.class文件的正确性 , 由有效的编译器生成 , 不会对影响JVM的正常运行 。 通常包含如下四种验证:
文件格式:验证文件的格式是否符合规范 , 如果符合规范 , 则将对应的二进制字节流存储到JVM内存的方法区中;否则抛出java.lang.VerifyError异常 。 元数据:对字节码的描述信息进行语义分析 , 确保符合Java语言规范 。 例如:是否有父类;是否继承了不允许继承的类(final修饰的类);如果是实体类实现接口 , 是否实现了所有的方法;等 。。 字节码:验证程序语义是否合法 , 确保目标类的方法在被调用时不会影响JVM的正常运行 。 例如int类型的变量是否被当成String类型的变量等 。 符号引用:目标类涉及到其他类的的引用时 , 根据引用类的全局限定名(例如:importcom.demo.StringOp)能否找到对应的类;被引用类的字段和方法是否可被目标类访问(public,protected,package-private,private) 。 这里主要是确保后续目标类的解析步骤可以顺利完成 。 2.2准备准备:为目标类的静态字段分配内存并设置默认初始值(当字段被final修饰时 , 会直接赋值而不是默认值) 。 需要注意的是 , 非静态变量只有在实例化对象时才会进行字段的内存分配以及初始化 。
publicclassCustomClassLoader{//加载CustomClassLoader类时 , 便会为var1变量分配内存//准备阶段 , var1赋值256publicstaticfinalintvar1=256;//加载CustomClassLoader类时 , 便会为var2变量分配内存//准备阶段 , var2赋值0 , 初始化阶段赋值128publicstaticintvar2=128;//实例化一个CustomClassLoader对象时 , 便会为var1变量分配内存和赋值publicintvar3=64;}注意:静态变量存在方法区内存中 , 实例变量存在堆内存中 。
这里简单贴一下Java不同变量的默认值:
数据类型默认值int0float0.0flong0Ldouble0.0dshort(short)0char'u0000'byte(byte)0StringnullbooleanfalseArrayListnullHashMapnull
2.3解析解析:将符号引用转化为直接引用的过程 。
符号引用(SymbolicReference):描述所引用目标的一组符号 , 使用该符号可以唯一标识到目标即可 。 比如引用一个类:com.demo.CustomClassLoader , 这段字符串就是一个符号引用 , 并且引用的对象不一定事先加载到内存中 。 直接引用(DirectReference):直接指向目标的指针 , 相对偏移量或者一个能间接定位到目标的句柄 。 根据直接引用的定义 , 被引用的目标一定事先加载到了内存中 。 3.初始化前面的准备阶段时 , JVM为目标类的静态变量分配内存并设置默认初始值(final修饰的静态变量除外) , 但到了初始化阶段会根据用户编写的代码重新赋值 。 换句话说:初始化阶段就是JVM执行类构造器方法()的过程 。
()和()从名字上来看 , 非常的类似 , 或许某些童鞋会给双方画上等号 。 然则 , 对于JVM来说 , 虽然两者皆被称为构造器方法 , 但此构造器非彼构造器 。
():对象构造器方法 , 用于初始化实例对象实例对象的constructor(s)方法 , 和非静态变量的初始化;执行new创建实例对象时使用 。 ():类构造器方法 , 用于初始化类类的静态语句块和静态变量的初始化;类加载的初始化阶段执行 。ForExample:
publicclassClassLoaderExample{privatestaticfinalLoggerlogger=LoggerFactory.getLogger(ClassLoaderExample.class);//privateStringproperty="custom";////static{System.out.println("StaticInitializing...");}//ClassLoaderExample(){System.out.println("InstanceInitializing...");}//ClassLoaderExample(Stringproperty){this.property=property;System.out.println("InstanceInitializing...");}}查看对应的字节码:
publicClassLoaderExample();
Code:0aload_0//将局部变量表中第一个引用加载到操作树栈1invokespecial#1>//调用java.lang.Object的实例初始化方法4aload_0//将局部变量表中第一个引用加载到操作树栈5ldc#2//将常量custom从常量池第二个位置推送至栈顶7putfield#3//设置com.kaiwu.ClassLoaderExample实例对象的property字段值为custom10getstatic#4//从java.lang.System类中获取静态字段out13ldc#5//将常量InstanceInitializing...从常量池第5个位置推送至栈顶15invokevirtual#6//调用java.io.PrintStream对象的println实例方法 , 打印栈顶的InstanceInitializing...18return//返回publicClassLoaderExample(Stringproperty);
Code:0aload_0//将局部变量表中第一个引用加载到操作树栈1invokespecial#1>//调用java.lang.Object的实例初始化方法4aload_0//将局部变量表中第一个引用加载到操作树栈5ldc#2//将常量custom从常量池第二个位置推送至栈顶7putfield#3//将常量custom赋值给com.kaiwu.ClassLoaderExample实例对象的property字段10aload_0//将局部变量表中第一个引用加载到操作树栈11aload_1//将局部变量表中第二个引用加载到操作树栈12putfield#3//将入参property赋值给com.kaiwu.ClassLoaderExample实例对象的property字段15getstatic#4//从java.lang.System类中获取静态字段out18ldc#5//将常量InstanceInitializing...从常量池第5个位置推送至栈顶20invokevirtual#6//调用java.io.PrintStream对象的println实例方法,打印栈顶的InstanceInitializing...23return//返回():
Code:0ldc#7//将com.kaiwu.ClassLoaderEexample的class_info常量从常量池第七个位置推送至栈顶2invokestatic#8//从org.slf4j.LoggerFactory类中获取静态字段getLogger5putstatic#9//设置com.kaiwu.ClassLoaderExample类的静态字段logger8getstatic#4//从java.lang.System类中获取静态字段out11ldc#10//将常量StaticInitializing...从常量池第10个位置推送至栈顶13invokevirtual#6//调用java.io.PrintStream对象的println实例方法,打印栈顶的StaticInitializing...16return//返回II.类加载器1.类加载器ClassLoaderjava.lang.ClassLoader本身是一个抽象类 , 它的实例用来加载Java类到JVM内存中 。 这里如果细心的小伙伴就会发现 , java.lang.ClassLoader的实例用来加载Java类 , 但是它本身也是一个Java类 , 谁来加载它?先有鸡 , 还是先有蛋??
不急 , 待我们细细说来!!
首先 , 我们看一个简单的示例 , 看看都有哪些不同的类加载器:
publicstaticvoidprintClassLoader(){//StringOP:自定义类System.out.println("ClassLoaderofStringOp:"+StringOp.class.getClassLoader());//com.sun.javafx.binding.Logging:Java核心类扩展的类System.out.println("ClassLoaderofLogging:"+Logging.class.getClassLoader());//java.lang.String:Java核心类System.out.println("ClassLoaderofString:"+String.class.getClassLoader());}output:
ClassLoaderofStringOp:sun.misc.Launcher$AppClassLoader@18b4aac2ClassLoaderofLogging:sun.misc.Launcher$ExtClassLoader@7c3df479ClassLoaderofString:null从输出可以看出 , 这里有三种不同的类加载器:应用类加载器(Application/Systemclassloader),扩展类加载器(Extensionclassloader)以及启动类加载器(Bootstrapclassloader) 。
启动类加载器:本地代码(C++语言)实现的类加载器 , 负责加载JDK内部类(通常是$JAVA_HOME/jre/lib/rt.jar和$JAVA_HOME/jre/lib目录中的其他核心类库)或者-Xbootclasspath选项指定的jar包到内存中 。 该加载器是JVM核心的一部分 , 以本机代码编写 , 开发者无法获得启动类加载器的引用 , 所以上述java.lang.String类的加载为null 。 此外 , 该类充当所有其他java.lang.ClassLoader实例共同的父级(区别为是否为直接父级) , 它加载所有直接子级的java.lang.ClassLoader类(其他子类逐层由直接父级类加载器加载) 。 扩展类加载器:启动类加载器的子级 , 由Java语言实现的 , 用来加载JDK扩展目录下核心类的扩展类(通常是$JAVA_HOME/lib/ext/*.jar)或者-Djava.ext.dir系统属性中指定的任何其他目录中存在的类到内存中 。 由sun.misc.Launcher$ExtClassLoader类实现 , 开发者可以直接使用扩展类加载器 。 应用/系统类加载器:扩展类加载器的子级 , 负责将java-classpath/-cp($CLASSPATH)或者-Djava.class.path变量指定目录下类库加载到JVM内存中 。 由sun.misc.Launcher$AppClassLoader类实现 , 开发者可以直接使用系统类加载器 。 2.类加载器的类图关系
【一文教你读懂JVM的类加载机制】但几种加载器是如何配合的呢?亦或是单枪匹马 , 各领风骚?
鉴于此 , 则不得不提JVM采用的双亲委派机制了 。
3.双亲委派机制核心思想:自底向上检查类是否已加载 , 自顶向下尝试加载类 。
loadClass(Stringname):根据类的全局限定名称 , 由类加载器检索 , 加载 , 并返回java.lang.Class对象 。
findLoadedClass(Stringname):从当前的类加载器的缓存中检索是否已经加载目标类 。 findLoadedClass0(name)其实是底层的native方法(C编写) 。
源码分析(JDK1.8):java.net.URLClassLoader.class
请求加载自定义类com.kaiwu3.CustomClassLoader
请求加载扩展类com.sum.javafx.binding.Logging
推荐阅读
- 喝酒|长期喝酒者,早起后,若有这5个表现,你得考虑戒酒保肝了!
- 1碗面粉,不加水,锅里蒸一蒸,做香甜可口的发糕,比蛋糕还香
- 教你自制岩烧乳酪
- 春天湿气重,多喝这碗糖水,祛湿清热又甘甜,我隔两天喝一次
- 从小就馋此口,比肉香多了,几块钱做一大盘,咋吃都不腻
- 别再买坚果零食吃了,自己在家就能做,酥脆香甜,没有一点苦涩味!
- 教你做虎皮蛋糕上的虎皮,掌握2个技巧,保证起虎皮,做法很简单
- 番茄炒鸡蛋先炒番茄还是先炒鸡蛋?其实都不对,正确方法送给你
- 剩米饭别再炒了,试试这样做,比蛋炒饭好吃一百倍
- 一碗糯米,半个南瓜,香甜软糯,好吃不上火,比南瓜饼香,太好吃
