概述
java中使用到的类型主要分为两种:基本类型和引用类型,基本类型是JVM预先定义好的,引用类型是用Class表示的一个数据结构,所以在使用前需要将Class进行解析至内存中,数据会放在方法区(《JVM之内存结构》)内,会经过加载、连接、初始化这几个流程,下面我们来详细了解下。
注意:环境为jdk1.8。
加载
加载分为三步:
- 通过一个类全限定名来获取定义此类的二进制字节流。(class文件可以从磁盘、网络、动态生成中获得)
- 将整个字节流所代表的静态存储结构转化为方法区的instanceKlass(普通类在JVM中对应的C++类,存在于方法区中,包含属性、方法等类元信息)。
- 在堆中生成一个代表整个类的InstanceMirrorKlass对象(class对象),作为方法区的这个类的各种数据的访问入口。(生成对象只是方便暴露一个类的访问入口,比如在连接的解析阶段会把符号引用转换为实际引用但是不一定会进行连接和初始化,等真正需要的时候才进行连接、初始化可以加快启动速度。(有点像bean的懒加载))
类加载器
Java类加载器(java Classloader)是java运行时环境的一个部件,负责动态(按需)加载Java类到Java虚拟机的内存空间。
JVM中有3个默认类加载器:
-
启动类加载器(Bootstrap ClassLoader):负责加载最为基础、最为重要的类,比如存放在JER的lib目录下jar包的类(以及由虚拟机参数-Xbootclasspath指定的路径)比如java.开头的类库比如Object类。
这里举个例子,jre的lib中的有些类的调用只能被内部调用,比如Unsafe:
import sun.misc.Unsafe; public class GetUnsafe { public static void main(String[] args) { Unsafe.getUnsafe(); System.out.println("ddddd"); } }输出:
Exception in thread "main" java.lang.SecurityException: Unsafe at sun.misc.Unsafe.getUnsafe(Unsafe.java:90) at com.study.jvm.classloader.GetUnsafe.main(GetUnsafe.java:7)可以看到会抛出一个SecurityException异常,原因是调用放的加载器为,被认为是不安全的。
@CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }所以我们就可以通过-Xbootclasspath指定GetUnsafe被Bootstrap ClassLoader加载。
java -Xbootclasspath/a:xxx.jar.original -cp xxx.jar.original com.xxx.GetUnsafe ddddd笔者这里打包了一个jar包然后使用-Xbootclasspath指定jar并运行jar包中的路径。
-
扩展类加载器(Extension CLassLoader):它的父类加载器是启动类加载器,它负责加载相对次要的、但又通用的类,比如存在JRE的lib/ext目录下jar包的类(以及由系统变量java.ext.dirs指定的类)比如javax.开头的类库DataSource。
-
应用类加载器(Application ClassLoader):它负责加载应用程序路径下的类。(可以使用-classpath指定用于搜索类文件)。默认情况下,应用程序包含的类便是由应用类加载器加载的。
可以运行下面程序查看不同的类加载器加载路径:
import sun.misc.Launcher;
import java.net.URL;
import java.net.URLClassLoader;
/**
* description : 获取三种JVM类加载器的加载路径
*
* @author : wanghaifeng
* date : 2021/5/29
*/
public class ClassLoaderPathTest {
/**
* 主函数,程序入口
*/
public static void main(String[] args) {
//首先是启动类加载器
System.out.println("启动类加载器的加载路径:");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urLs) {
System.out.println(url);
}
System.out.println("=======================================================");
System.out.println("扩展类加载器的路径:");
//然后是扩展类加载器。获取方式是获取系统类加载器(AppClassLoader)的父加载器
//(注意这就需要开启双亲委派模型)
URLClassLoader extClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader().getParent();
urLs = extClassLoader.getURLs();
for (URL url : urLs) {
System.out.println(url);
}
System.out.println("=======================================================");
//应用程序类加载器
System.out.println("应用程序类加载器的路径:");
URLClassLoader appClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
urLs = appClassLoader.getURLs();
for (URL url : urLs) {
System.out.println(url);
}
}
}
输出:
启动类加载器的加载路径:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/classes
=======================================================
扩展类加载器的路径:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/jaccess.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/zipfs.jar
=======================================================
应用程序类加载器的路径:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/deploy.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/jaccess.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/zipfs.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/javaws.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jfxswt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/management-agent.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/plugin.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/rt.jar
file:/Users/xxx/IdeaProjects/xxx/target/classes/
file:/Users/xxx/.m2/**/*.jar
file:/Applications/IntelliJ%20IDEA.app/Contents/lib/idea_rt.jar
可以通过下面的代码打印不同类加载器:
public static void main(String[] args) {
ClassLoader appClassLoader = Launcher.getLauncher().getClassLoader();
System.out.println(appClassLoader);
System.out.println(appClassLoader.getParent());
System.out.println(appClassLoader.getParent().getParent());
}
输出
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@49097b5d
null
我们也可以自定义类加载器,比如对class文件进行加密,使用自定义类加载器来进行加密;比如因为类的唯一性是由类加载器实例以及类的全名一同确定的,我们可以利用这个特点实现运行同一个类的不同版本。
双亲委派
根据上面的描述可以看到,默认的类加载器具有父子关系,这样涉及的目的是防止同一个class文件在不同的类加载器中加载多次(这样就出现了多个版本),保证java程序的安全稳定运行。所以当加载一个类的时候会从顶级的父类开始查看是否能加载,不能才会传递到下面的字类加载器进行加载。
相同的类被加载了如果不被销毁的话不会加载多次(会进行判断是否加载过):
package com.study.jvm.classloader;
import lombok.SneakyThrows;
import sun.misc.Launcher;
public class ClassLoaderRepeatedlyTest {
@SneakyThrows
public static void main(String[] args) {
Class<?> aClass = Launcher.getLauncher().getClassLoader().loadClass("com.study.jvm.classloader.ClassLoaderRepeatedlyTest");
Class<?> bClass = Launcher.getLauncher().getClassLoader().loadClass("com.study.jvm.classloader.ClassLoaderRepeatedlyTest");
System.out.println(aClass == bClass);
}
}
输出:
true
连接
连接就是将创建的类合并至java虚拟机中,使之能够执行的过程。它可以分为验证、准备以及解析三个阶段。
验证:确保加载的类能够满足Java虚拟机的约束条件。
准备:被加载类的静态字段分配内存。静态字段的具体初始化会在“初始化阶段“进行。
解析:将符号引用解析成实际引用。如果符号引用指向一个未被加载的类或者未被加载类的字段或者方法,那么解析将触发这个类的加载(连接和初始化不一定)。
初始化
就是对类的静态字段进行赋值,如果是常量值会由java虚拟机完成,除此之外的直接赋值操作以及静态代码块中的代码都会被编译器放在<clinit>中,JVM会通过加锁来确保类的<clinit>方法仅被执行一次。完成类初始化类才正式成为可执行的状态。
JVM规范枚举了下述多种类初始化触发情况:
- 当虚拟机启动时,初始化用户指定的主类。
- 当遇到用以新建目标类实例的new指令,初始化new指令的目标类。
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类。
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类。
- 子类初始化会触发父类初始化。
- 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化。(笔者认为如果没有default方法的接口,实现类已经包含了所需要的信息,再编译阶段就已经完成是否实现接口了)
- 使用反射API对某个类进行反射调用的时候,初始化这个类。
- 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。