思维导图
一、JVM介绍
在介绍JVM之前,先看一下.java文件从编码到执行的过程:
整个过程是,x.java文件需要编译成x.class文件,通过类加载器加载到内存中,然后通过解释器或者即时编译器进行解释和编译,最后交给执行引擎执行,执行引擎操作OS硬件。
从类加载器到执行引擎这块内容就是JVM。
JVM是一个跨语言的平台。从上面的图中可以看到,实际上JVM上运行的不是.java文件,而是.class文件。这就引出一个观点,JVM是一个跨语言的平台,他不仅仅能跑java程序,只要这种编程语言能编译成JVM可识别的.class文件都可以在上面运行。
所以除了java以外,能在JVM上运行的语言有很多,比如JRuby、Groovy、Scala、Kotlin等等。
从本质上讲JVM就是一台通过软件虚拟的计算机,它有它自身的指令集,有它自身的操作系统。
所以Oracle给JVM定了一套JVM规范,Oracle公司也给出了他的实现。基本上是目前最多人使用的java虚拟机实现,叫做Hotspot。使用java -version可以查看:
一些体量较大,有一定规模的公司,也会开发自己的JVM虚拟机,比如淘宝的TaobaoVM、IBM公司的J9-IBM、微软的MicrosoftVM等等。
二、JDK、JRE、JVM
JVM应该很清楚了,是运行.class文件的虚拟机。JRE则是运行时环境,包括JVM和java核心类库,没有核心的类库是跑不起来的。
JDK则包括JRE和一些开发使用的工具集。
所以总的关系是JDK > JRE > JVM。
三、Class加载过程
类加载是JVM工作的一个很重要的过程,我们知道.class是存在在硬盘上的一个文件,如何加载到内存工作的呢,面试中也经常问这个问题。所以你要和其他程序员拉开差距,体现差异化,这个问题要搞懂。
类加载的过程实际上分为三大步:Loading(加载)、Linking(连接)、Initlalizing(初始化)。
其中第二步Linking又分为三小步:Verification(验证)、Preparation(准备)、Resolution(解析)。
3.1 Loading
Loading是把.class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据,在堆中生成一个java.lang.Class类对象代表这个类,作为方法区这些类型数据的访问入口。
3.2 Linking
Linking简单来说,就是把原始的类定义的信息合并到JVM运行状态之中。分为三小步进行。
3.2.1 Verification
验证加载的类信息是否符合class文件的标准,防止恶意信息或者不符合规范的字节信息。是JVM虚拟机运行安全的重要保障。
3.2.2 Preparation
创建类或者接口中的静态变量,并初始化静态变量赋默认值。赋默认值不是赋初始值,比如static int i = 5,这一步只是把i赋值为0,而不是赋值为5。赋值为5是在后面的步骤。
3.2.3 Resolution
把class文件常量池里面用到的符号引用转换成直接内存地址,直接可以访问到的内容。
3.3 Initlalizing
这一步真正去执行类初始化clinit()(类构造器)的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态代码块内(static{})的逻辑。当初始化一个类时,发现父类还没有进行过初始化,则先初始化父类。虚拟机会保证一个类的clinit()方法在多线程环境中被正确加锁和同步。
四、类加载器
上面就是类加载的整个过程。而最后一步Initlalizing是通过类加载器加载类。类加载器这里我单独讲一下,因为这是一个重点。
Java中的类加载器由上到下分为:
- Bootstrap ClassLoader(启动类加载器)
- ExtClassLoader(扩展类加载器)
- AppClassLoader(应用程序类加载器)
从类图,可以看到ExtClassLoader和AppClassLoader都是ClassLoader的子类。
所以如果要自定义一个类加载器,可以继承ClassLoader抽象类,重写里面的方法。重写什么方法后面再讲。
五、双亲委派机制
讲完类加载器,这些类加载器是怎么工作的呢。对于双亲委派机制可能多多少少有听过,没听过也没关系,我正要讲。
上面说过有Bootstrap,ExtClassLoader,AppClassLoader三个类加载器。工作机制如下:
加载类的逻辑是怎么样的呢,核心代码是可以在JDK源码中找到的,在抽象类ClassLoader类的loadClass(),有兴趣可以源码看看:
代码语言:javascript复制protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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
}
//如果上层的都找不到相应的class
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;
}
}
其实整个逻辑已经很清晰了,为了更好理解,我这里画张图给给大家,更好理解一点:
看到这里,应该都清楚了双亲委派机制的流程了。重点来了,为什么要使用双亲委派机制呢?
如果面试官问这个问题,一定要答出关键字:安全性。
反证法来辩证。假设不采用双亲委派机制,那我可以自定义一个类加载器,然后我写一个java.lang.String类用自定义的类加载器加载进去,原来java本身又有一个java.lang.String类,那么类的唯一性就没法保证,就不就给虚拟机的安全带来的隐患了吗。所以要保证一个类只能由同一个类加载器加载,才能保证系统类的的安全。
六、自定义类加载器
自定义类加载器,上面讲过可以有样学样,自定义一个类继承ClassLoader抽象类。重写哪个方法呢?loadClass()方法是加载类的方法,重写这个不就行了?
如果重写loadClass()那证明有思考过,但是不太对,因为重写loadClass()会破坏了双亲委派机制的逻辑。应该重写loadClass()方法里的findClass()方法。
findClass()方法才是自定义类加载器加载类的方法。
那findClass()方法源码是怎么样的呢?
明显这个方法是给子类重写用的,权限修饰符也是protected,如果不重写,那就会抛出找不到类的异常。如果学过设计模式的同学,应该看得出来这里用了模板模式的设计模式。所以我们自定义类加载器重写此方法即可。开始动手!
创建CustomerClassLoader类,继承ClassLoader抽象类的findClass()方法。
代码语言:javascript复制public class CustomerClassLoader extends ClassLoader {
//class文件在磁盘中的路径
private String path;
//通过构造器初始化class文件的路径
public CustomerClassLoader(String path) {
this.path = path;
}
/**
* 加载类
*
* @param name 类的全路径
* @return Class<?>
* @author Ye hongzhi
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
//获取class文件,转成字节码数组
byte[] data = getData();
if (data != null) {
//将class的字节码数组转换成Class类的实例
clazz = defineClass(name, data, 0, data.length);
}
//返回Class对象
return clazz;
}
private byte[] getData() {
File file = new File(path);
if (file.exists()) {
try (FileInputStream in = new FileInputStream(file);
ByteArrayOutputStream out = new ByteArrayOutputStream();) {
byte[] buffer = new byte[1024];
int size;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
return out.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
} else {
return null;
}
}
}
这样就完成了,接下来测试一下,定义一个Hello类。
代码语言:javascript复制public class Hello {
public void say() {
System.out.println("hello.......java");
}
}
使用javac命令编译成class文件,如下图:
最后写个main方法运行测试一把:
代码语言:javascript复制public class Main {
public static void main(String[] args) throws Exception {
String path = "D:\mall\core\src\main\java\io\github\yehongzhi\classloader\Hello.class";
CustomerClassLoader classLoader = new CustomerClassLoader(path);
Class<?> clazz = classLoader.findClass("io.github.yehongzhi.classloader.Hello");
System.out.println("使用类加载器:" clazz.getClassLoader());
Method method = clazz.getDeclaredMethod("say");
Object obj = clazz.newInstance();
method.invoke(obj);
}
}
运行结果:
七、破坏双亲委派机制
看到这里,你肯定会很疑惑。上面不是才讲过双亲委派机制为了保证系统的安全性吗,为什么又要破坏双亲委派机制呢?
重温一下双亲委派机制,应该还记得,就是底层的类加载器一直委托上层的类加载器,如果上层的已经加载了,就无需加载,上层的类加载器没有加载则自己加载。这就突出了双亲委派机制的一个缺陷,就是只能子的类加载器委托父的类加载器,不能反过来用父的类加载器委托子的类加载器。
那你会问,什么情况会出现父的类加载器委托子的类加载器呢?
还真有这个场景,就是加载JDBC的数据库驱动。在JDK中有一个所有 JDBC 驱动程序需要实现的接口Java.sql.Driver。而Driver接口的实现类则是由各大数据库厂商提供。那问题就出现了,DriverManager(JDK的rt.jar包中)要加载各个实现了Driver接口的实现类,然后进行统一管理,但是DriverManager是由Bootstrap类加载器加载的,只能加载JAVA_HOME下lib目录下的文件(可以看回上面双亲委派机制的第一张图),但是实现类是服务商提供的,由AppClassLoader加载,这就需要Bootstrap(上层类加载器)委托AppClassLoader(下层类加载器),也就破坏了双亲委派机制。这只是其中一种场景,破坏双亲委派机制的例子还有很多。
那么怎么实现破坏双亲委派机制呢?
- 最简单就是自定义类加载器,前面讲过为了不破坏双亲委派机制重写findClass()方法,所以如果我要破坏双亲委派机制,那就重写loadClass()方法,直接把双亲委派机制的逻辑给改了。在JDK1.2后不提倡重写此方法。所以提供下面这种方式。
- 使用线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是AppClassLoader类加载器。
那么刚刚说的JDBC又是采用什么方式破坏双亲委派机制的呢?
当然是采用上下文文件类加载器,还有使用了SPI机制,下面一步一步分解。
第一步,Bootstrap加载DriverManager类,在DriverManager类的静态代码块调用初始化方法。
代码语言:javascript复制public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
第二步,加载Driver接口的所有实现类,得到Driver实现类的集合,获取一个迭代器。
第三步,看ServiceLoader.load()方法。
第四步,看迭代器driversIterator。
接着一直找下去,就会看到一个很神奇的地方。
而这个常量值PREFIX则是:
代码语言:javascript复制private static final String PREFIX = "META-INF/services/";
所以我们可以在mysql驱动包中找到这个文件:
通过文件名找接口的实现类,这是java的SPI机制。到此为止,破案了大人!
作为暖男的我,就画张图,总结一下整个过程吧:
总结
这篇文章主要介绍了JVM,然后讲到JVM的类加载机制的三大步骤,接着讲自定义类加载器以及双亲委派机制。最后再深入探讨了为什么要使用双亲委派机制,又为什么要破坏双亲委派机制的问题。可能讲得有点长,不过我相信应该都看懂了,因为我讲得比较通俗,而且图文并茂。
上面所有例子的代码都上传Github了:
https://github.com/yehongzhi/mall