- 本文主要参考<<深入理解Java虚拟机>>一书,并加入了自己的理解,如果不对请指正,谢谢
ClASSPATH
- 之前刚入门Java语言的时候还是在windows上安装环境,当时是使用的Java8,而大学的老师还是让配置
CLASSPATH
环境变量,但是在之后的学习中,发现这个麻烦的配置JVM已经帮助我们给省略掉了,只需要JAVA_HOME
和PATH
变量就可以在cmd中java -version
了 -
说起之前的
ClASSPATH
,那么这个环境变量究竟是有什么用的呢?指定自定义的类文件或者包的加载路径.
,记住这个是我们自定义的,同样我们也可以通过java.class.path
变量去指定加载路径,首先我们可以通过程序来看一下默认给我配置好的加载路径是在哪里public static void main(String[] args) { System.out.println(System.getProperty("java.class.path")); }
结果 D:\jdk1.8.0_192\jre\lib\charsets.jar; D:\jdk1.8.0_192\jre\lib\deploy.jar; D:\jdk1.8.0_192\jre\lib\ext\access-bridge-64.jar; D:\jdk1.8.0_192\jre\lib\ext\cldrdata.jar; D:\jdk1.8.0_192\jre\lib\ext\dnsns.jar; D:\jdk1.8.0_192\jre\lib\ext\jaccess.jar; D:\jdk1.8.0_192\jre\lib\ext\jfxrt.jar; D:\jdk1.8.0_192\jre\lib\ext\localedata.jar; D:\jdk1.8.0_192\jre\lib\ext\nashorn.jar; D:\jdk1.8.0_192\jre\lib\ext\sunec.jar; D:\jdk1.8.0_192\jre\lib\ext\sunjce_provider.jar; D:\jdk1.8.0_192\jre\lib\ext\sunmscapi.jar; D:\jdk1.8.0_192\jre\lib\ext\sunpkcs11.jar; D:\jdk1.8.0_192\jre\lib\ext\zipfs.jar; D:\jdk1.8.0_192\jre\lib\javaws.jar; D:\jdk1.8.0_192\jre\lib\jce.jar; D:\jdk1.8.0_192\jre\lib\jfr.jar; D:\jdk1.8.0_192\jre\lib\jfxswt.jar; D:\jdk1.8.0_192\jre\lib\jsse.jar; D:\jdk1.8.0_192\jre\lib\management-agent.jar; D:\jdk1.8.0_192\jre\lib\plugin.jar; D:\jdk1.8.0_192\jre\lib\resources.jar; D:\jdk1.8.0_192\jre\lib\rt.jar; G:\IdeaProjects\untitled\out\production\untitled; C:\Users\qidai\.m2\repository\junit\junit\4.12\junit-4.12.jar; C:\Users\qidai\.m2\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar; D:\IntelliJ IDEA 2018.2.6\lib\idea_rt.jar
- 如上结果,即
CLASSPATH
主要是去加载jre\lib\ext\...
和G:\项目目录
以及maven中涉及的jar文件
,这是目前我们看到的结果 - 如果你还知道引导加载器和扩展类加载器的话,我们可以接着往下看看,如果您不知道也没关系,有个印象就好了,下面也是去用程序证明一下这两类的加载器分别去加载什么jar包或者class文件的
-
引导加载器
Bootstrap ClassLoader
:对应到相应的变量就是sun.boot.class.path
,好我们来看一下他加载了什么类public static void main(String[] args) { System.out.println(System.getProperty("sun.boot.class.path")); }
D:\jdk1.8.0_192\jre\lib\resources.jar; D:\jdk1.8.0_192\jre\lib\rt.jar; D:\jdk1.8.0_192\jre\lib\sunrsasign.jar; D:\jdk1.8.0_192\jre\lib\jsse.jar; D:\jdk1.8.0_192\jre\lib\jce.jar; D:\jdk1.8.0_192\jre\lib\charsets.jar; D:\jdk1.8.0_192\jre\lib\jfr.jar; D:\jdk1.8.0_192\jre\classes
- 结果比刚才的结果少很多了,主要是去加载一些
jre\lib
下的jar包和类,但是我们不难发现,这个的输出跟前面ClASSPATH
的输出好像有重叠啊,一个类不是不可以被加载两次的吗?所以这也证实了,他们存在继承关系,并不是extends
的继承关系,而是一种先后关系 -
扩展类加载器
Extention ClassLoader
:对应到的变量就是java.ext.dirs
,那么我们来看一下他的输出public static void main(String[] args) { System.out.println(System.getProperty("java.ext.dirs")); }
D:\jdk1.8.0_192\jre\lib\ext; C:\Windows\Sun\Java\lib\ext
-
好了到这我们就可以总结一下了三个类加载器分别加载那些东西了:
- Bootstrap ClassLoader : 主要加载在jre/lib下的包和类
- Extention ClassLoder: 主要加载jre/lib/ext下的包和类
- App ClassLoder即上面第一个类加载器: 主要加载我们自己使用的环境当中的一些包和类,比如maven的包,自己定义的class或者interface等
- 如果理解错误请指正谢谢
类加载机制
- 我们之前从
ClASSPATH
的变量知道了三个类加载器的加载路径,好像我们就在IDE中Run一下结果就出来了,但是结果是怎么跑出来的呢?也就是类如何加载使用的呢? - 首先我们要清楚一个概念,那就是什么是类加载,然后我们才能去了解类是如何进行加载的
- 类加载就是JVM将class文件加载到内存,并对数据进行校验转换解析等操作,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制
类的加载时机
- 我们还要知道一个知识点,在Java中,类型的加载,连接和初始化过程都是程序运行期间完成的,这种方法可能会带来性能上一定的下降,但是这可以提高Java的灵活度,对于编译时直接进行连接工作的程序来说,Java可以做到运行时加载类,加载类的方式可以是多种多样的,比如网络加载之类的,Java对加载方式还是很开放的,一种语言直接在编译期将程序代码编译完成,那么在进行更改的时候就不能在运行时进行修改了,而Java可以通过
Class.forName()
等方法动态的加载一个类进来,供我们使用(自己的理解,如果错误请指正) - 好了知道了上面的一个额外的知识点,如果我们要解决留在类加载机制那的一个问题,我们就先要了解一下类的加载时机
- 类从被加载到虚拟机内存中开始,到卸载出内存为止,生命周期包括:
加载
,验证
,准备
,解析
,初始化
,使用
,卸载
七个阶段,其中的验证
,准备
,解析
三个阶段可以统称为连接
,如下图
- 注意的是
加载
,验证
,准备
,初始化
,卸载
这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析
阶段不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的运行时绑定(自己理解:即多态情况下的应用.如果错误请指正),说这个注意的目的就是为了说明:这些阶段通常是互相交叉运行的,即一个阶段可能会触发调用另一个阶段,导致另一个阶段的开始 -
类的主动初始化的情况有且仅有五种情况:
- 遇到
new
,getstatic
,putstatic
,invokestatic
四条字节码指令的时候,如果类没有进行过初始化,那么就会触发类的初始化,这四条字节码指令对应的最常见的代码场景就是:new对象的时候
,读取或设置一个static静态字段的时候
,调用类方法的时候
- 使用反射的时候,如果类没有进行过初始化,则需要先触发初始化
- 当初始化一个类的时候其父类还没有初始化,那么优先初始化父类
- 当虚拟机启动的时候,优先初始化指定的运行类(main)
- 当使用动态语言支持时,如果一个
MethodHandle
实例最后的解析结果为REF_getStatic
,REF_putStatic
,REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,那么就先触发其初始化(如果你不清楚MethodHandle
,请看文章尾部)
- 遇到
-
上面是主动初始化,只有这五种情况,除此之外,所有引用类的方法都不会触发初始化,被称为被动引用,如下例子
public class SuperClass { static{ System.out.println("SuperClass static init"); } public static int value = 123; } class SubClass extends SuperClass{ static{ System.out.println("SubClass static init"); } } class A{ public static void main(String[] args) { System.out.println(SubClass.value); } } /** * SuperClass static init * 123 */
-
对于上面的结果,本期待的是
SubClass
一样会得到初始化,但是并没有,因为value是存在与SuperClass
的,并且是static
修饰,也就是说这个value变量是属于SuperClass
类的,所以并不会导致子类的初始化,如果我们想看到虚拟机的类加载过程,可以加上参数-XX:+TraceClassLoading
,我们再次运行会看到这样的输出[Loaded SuperClass from file:/G:/IdeaProjects/untitled/out/production/untitled/] [Loaded SubClass from file:/G:/IdeaProjects/untitled/out/production/untitled/] SuperClass static init 123
- 我们看到两个类都加载了,但是并不是都初始化了,如果全部初始化了,那子类应该也会有static输出的
-
第二个例子,复用前面的
SuperClass
与SubClass
,但是我们修改main方法SubClass[] subClasses = new SubClass[1];
- 我们会发现并没有任何输出,所以就没有触发类
SuperClass
的初始化,但是这段代码会触发一个由虚拟机创建的[L....SuperClass
类的初始化阶段,是由指令newarray
触发生成的,但是虚拟机依旧会加载上这两个类,只是不初始化 - 对于
[L....SuperClass
这样的格式,我们知道main的参数为String[]
,所以在javap查看的时候,也会发现这样的格式:[Ljava/lang/String;
-
对于宏变量的注意
public class SuperClass { static{ System.out.println("SuperClass static init"); } public static final int value = 123; } class A{ public static void main(String[] args) { System.out.println(SuperClass.value); } }
- 上面依旧不会输出static初始化信息,因为value变量已经在编译的时候确定了下来,所以虚拟机就进行了优化,将value的值存在的地方都替换为了常量,并且将此常量放入引用value的类的常量池.即A与SuperClass已经没有关系了,A输出只是引用自己常量池中的value常量
-
javap -v A
,可以看到... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: bipush 123 //将一个byte型常量值推送至栈顶 5: invokevirtual #4 // Method java/io/PrintStream.println:(I)V 8: return ...
- 对于类加载过程和接口加载过程还是有一些差别的:接口也有初始化过程,上面的代码使用静态初始化块来初始化,但是在接口中是不可以使用静态初始化块的,但是仍然会为接口生成一个
<clinit>()
类构造器,用于初始化接口中所定义的成员变量,还有一点就就是区别之前的有且仅有5点中的第三点:一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化 - 在Java中类型的加载,连接和初始化过程都是程序运行期间完成的
- 生命周期包括:
加载
,验证
,准备
,解析
,初始化
,使用
,卸载
七个阶段,其中的验证
,准备
,解析
三个阶段可以统称为连接
- 这些阶段通常是互相交叉运行的,即一个阶段可能会触发调用另一个阶段,导致另一个阶段的开始
- 类加载过程和接口加载过程的一些差别:会为接口生成一个
<clinit>()
类构造器,用于初始化接口中所定义的成员变量,并且一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化 - 主动引用和被动引用
类加载过程
加载
-
主要完成三件事
- 通过全限定名获取类的二进制数据流
- 将流中所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
- 获取数据流可以通过网络,zip包或者运行时生成等多种方式加载都可以
-
之前说到的数组初始化,在这也需要注意一下,因为还是会有一些不一样
- 当我们的类不是数组的时候,在加载这个类的时候,我们可以控制这个类的加载阶段,因为我们可以自定义一个类加载器进行加载这个非数组类,只需要重写loadClass即可
- 当是数组类的时候,情况就会不一样,因为数组类本身不通过类加载器创建,是由虚拟机直接创建的,但是追根溯源他还是基于一个类的,所以最终还是要靠类加载器创建的,我们需要知道一点是不存在多维数组,只是一维数组中又引用了多个数组罢了,所以以下的解释也就能证明我们知道的这一点
- 解释:一个数组去掉一个维度称为这个数组的组件类型,当组件类型是一个引用类型的时候(Integer[]),那么就递归去加载这个组件类型,数组将在加载该组件类型的类加载器的类名空间中做一个标识,因为要区分一个类,还需要加上加载他的类加载器
- 解释:当这个组件类型不是引用类型的时候(int[]),虚拟机就会把数组标记为与引导类加载器关联
- 解释:数组类的可见性与他的组件类型的可见性一直,如果组件类型不是引用类型,那数组类的可见性就为public
- 解释递归: 第一个解释中的递归创建,自己的的理解是,比如加载一个
Integer[][][]
,去掉一个维度后是他的组件类型Integer[][]
,发现是引用类型,然后再去掉一个维度Integer[]
,然后依次就会创建很多的[Ljava.lang.Integer
,并且他们是递归创建的,所以这些[Ljava.lang.Integer
是包含关系,所以就能说是不存在多维数组的,这个递归的理解是我自己的理解,不对请指正,谢谢
- 当加载阶段完成后,那些被读取的二进制流,就被被按照虚拟机需要的格式存储在方法区中,方法区中的数据存储格式由虚拟机自行实现,Class对象比较特殊,它会存在方法区中,被放入方法区的对象将作为程序访问方法区中的这些类型数据的外部接口
- 加载阶段和连接阶段是交叉运行的,加载没有完成的时候,连接或许已经开始了,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容
验证
- 为了保证加载的Class类的正确性,并且确保其不能危害虚拟机安全,所以需要进行各种验证操作
-
文件格式验证:是否符合虚拟机要求的Class格式
- 主次版本号是否在当前虚拟机处理范围之内
- 常量池的常量中是否有不被支持的常量类型
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- ...
- 主要为了保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个java类型信息的要求,只有通过这个阶段的验证,字节流才会进入内存的方法区中进行存储,所以后面的三个验证阶段都基于方法区的存储结构进行的,不会再直接操作字符流
-
元数据验证:对字节码描述的信息进行语义分析,确保其描述的信息符合java语言规范
- 是否有父类
- 是否继承了final类
- 是不是抽象类
- ..
- 主要目的是对类的元数据信息进行语义校验,保证不存在不合符的元数据信息
-
字节码验证:主要目的是通过数据流和控制流分析,确保程序语义是合法的,符合逻辑的
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换是有效的
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
- ..
- 这是最麻烦的一个步骤
-
符号引用验证:发送在虚拟机将符号引用转换为直接引用的时候,这个转化动作将在连接的解析阶段中发生.
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 符号引用中的类,字段,方法的访问性是否可被当前类访问
- ..
- 确保解析动作能正常执行,如果无法通过符号引用验证,那么将会排除类似NoSuchMethodError这样的异常
- 验证阶段不一定是不要的,可以通过
-Xverify:none
参数来关闭大部分的类验证措施
准备
- 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配,现在分配的知识static类变量,不包括实例变量,实例变量将会被分配到java堆中.
- 当
static int value = 123
经过准备阶段过后的初始值为0而不是123,因为这时候尚未开始 执行任何java方法,而把value赋值为123的putstatic是程序被编译后,存放于类构造器方法之中,所以把value赋值为123的动作将在初始化阶段才会执行 - 即准备阶段会赋值为默认值,但是需要注意final宏变量的替换
解析
-
是虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用:以一组符号来描述所引用额的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可
- 直接引用:可以是直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行
初始化
- 初始化之前的所有步骤,只有在加载类的时候,程序员可以重写加载逻辑,其余的操作都是由虚拟机主导和控制,到了初始化阶段,才开始真正的执行类中定义的Java程序代码
- 在准备阶段,我们了解到,虚拟机会为各个变量设置一个默认值,在初始化阶段后,虚拟机就会将我们指定的值赋值给变量,初始化解读那是执行类构造器
<clinit>()
方法的过程 -
<clinit>()
方法是由编译器自动收集类中的所有类变量的复制动作和静态初始化块中的语句合并而成的,注意是类static的,收集的顺序就是语句在源文件中的顺序决定的,如果定义错误顺序,会造成非法向前引用变量错误 -
<clinit>()
方法与构造函数不同,<clinit>()
不需要显示的调用父类构造器,虚拟机会保证子类的<clinit>()
执行之前,父类的<clinit>()
方法已经执行完毕 -
<clinit>()
方法对于类和接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么就不会为这个类生成<clinit>()
方法 - 接口中不能使用静态初始化块,但是依旧有赋值变量的操作,所以一样会生成
<clinit>()
方法,但是与之前提到的一致,接口中的<clinit>()
并不会保证父类的<clinit>()
执行完毕才会执行子类,而是用到父类的变量的才会去执行 -
<clinit>()
是一个类中的方法,在多线程下,创建这个类,如果<clinit>()
方法耗时很长,就会造成线程阻塞,所以这是创建对象的时候一个隐晦的坑
类加载器
- 把类加载阶段中的
通过类的全限定名来获取描述此类的二进制字节流
这个动作放在虚拟机外部,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器
类和类加载器
- 类在虚拟机中是有
加载它的类加载器+全限定名
来区分两个类是否相等的,即不是同一个类加载器加载的class对象,根部没有可比性,肯定不一样
双亲委派模型
- 到这就可以扩展的说一下文章开头的三个类加载器了,
- Bootstrap ClassLoder:除了他的加载路径之前有介绍,这个类加载器无法被java直接引用
- Extention ClassLoder:除了他的加载路径之前有介绍,这个类加载器是可以直接被java使用的
- Application ClassLoder:除了他的加载路径之前有介绍,这个类加载器是可以直接被java使用的,并且这是程序中默认使用的类加载器
- 自定义 ClassLoder:自己实现的类加载器
- 下面就是类加载器的关系图
- 如上除了Bootstrap,都会有一个父类加载器,但是这实现的父类并不是通过继承来实现的,而是通过组合
- 工作过程:即一个类先交由最底下的类加载器加载,但是最底下的类加载器并不立刻加载他会交由他的父类去尝试加载,而他的父类在交由他父类的父类去加载,直到Bootstrap加载器为止,当Bootstrap说自己加载不了,Extension加载器才会尝试加载,如果Extension也加载不了 ,就交由Application加载器尝试加载,一直到最后的User加载器,如果轮到Application加载器尝试加载此类了,发现自己能加载进来,那么就不会再调用子类加载器加载了,而是自己完成,即先找祖宗,祖宗干不了,在靠自己. 林正英打恶鬼唤师傅的流程??
自定义类加载器
-
知道了前面的知识,我们可以尝试着定义一个简单的类加载器
import java.io.FileInputStream; public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = getClassBytes(); //将字节数组转换为class的实例 Class<?> aClass = this.defineClass(name, bytes, 0, bytes.length); return aClass; } private byte[] getClassBytes() { byte[] bytes = null; try (FileInputStream fis = new FileInputStream("G:\\testc\\TargetClass.class");) { bytes = new byte[fis.available()]; fis.read(bytes); } catch (Exception e) { e.printStackTrace(); } return bytes; } } class ClassLoaderTest{ public static void main(String[] args) throws Exception { MyClassLoader loader = new MyClassLoader(); Class<?> c1 = Class.forName("TargetClass", true, loader); Object o = c1.newInstance(); System.out.println(o.getClass()); } }
-
.class和getClass()的区别:
-
.class
用于类名,getClass()
是一个final native
的方法,因此用于类实例 -
.class
在编译期间就确定了一个类的java.lang.Class
对象,但是getClass()
方法在运行期间确定一个类实例的java.lang.Class
对象
-
MethodHandle
- 可以看作为反射的升级版,但它不像反射API那样显得冗长繁重,当然是跟之前的反射操作的底层是不同的,但是这里先不说,只是说一下简单使用
-
MethodHandle: 方法句柄.对可直接执行的方法的类型化引用,能够安全调用方法的对象.MethodHandle 是抽象类,无法直接实例化,需通过
MethodHandles.Lookup
的工厂方法来创建 - MethodType:每个方法句柄都有一个MethodType实例,用来指明方法的返回类型和参数类型
-
MethodHandles:这个类只包含操作或返回方法句柄的静态方法,它们分为以下几类:
- 查找方法,帮助创建方法和字段的方法句柄
- 组合方法,将先前的方法合并或转换成新的方法
- 其他工厂方法来创建方法来模拟其他常见的JVM操作或控制流模式
- MethodHandles.Lookup:创建MethodHandle对象的工厂方法查找类
-
实例使用
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class TargetClass { public String say(int number){ System.out.println("say " + number); return "say return " + number; } public static void staticSay(){ System.out.println("xxx"); } public static void main(String[] args) throws Throwable{ // public String say(int number) //返回值类型,参数值类型 MethodType methodType = MethodType.methodType(String.class, int.class); //目标类,目标方法名,methodType MethodHandle say = MethodHandles.lookup().findVirtual(TargetClass.class, "say", methodType); Object o = say.invoke(new TargetClass(), 1); System.out.println(o); // public static void staticSay() MethodType staticMethodType = MethodType.methodType(void.class); MethodHandle staticSay = MethodHandles.lookup().findStatic(TargetClass.class, "staticSay", staticMethodType); staticSay.invoke(); } }
-
那么上面只是调用类方法啊,怎么解析出之前说的那些
REF_getStatic
,REF_putStatic
或者REF_invokeStatic
呢?public class TargetClass { public static void staticSay(){ System.out.println("xxx"); } public static void main(String[] args) throws Throwable{ MethodType staticMethodType = MethodType.methodType(void.class); MethodHandle staticSay = MethodHandles.lookup().findStatic(TargetClass.class, "staticSay", staticMethodType); MethodHandleInfo methodHandleInfo = MethodHandles.lookup().revealDirect(staticSay); System.out.println(methodHandleInfo); //invokeStatic TargetClass.staticSay:()void } }
-
看到结果的前面了吗? 就是对应的方法句柄,这些句柄的定义初始化在类
MethodHandleInfo
中完成public static final int REF_getField = Constants.REF_getField, REF_getStatic = Constants.REF_getStatic, REF_putField = Constants.REF_putField, REF_putStatic = Constants.REF_putStatic, REF_invokeVirtual = Constants.REF_invokeVirtual, REF_invokeStatic = Constants.REF_invokeStatic, REF_invokeSpecial = Constants.REF_invokeSpecial, REF_newInvokeSpecial = Constants.REF_newInvokeSpecial, REF_invokeInterface = Constants.REF_invokeInterface;
-
其中的
Constants
中的定义如下,这些赋值操作是发生在MethodHandleNatives
类中的static final byte REF_NONE = 0, // null value REF_getField = 1, REF_getStatic = 2, REF_putField = 3, REF_putStatic = 4, REF_invokeVirtual = 5, REF_invokeStatic = 6, REF_invokeSpecial = 7, REF_newInvokeSpecial = 8, REF_invokeInterface = 9, REF_LIMIT = 10;
-
在使用javap查看一个class文件的时候也会看到相关指令,比如
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; !!!!! 3: bipush 123 5: invokevirtual #4 // Method java/io/PrintStream.println:(I)V 8: return
引用资源
- 下面的资源,文章中有引用的,我将标注
*
号,其余的只是留作记录,方便以后系统查询 - 深入理解Java虚拟机*****
- 自定义加载器内容*
- 深入理解Java类加载器CSDN
- 深入理解Java类加载器cnblogs
- 包含类加载器与OSGi
- 最详细的Java的ClassLoader机制讲解