深入理解JVM原理-第一节 Java类加载机制

深入理解JVM原理-第一节 Java类加载机制

什么是JVM ?
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

一、类加载机制

1.1 类加载过程

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

虚拟机加载.class文件的方式:从本地系统中直接加载、通过网络下载.class文件、从zip,jar等归档文件中加载.class文件、从专有数据库中提取.class文件、将Java源文件动态编译为.class文件

1.2 类的生命周期

image

类加载到使用整个过程有如下几步:

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等
  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

二、虚拟机类加载器

2.1 类加载器

Java里有如下几种类加载器:

  • 启动类加载器(Bootstrap ClassLoader):负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载),启动类加载器是无法被Java程序直接引用的。
  • 扩展类加载器(Extension ClassLoader):负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
  • 自定义加载器:自定义加载路径,通过继承ClassLoader类实现,重写findClass方法。应用场景:加密、非标准的来源加载代码(数据库、网络等)。

除了以上列举的类加载器,还有一种比较特殊的类型 — 线程上下文类加载器。

类加载例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
/*
输出结果:
null //启动类加载器是C++语言实现,所以打印不出来
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader
*/
}
}
2.2 自定义加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自ClassLoader类,从上面对loadClass方法来分析来看,我们只需要重写 findClass 方法即可。

自定义加载例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.io.*;
import java.lang.reflect.Method;

public class MyClassLoader extends ClassLoader {
//类加载路径
private String classPath;

public MyClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
try {
byte[] data = loadByte(className);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终 的字节数组。
return defineClass(className, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}

private byte[] loadByte(String className) throws IOException {
String fileName = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
FileInputStream fis = new FileInputStream(fileName);
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}

public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("D:/Workspace2019/demo/my-class");
Class clazz = myClassLoader.loadClass("com.cnsyear.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("hello",null);
method.invoke(obj,null);
System.out.println(clazz.getClassLoader().getClass().getName());
/*
输出结果:
自定义类加载器。。。
com.example.demo.MyClassLoader
*/
}
}

三、双亲委派机制

3.1 什么是双亲委派机制?

JVM类加载器是有亲子层级结构的,如下图:

image

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载。

3.2 为什么要设计双亲委派机制?
  • 沙箱安全机制:自己写的java.lang.String类不会被加载,这样便可以防止核心API库被随意篡改,使程序更安全。
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性。

例子:

1
2
3
4
5
6
7
8
9
10
11
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("自定义String类。。。");
/*
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
*/
}
}

四、打破双亲委派机制

4.1 Tomcat为什么打破双亲委派机制?

以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?

我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  • 1、一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  • 2、部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
  • 3、web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  • 4、web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。
    再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行?
    答案是不行的。为什么?

第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。

第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。

第三个问题和第一个问题一样。

第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

4.2 Tomcat自定义加载器详解

image

Tomcat的几个主要类加载器:

  • CommonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于 Webapp不可见;
  • SharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对 当前Webapp可见;

从图中的委派关系中可以看出:

CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使 用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加 载的类则与对方相互隔离。

WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个 WebAppClassLoader实例之间相互隔离。

而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的 目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的 JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。

tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?答案是:违背了。

我们前面说过,双亲委派机制要求除了顶层的启动类加载器之外,其余的类加载器都应当由 自己的父类加载器加载。很显然,tomcat不是这样实现,tomcat为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。

五、ClassLoader源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
-------------已经触及底线 感谢您的阅读-------------

本文标题:深入理解JVM原理-第一节 Java类加载机制

文章作者:趙小傑~~

发布时间:2019年10月02日 - 19:53:55

最后更新:2019年11月04日 - 20:08:27

原始链接:https://cnsyear.com/posts/5d86127d.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%