笔记来源:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)
笔记来源:黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓附有一些个人见解,如有错误,请指正!
文章目录
1.类的加载过程
类加载器子系统作用
- 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。(魔数 cafebabe 4字节)
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
- 在HotSpot虚拟机JDK1.6版本当中,方法区的实现是永久代(方法区处在JVM内存当中)此时保存着静态变量的引用,静态变量的实体对象还是在堆当中!
- 在HotSpot虚拟机JDK1.7版本当中,永久代存在,静态变量的引用移除,此时静态变量存储在堆中,StringTable 字符串常量池移到了堆当中
- 在HotSpot虚拟机JDK1.8版本当中,方法区的实现是元空间Metaspace,(元空间处在电脑本地内存当中) ,此时StringTable 字符串常量池以及静态变量还是在堆中
- 所以在HotSpot虚拟机当中,对象的实体内容一直都在堆当中,变化的只是对它的引用!
加载阶段
类的加载指的是将类的.class文件中的二进制数据读取到内存中,存放在运行时数据区的方法区中,并创建一个大的Java.lang.Class对象放在堆中(此时还未对静态变量进行赋值),用来封装方法区内的数据结构 在加载类时,Java虚拟机必须完成以下3件事情:
-
通过类的全名,获取类的二进制数据流
- 虚拟机可能通过文件系统读入一个class后缀的文件(最常见)
- 读入jar、zip等归档数据包,提取类文件。
- 事先存放在数据库中的类的二进制数据
- 使用类似于HTTP之类的协议通过网络进行加载
- 在运行时生成一段Class的二进制信息等
-
解析类的二进制数据流为方法区内的数据结构(Java类模型)(将字节码中的方法字节码,常量池等信息放入instanceKlass 当中)
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用 (指向堆中的Class对象)
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池(也就是运行时常量池,每个类独一份)
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
如果这个类还有父类没有加载,先加载父类
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror对应的对象(xxx.class)是存储在堆中 -
创建java.lang.Class类的实例放在堆中,同时它的对象头里有指向方法区instanceKlass的地址的引用(当class对象获取方法,字段,权限等信息时,实际上是通过这个引用,跑到方法区里获取相关内容)
链接阶段
验证(Verify)
- 目的在子确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,==文件格式验证,元数据验证,字节码验证,符号引用验证。
格式检查:是否以魔数oxCAFEBABE开头,主版本和副版本是否在当前Java虚拟机的支持范围内,数据中每一项是否都拥有正确的长度等
准备(Prepare)
-
为类变量(静态变量,而不是实例变量)分配内存并且设置该类变量的默认初始值,即零值。
static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成 -
如果 static 变量是 final修饰的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成,即没有赋初值的操作
-
如果 static 变量是 final修饰的引用类型,那么赋值则会在初始化阶段完成
-
这里不会为实例变量分配初始化,实例变量是会随着对象创建一起分配到Java堆中。
注意!
- 在HotSpot虚拟机JDK1.6版本当中,方法区的实现是永久代(方法区处在JVM内存当中)此时保存着静态变量的引用,静态变量的实体对象还是在堆当中!
- 在HotSpot虚拟机JDK1.7版本当中,永久代存在,静态变量的引用移除,此时静态变量存储在堆中,StringTable 字符串常量池移到了堆当中
- 在HotSpot虚拟机JDK1.8版本当中,方法区的实现是元空间Metaspace,(元空间处在电脑本地内存当中) ,此时StringTable 字符串常量池以及静态变量还是在堆中
- 所以在HotSpot虚拟机当中,对象的实体内容一直都在堆当中,变化的只是对它的引用!
解析(Resolve)
即将方法区常量池中的符号引号转换为直接引用的过程(简言之,将类、接口、字段和方法的符号引用转为直接引用)
-
符号引用: 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中
符号引号有:类和接口的权限定名、字段的名称和描述符、方法的名称和描述符
-
直接引用: 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的
-
在HotSpot VM中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析操作也许会伴随着JVM在执行完初始化之后再执行
因为引用类型的类变量在初始化阶段才被创建赋值,在初始化j结束后解析,才能把这些引用类型的类变量的符号引用转换成直接引用
初始化阶段
-
初始化阶段就是执行类构造器方法
<clinit>
()的过程。 -
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来(按照代码顺序拼凑而成)。
-
构造器方法中指令按语句在源文件中出现的顺序执行。
-
<clinit>
()不同于类的构造器。(关联:对象构造器是虚拟机视角下的init
()) -
若该类具有父类,JVM会保证子类的 clinit()执行前,父类的
<clinit>
()已经执行完毕。(由父及子,静态先行) -
虚拟机必须保证一个类的
<clinit>
()方法在多线程下被同步加锁。
什么情况下不会生成
clinit
方法呢?
- 一个类中并没有声明任何的类变量,也没有静态代码块时
- 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
- 一个类中包含的基本数据类型的类变量是static final修饰的(这种变量会在链接阶段的准备阶段中进行赋值,直接赋值常量因为他存在于class文件的常量池中)
public class Load9 {
// //场景1:对应非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
public int a = 1;
// //场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
public static int b;
// //场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
public static final int c = 1;
//这种情况会生成<clinit>()方法
public static final Object d = new Object();
public static void main(String[] args) {
}
}
2. 类加载后是否初始化
class A {
static int a = 0;
static final int aFinal = 1;
static final Object aObj = new Object();
static {
System.out.println("a init");
}
}
class B extends A {
static int b = 2;
static final int bFinal = 3;
static final Object bObj = new Object();
static {
System.out.println("b init");
}
}
public class Load9 {
//虚拟机启动,会初始化有main方法的类
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
// 1. 子类访问父类未用final修饰的静态变量,会触发父类初始化
System.out.println(B.a);
// 1. 子类访问父类用final修饰的基本类型的静态变量,不会触发父类初始化(因为在链接的准备阶段,值已确定)
System.out.println(B.aFinal);
// 1. 子类访问父类用final修饰的引用类型的静态变量,会触发父类初始化
System.out.println(B.aObj);
// 1. 静态常量(基本类型和字符串)不会触发初始化
System.out.println(B.bFinal);
// 1. 首次访问这个类的静态变量或静态方法时,会触发初始化
System.out.println(B.b);
// 1. 子类初始化,会导致父类初始化
System.out.println(B.b);
// 2. 创建该类的数组不会触发该类初始化,也不会触发父类初始化
System.out.println(new B[0]);
// 3. 类对象.class 不会触发初始化
System.out.println(B.class);
// 4. 不会初始化类 B,但会加载 B、A
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass("B");
// 5. 不会初始化类 B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("B", false, c2);
// 6. 会初始化类 B,并先初始化类 A
Class.forName("B", true, c2);
// 7. 会初始化类 B,并先初始化类 A
Class.forName("B");
}
}
3. 类加载器分类
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
3.1 启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自ava.lang.ClassLoader,没有父加载器。
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
3.2 扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
3.3 应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun.misc.LaunchersAppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器
3.4 用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。(自定义类加载器通常需要继承于 ClassLoader)
自定义 ClassLoader 的子类时候,我们常见的会有两种做法:
- 重写loadClass()方法(JDK1.2之后不推荐)
- 重写findClass()方法 -->推荐
这两种方法本质上差不多,毕竟loadClass()也会调用findClass(),但是从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用。
3.5 ClassLoader的使用说明
每个方法区的instanceKlass对象都会包含一个定义它的ClassLoader的一个引用
ClassLoader类是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
sun.misc.Launcher 它是一个java虚拟机的入口应用
获取ClassLoader的途径
-
方式一:获取当前ClassLoader
clazz.getClassLoader()
-
方式二:获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
-
方式三:获取系统的ClassLoader
ClassLoader.getSystemClassLoader()
-
方式四:获取调用者的ClassLoader
DriverManager.getCallerClassLoader()
3.6 双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作原理
- 1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
本质
规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载
如何破坏双亲委派机制?
- 自定义类加载器,重写loadclass方法
- SPI机制(Service Provider Interface)绕开loadclass 方法。获取当前线程上下文类加载器进行加载
线程上下文类加载器(破坏双亲委派机制及举例)
当我们加载jdbc.jar 用于实现数据库连接的时候,首先我们需要知道的是 jdbc.jar是基于SPI接口进行实现的,所以在加载的时候,会进行双亲委派,最终从根加载器中加载 SPI核心类,然后在加载SPI接口类,接着在进行反向委派,通过线程上下文类加载器进行实现类jdbc.jar的加载。
我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写
Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?
让我们追踪一下源码:
public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
先不看别的,看看 DriverManager 的类加载器:
System.out.println(DriverManager.class.getClassLoader());
打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?
继续看 loadInitialDrivers() 方法:
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String> () {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 1)使用 ServiceLoader 机制加载驱动,即 SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2)使用 jdbc.drivers 定义的驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载,破坏了双亲委派机制,直接让应用程序类加载器加载。
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)
约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称
这样就可以使用
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}
来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:
- JDBC
- Servlet 初始化器
- Spring 容器
- Dubbo(对 SPI 进行了扩展)
接着看 ServiceLoader.load 方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类LazyIterator 中:
private S nextService() {
if (!hasNextService()) throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// loader即线程上下文类加载器
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error();
// This cannot happen
}
双亲委托模式的优势
- 避免类的重复加载(当父ClassLoader已经加载了该类的时候,就没有必要子ClassLoader再加载一次)
- 保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.String
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
双亲委托模式的弊端
检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类
结论
由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而已。比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的上级加载器去执行,这同时也是Servlet规范推荐的一种做法
沙箱安全机制
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
3.7 其他
如何判断两个class对象是否相同
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。