JVM基本概念

Java虚拟机(英语:Java Virtual Machine,缩写为JVM),一种能够运行Java bytecode的虚拟机,以堆栈结构机器来进行实做。最早由Sun微系统所研发并实现第一个实现版本,是Java平台的一部分,能够运行以Java语言写作的软件程序。

Java虚拟机作用:就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。
由于跨平台性的设计,Java的指令都是根据栈来设计的。
JVM的生命周期:启动、运行、退出。

JVM内存架构

这里的内存架构画的是JDK 7版本的,JDK 8之后方法区的画法有所改变。

运行时数据区中,红色框里的是线程共享的区域,灰色的区域是每个线程独有的区域。

JVM内存构

类加载器与类的加载过程

类加载器子系统负责从文件系统或者网络中加载Class文件,Class文件在文件开头有特定的文件标识。
类加载系统只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

类加载过程

类加载过程

类加载之链接

加载

  1. 通过一个类的全限定名获取定义此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

链接

验证(Verification)
目的在于确保Class文件的字节流中包含的信息符合当前虚拟机要求,保证被加载类的正确性;主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。

准备(Preparation)
该阶段会为为类变量分配内存,并设置类变量的默认初始值(比如int类型会赋0)。

这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会给这些final static修饰的变量进行显示初始化。
注意,这里不会为实例变量分配初始化,实例变量将会在对象实例化时随着对象一起分配在堆中。

解析(Resolution)
将常量池内的符号引用转换为直接引用的过程。

初始化

初始化阶段就是执行类构造器方法<clinit>()的过程,这个阶段会进行显示初始化类变量和其他资源。

<clinit>()不同于类的构造器。
若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
虚拟机会保证一个类的<clinit>()方法在多线程下被同步加锁,确保只被加载一次。

类加载器的分类

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)

Java虚拟机规范将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。

引导类加载器(启动类加载器,Bootstrap ClassLoader)

用C++编写的,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库;该加载器无法直接获取。

并不继承自java.lang.ClassLoader,没有父加载器。
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。

扩展类加载器(Extension ClassLoader)

负责jre/lib/ext目录下的jar包或 -D java.ext.dirs 命令指定目录下的jar包装入工作库。派生于ClassLoader类。
父类加载器为启动类加载器。

系统类加载器(也称应用程序类加载器,AppClassLoader)

负责java -classpath-D java.class.path命令所指的目录下的类与jar包装入工作库,是最常用的加载器。派生于ClassLoader类。

父类加载器为启动类加载器。
该类加载时程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载

获取ClassLoader的几种方式

1
2
3
4
5
6
7
8
9
10
11
// 方式一:获取当前类的ClassLoader
clazz.class.getClassLoader();

// 方式二:获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader();

// 方式三:获取系统的ClassLoader
ClassLoader.getSystemClassLoader();

// 方式四:获取调用者的ClassLoader
DriverManager.getCallerClassLoader();

双亲委派机制

工作原理

双亲委派机制的工作原理如下图所示;但是请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。

双亲委派机制

(1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
(2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
(3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会自己去加载。

以上就是双亲委派机制的工作机制。子类加载器可以见到父类加载器加载的类,而父类加载器看不见子类加载器加载的类。

双亲委派机制的优势:

1、避免类的重复加载,当父类加载器已经加载了,那么子加载器就没有必要重新加载一遍;
2、保护程序安全,防止核心API被随意篡改。

沙箱安全机制

如果你自定义了String类,但是在加载自定义String类的时候JVM会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class);然后此时会报错,报错信息可能是没有main方法,因为加载的是rt.jar包中的String类,这个类并不是自定义的String,所以没有相关的main方法。

这样就可以保证对java核心源代码的保护,避免核心类被篡改,这就是沙箱安全机制。

自定义类加载器

根据上面的介绍我们知道,Java虚拟机规范将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器;所以如果我们要自定义类加载器的话,也需要去继承ClassLoader这个类。

ClassLoader

我们先来看看ClassLoaderloadClass方法的源码,该方法是双亲委派以及自定义类加载器的关键:

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
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文件,加载成功就返回一个java.lang.Class,加载不成功就抛出一个ClassNotFoundException,给子加载器去加载。
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// 若以上条件还未加载完成,那么通过findClass方法去寻找该类。
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;
}
}

根据loadClass方法的源码,可以看到双亲委派机制具体是怎么实现的,在每个类被加载前都会通过递归的方式,调用父类加载器去加载;当父类加载器抛出异常或类还未被加载完成,才会一级级的给子加载器去加载。

这里还涉及到findClass方法,我们也来看看:

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

可以看到,没有具体的实现,并直接返回ClassNotFoundException,还是protected的,这充分说明了这个方法就是给开发者重写的。

我们可以重写该方法,让该方法成功的执行并且有返回值,也就是让类加载成功,也就完成了我们想要的自定义类加载器的功能,接下来我们来具体实现一下。

自定义类加载器

从上面的源码解析来看,可以有两个结论:

  • 如果不想打破双亲委派模型,那么自定义类加载器只需要重写findClass方法即可。
  • 如果想打破双亲委派模型,那么自定义类加载器就要重写整个loadClass方法

打破双亲委派模型也很简单,就重写整个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
package com.myspring.demo.config;

public class User {
private String name;
private String id;
private String phoneNumber;

public User() {
}

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public String getId() {
return this.id;
}

public void setId(String id) {
this.id = id;
}

public String getPhoneNumber() {
return this.phoneNumber;
}

public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}

public String toString() {
return "User{name='" + this.name + '\'' + ", id='" + this.id + '\'' + ", phoneNumber='" + this.phoneNumber + '\'' + '}';
}
}

接下来编写自定义的类加载器,跟上面说的流程一样:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class MyClassLoader extends ClassLoader{

private String filePathName;

public MyClassLoader() {
}

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

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException
{
File file = new File(filePathName);
try
{
byte[] bytes = getClassBytes(file);
return this.defineClass(name, bytes, 0, bytes.length);
}
catch (Exception e)
{
e.printStackTrace();
}
return super.findClass(name);
}


private byte[] getClassBytes(File file) {
// 这里要读入.class的字节,因此要使用字节流
FileInputStream fis = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);

while (true)
{
int i = fc.read(by);
if (i == 0 || i == -1) {
break;
}
by.flip();
wbc.write(by);
by.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 保证流的关闭
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return baos.toByteArray();
}
}

自定义类加载器的我们就算完成了,接下来我们试试去使用他:

1
2
3
4
5
6
7
8
9
10
11
public class MyClassLoaderRunTest {
public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("/Users/PersonStudy/User.class");
// 包名+类名需要跟上面生成的字节码文件一致。
// 通过反射的方式创建对象,并指定类加载器。
Class<?> c1 = Class.forName("com.myspring.demo.config.User", true, myClassLoader);
Object obj = c1.newInstance();
System.out.println(obj);
System.out.println(obj.getClass().getClassLoader());
}
}

最后正确输出如下:

1
2
User{name='null', id='null', phoneNumber='null'}
com.myspring.demo.classloader.MyClassLoader@7a81197d

如果得到的不是自定义类加载器加载的,而是系统类加载器加载的,需要删除target目录下已经缓存的User.class文件,避免系统类加载器找到这个文件并去加载完成。

自定义加载器的用途

类的隔离,将非class文件转为Java类等。

其他

在JVM中表示两个class对象是否为同一类,有两个必要条件:

  1. 类的完整类名必须一致,包括包名。
  2. 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

换句话说,在JVM中,即使这两个类对象(class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

对类加载器的引用

JVM必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

类的主动使用和被动使用

Java程序对类的使用方式分为两种:主动使用和被动使用。

主动使用,分为七种情况:

1、创建类的实例;
2、访问某个类或接口的静态变量,或者对该静态变量赋值;
3、调用类的静态方法;
4、反射(比如:Class.forName(“com.aa.Test”));
5、初始化一个类的子类;
6、Java虚拟机启动时被标明为启动类的类;
7、JDK 7 开始提供的动态语言支持,java.lang.invoke.MethodHandle实例的解析结果;

除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。