目录
一、class文件结构
在了解jvm将class文件加载到虚拟机的过程之前,先大致了解一下class文件结构:
1)magic number:java文件目前在16进制class文件中的标识为cafe babe
2)minor version:子版本号
3)major version:主版本号,1.7为51,1.8为52
4)constant_pool_count:常量池中的内容数量
5)constant_pool:常量池,里面存放当前class使用到的所有常量信息
6)access_flag:对类的相关描述符,包含是否为public,是否为final,是否为借口等
7)this_class:指向常量池的指针,对该类的描述
8)super_class:指向常量池的指针,对父类的描述
9)interfaces_counts:接口数量
10)interfaces:接口信息,各个接口内容为指向常量池的指针
11)fields_counts:字段数量
12)fields:字段信息,每个字段信息包含:字段名称(指向常量池的指针),字段类型(指向常量池的指针),字段描述符
13)methods_counts:方法数量
14)methods:方法信息,每个方法信息包含:方法名称(指向常量池的指针),方法类型(指向常量池的指针),方法描述符
15)attributes_counts
16)attributes
介绍两款idea插件:
1)BinEd:可以查看class文件的16进制内容
2)jClassLib:可以查看class文件里面的内容
二、jvm的加载、链接、初始化
1.加载(loading)
jvm加载类使用的方式是双亲委派的方式,由父到子分别是:bootstrap->ext->app->customer。需要注意的是这里面的父加载器不等同于java中的父类,例如:app的父加载器是ext,但是AppClassLoader和ExtClassLoader加载器对java中的父类加载器是URLClassLoader,这两个类都是Launcher的内部类。
双亲委派的流程见下图:
大体的一个流程为:先从下往上进行判断类是否已经加载,如果加载就直接结束;如果都没有加载,到达最上层以后,再从上往下判断加载器是否可以加载,如果可以,则进行加载并结束,如果不能加载,则一直往下进行该流程。采用双亲委派机制的好处是安全,防止加载一些同名类,影响系统安全,也能保证每个类只加载一次。
每个类加载器都有属于自己的加载范围:
1)bootstrap类加载器的加载范围是(System.getProperty(“sun.boot.class.path”)):
/jdk1.8.0_192.jdk/Contents/Home/jre/lib/resources.jar
/jdk1.8.0_192.jdk/Contents/Home/jre/lib/rt.jar
/jdk1.8.0_192.jdk/Contents/Home/jre/lib/sunrsasign.jar
/jdk1.8.0_192.jdk/Contents/Home/jre/lib/jsse.jar
/jdk1.8.0_192.jdk/Contents/Home/jre/lib/jce.jar
/jdk1.8.0_192.jdk/Contents/Home/jre/lib/charsets.jar
/jdk1.8.0_192.jdk/Contents/Home/jre/lib/jfr.jar
/jdk1.8.0_192.jdk/Contents/Home/jre/classes
2)ext类加载器的加载范围(System.getProperty(“java.ext.dirs”)):
/Users/yangjianing/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java
3)app类加载器的加载范围(System.getProperty(“java.class.path”)):
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/jre/lib/deploy.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/jre/lib/ext/dnsns.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/jre/lib/ext/jaccess.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
…
4)customer类加载器由自己定义
类加载器的加载范围可以再Launcher类的源码中找到,可以自己进行打印查看。
2.链接(linking)
链接分为三步:
1.验证(verification):验证class文件格式是否正确,例如:java的class文件是否以cafe baby开头。
2.准备(preparation):将类的静态变量赋初始值,需要注意的是,这一步是将静态变量赋初始值,例如:类中的一个变量public static int = 12;在这一步完成的时候,int的值是0。
3.解析(resolution):将符号引用改为直接引用,在上面的class文件说明中,很多信息都是指向常量池,这一步就是将常量池中的符号引用解析为能够直接访问的指针,偏移量等。
3.初始化(initializing)
给静态变量赋初始值,这一步才会将上面的int的值赋为12。
三、自定义类加载器
自定义类加载器位于双亲委派中的最下层位置,也是可以由开发者自行处理的类加载器,可以用来加载项目以外需要特殊加载的类,自定义加载器的实现方法也比较简单啊,查看以下代码:
import java.io.*;
public class SelfClassLoader extends ClassLoader{
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 自定义加载器加载的路径
File file = new File("/Users/yjn/Desktop/"+name+".class");
try(ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
// 指定文件的输入流
FileInputStream stream = new FileInputStream(file);
// 指定文件的输出流
int b = 0;
while ((b = stream.read()) != 0){
byteArrayOutputStream.write(b);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
stream.close();
return defineClass(name, bytes, 0, bytes.length-1);
}catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return super.loadClass(name);
}
}
继承ClassLoader类,然后重写其中的loadClass方法即可。
自定义加载器也可以打破双亲委派机制,先看一下ClassLoader源码中的实现双亲委派机制的代码,如下:
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) {
c = parent.loadClass(name, false);
} else {
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();
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(c);
}
return c;
}
}
如果想要打破双亲委派机制,那么就只需要改变上面的查找和加载机制即可。
四、类的加载和执行
1)jvm中类的加载时采用lazyLoading的方式,只有当该类要被使用的时候才会去被加载,有以下五种情况会触发加载:
1.new getstatic putstatic invokestatic指令,访问final变量除外
2.java.lang.reflect对类进行反射调用时
3.初始化子类的时候,父类首先初始化
4.初始化子类的时候,父类首先初始化
5.动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
2)java属于一种解释执行的语言,但是这不是绝对的,jit编译器可以实现java的编译执行,目前java一般是解释+编译混合执行。可以通过设置参数来指定java时如何执行,有以下三个参数:
-Xint:解释执行模式,启动速度快,执行效率较低
-Xcomp:编译模式,启动速度慢,执行效率较高
-Xmixed:混合执行,开始阶段使用解释执行,启动较快,在运行阶段对热点代码进行编译,可以加快执行效率