本文共 14525 字,大约阅读时间需要 48 分钟。
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文件
,这是目前我们看到的结果引导加载器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
好了到这我们就可以总结一下了三个类加载器分别加载那些东西了:
ClASSPATH
的变量知道了三个类加载器的加载路径,好像我们就在IDE中Run一下结果就出来了,但是结果是怎么跑出来的呢?也就是类如何加载使用的呢?Class.forName()
等方法动态的加载一个类进来,供我们使用(自己的理解,如果错误请指正)加载
,验证
,准备
,解析
,初始化
,使用
,卸载
七个阶段,其中的 验证
,准备
,解析
三个阶段可以统称为连接
,如下图加载
,验证
,准备
,初始化
,卸载
这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析
阶段不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的运行时绑定(自己理解:即多态情况下的应用.如果错误请指正),说这个注意的目的就是为了说明:这些阶段通常是互相交叉运行的,即一个阶段可能会触发调用另一个阶段,导致另一个阶段的开始类的主动初始化的情况有且仅有五种情况:
new
,getstatic
,putstatic
,invokestatic
四条字节码指令的时候,如果类没有进行过初始化,那么就会触发类的初始化,这四条字节码指令对应的最常见的代码场景就是:new对象的时候
,读取或设置一个static静态字段的时候
,调用类方法的时候
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 init123
第二个例子,复用前面的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); }}
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点中的第三点:一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化加载
,验证
,准备
,解析
,初始化
,使用
,卸载
七个阶段,其中的 验证
,准备
,解析
三个阶段可以统称为连接
<clinit>()
类构造器,用于初始化接口中所定义的成员变量,并且一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化主要完成三件事
之前说到的数组初始化,在这也需要注意一下,因为还是会有一些不一样
Integer[][][]
,去掉一个维度后是他的组件类型Integer[][]
,发现是引用类型,然后再去掉一个维度Integer[]
,然后依次就会创建很多的[Ljava.lang.Integer
,并且他们是递归创建的,所以这些[Ljava.lang.Integer
是包含关系,所以就能说是不存在多维数组的,这个递归的理解是我自己的理解,不对请指正,谢谢文件格式验证:是否符合虚拟机要求的Class格式
元数据验证:对字节码描述的信息进行语义分析,确保其描述的信息符合java语言规范
字节码验证:主要目的是通过数据流和控制流分析,确保程序语义是合法的,符合逻辑的
符号引用验证:发送在虚拟机将符号引用转换为直接引用的时候,这个转化动作将在连接的解析阶段中发生.
-Xverify:none
参数来关闭大部分的类验证措施static int value = 123
经过准备阶段过后的初始值为0而不是123,因为这时候尚未开始 执行任何java方法,而把value赋值为123的putstatic是程序被编译后,存放于类构造器方法之中,所以把value赋值为123的动作将在初始化阶段才会执行是虚拟机将常量池内的符号引用替换为直接引用的过程
<clinit>()
方法的过程<clinit>()
方法是由编译器自动收集类中的所有类变量的复制动作和静态初始化块中的语句合并而成的,注意是类static的,收集的顺序就是语句在源文件中的顺序决定的,如果定义错误顺序,会造成非法向前引用变量错误<clinit>()
方法与构造函数不同,<clinit>()
不需要显示的调用父类构造器,虚拟机会保证子类的<clinit>()
执行之前,父类的<clinit>()
方法已经执行完毕<clinit>()
方法对于类和接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么就不会为这个类生成<clinit>()
方法<clinit>()
方法,但是与之前提到的一致,接口中的<clinit>()
并不会保证父类的<clinit>()
执行完毕才会执行子类,而是用到父类的变量的才会去执行<clinit>()
是一个类中的方法,在多线程下,创建这个类,如果<clinit>()
方法耗时很长,就会造成线程阻塞,所以这是创建对象的时候一个隐晦的坑通过类的全限定名来获取描述此类的二进制字节流
这个动作放在虚拟机外部,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器
加载它的类加载器+全限定名
来区分两个类是否相等的,即不是同一个类加载器加载的class对象,根部没有可比性,肯定不一样知道了前面的知识,我们可以尝试着定义一个简单的类加载器
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
对象MethodHandles.Lookup
的工厂方法来创建MethodHandles:这个类只包含操作或返回方法句柄的静态方法,它们分为以下几类:
实例使用
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
*
号,其余的只是留作记录,方便以后系统查询转载地址:http://lznax.baihongyu.com/