Java中的数据类型分为两类:基本数据类型和引用数据类型。基本数据类型由JVM预先定义,引用数据类型则需要进行经过类的加载过程加载到JVM的内存中。
按照JVM规范,从字节码文件到加载到JVM内存中的类,到类卸载出内存为止,其整个生命周期包括如下7个阶段:
其中验证、准备、解析三个阶段统称为连接(Linking)。
加载阶段的主要作用是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。其类似于Java类在JVM内存中的一个快照,存储了从字节码文件中解析出的常量池、类字段、类方法等信息。
反射的机制即基于这一基础。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法实现反射的能力。
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,在加载阶段,JVM需要完成以下三件事情:
对于类的二进制数据流,JVM可以通过多种途径获取(只要所读取的字节码符合JVM规范即可):
如果输入数据不是ClassFile的结构,则会抛出ClassFormatError。在获取到类的二进制信息后,JVM会处理这些数据,并最终生成一个java.lang.Class的实例。
加载的类在JVM中创建相应的类模板结构,并存储在方法区中(JDKl.8之前:永久代;J0Kl.8及之后:元空间)。
类将.class文件加载至方法区后,会在堆中创建一个Java.lang.Class对象,每个类都对应有一个Class对象。
数组类本身并不是由类加载器负责加载,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。数组类型(下述简称A)的生成过程:
如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定,否则数组类的可访问性将被缺省定义为public。
验证环节的目的是保证加载的字节码是合法、合理并符合规范的。验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证以及符号引用验证等。
链接阶段的验证虽然拖慢了加载速度,但是它避免了在字节码运行时还需要进行各种检查。
验证的具体内容如下:
格式验证:是否以魔数0XCAFEBABE开头,主版本和副版本号是否在当前Java虚拟机的支持范围内,字节码文件中每一个项是否都拥有正确的长度等。
语义检查:JVM会进行字节码的语义检查,如果存在语义上不符合规范的,JVM不会给予验证通过。比如:
字节码验证:这是验证过程中最为复杂的一个过程,JVM试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:
符号引用的验证:此环节在解析阶段才会执行。Class文件在其常量池会通过字符串记录将要使用的其他类或者方法,验证阶段,JVM会检查这些类或者方法是否存在,以及当前类是否有权限访问这些数据。
准备阶段完成的工作是为类的静态变量分配内存,并将其初始化为默认值。JVM为各类型变量默认的初始值:
类型 | 默认初始值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0F |
double | 0.0D |
char | \u0000 |
boolean | false |
reference | null |
Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,故对应的boolean的默认值为false。
补充两点细节:
解析阶段是JVM将常量池内的符号引用替换为直接引用的过程。
在程序实际运行时,只有符号引用是不够的,比如当如下println()方法被调用时,系统需要明确知道该方法的位置。
//System.out.println()对应的字节码:
invokevirtual #2 <java/io/PrintStream.println : ()V>
以方法为例,JVM为每个类都准备了一张方法表,当需要调用一个类的方法的时候,只要知道其在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。
《Java虚拟机规范》之中并未规定解析阶段发生的具体时间,虚拟机实现可以根据需要自行判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
初始化阶段就是执行类构造器方法
结论:使用final修饰的静态变量,如果直接通过字面量的方式显示赋值,不涉及到方法或构造器调用,则是在连接阶段的准备环节进行赋值,否则是初始化执行
public static final int INT_CONSTANT = 10; // 在连接阶段的准备环节赋值
public static final int NUM1 = new Random().nextInt(10); // 在初始化阶段clinit>()中赋值
public static int a = 1; // 在初始化阶段()中赋值
public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100); // 在初始化阶段()中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(100); // 在初始化阶段()中概值
public static final String s0 = "helloworld0"; // 在连接阶段的准备环节赋值
public static final String s1 = new String("helloworld1"); // 在初始化阶段()中赋值
public static String s2 = "hellowrold2"; // 在初始化阶段()中赋值
如果在一个类的
()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。
Java程序对类的使用分为两种:主动使用和被动使用。
JVM规定,一个类或接口在初次使用前,必须要进行初始化。这里指的“使用”,是指主动使用,主动使用只有下列几种情况:
这里的静态字段指的是需要在初始化阶段进行赋值,才算是主动引用
public class Test {
public static final String a = new String("111"); //在初始化阶段()中赋值
public static final String b = "111"; //在连接阶段的准备环节赋值
}
Test.a; //属于对Test类的主动使用
Test.b; //不属于对Test类的主动使用
主动使用之外的情况,均属于被动使用,被动使用不会触发类的初始化。
并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。
通过子类引用父类的静态变量,不会触发子类初始化
public class PassiveUse {
@Test
public void test() {
System.out.println(Child.num); //会触发Parent的初始化,但不会触发Child的初始化
}
}
class Child extends Parent {
}
class Parent {
public static int num = 1;
}
通过数组定义类引用,不会触发此类的初始化
Parent[] parents= new Parent[10]; //不会触发Parent的初始化
使用常量不会触发类或接口的初始化(因为常量在准备阶段就已经被显式赋值了)
在JVM中,每个类的实例都关联着代表这个类的Class对象
一个类所关联的Class实例与类加载器之间为双向关联关系
一个类何时结束生命周期,取决于其关联的Class对象何时结束生命周期
总结一下,标记方法区中一个类的模板数据“不再使用(允许被回收)”需要满足以下条件:
类加载器(ClassLoader)是JVM执行类加载机制的前提。类加载器负责将Class信息的二进制数据流读入JVM内部,转换为一个与其对应的java.lang.Class对象实例。
类加载器的命名空间:
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。即使两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,在JVM中也会被认为是不同的类。
JVM在启动的时候会首先创建sun.misc.Launcher类,Launcher类的构造器中设置了程序运行中需要的类加载器:
public Launcher() {
ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
...
}
这里的parent并不是Java语言意义上的继承关系,而是一种包含关系!!!
JVM支持两种类型的类加载器 :引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
使用 C/C++语言实现,嵌套在JVM内部,并不继承自java.lang.ClassLoader,没有父加载器。尝试获取引导类加载器,获取到的值为 null
ClassLoader classLoader = java.security.Provider.class.getClassLoader();//由启动类加载器加载
System.out.println(classLoader); //打印为null
用于加载扩展类和应用程序类加载器,并指定为他们的父类加载器
出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类(JAVA_HOME/jre/lib/rt.jar、sun.boot.class.path等路径下的内容)
public static void main(String[] args) {
// 获取BootstrapclassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
}
public static void main(String[] args) {
ClassLoader classLoader = sun.security.ec.CurveDB.class.getClassLoader();
System.out.println(classLoader); //sun.misc. Launcher$ExtCLassLoader@1540e19d
System.out.println(classLoader.getParent()); //null,说明父类加载器是Bootstrap ClassLoader
}
public static void main(String[] args) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2
}
public Class<?> loadClass(String name) throws ClassNotFoundException
ClassLoader的loadClass()方法用于加载名称为name的类,返回结果为java.lang.Class类的实例。该方法中实现了双亲委派机制的逻辑(剔除了部分非核心代码):
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
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) {
}
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name); //父加载器无法加载,尝试自己加载
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
ExtClassLoader并没有重写loadClass(),而AppClassLoader虽然重写了loadClass()方法,但最终调用的还是父类的loadClass()方法,这说明两者的类加载流程均遵守双亲委派机制
protected Class<?> findClass(String name) throws ClassNotFoundException
protected final Class<?> defineClass(String name, byte[] b,int off,int len)
双亲委派机制优势:
双亲委派机制弊端:
双亲委派机制并不是具有强制性约束的机制。,而是Java设计者推荐给开发者的类加载器实现方式。在Java中大部分的类加载器都遵循这个机制,但也有例外的情况。
以Java的标准服务JNDI服务为例,其代码由启动类加载器来完成加载(位于rt.jar)。但JNDI需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?
在Java平台中,通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI
为了解决这个困境,Java的设计团队引入了一个不太优雅的设计:线程上下文类加载器(ThreadContextClassLoader)。
Java提供了抽象类java.lang.ClassLoader,用户自定义的类加载器需要继承ClassLoader类。通常有两种具体的实现:
loadclass()方法中实现了双亲委派机制,重写这个方法可能会导致机制被破坏,容易造成问题。因此在findClass()里重写自定义类加载器的加载逻辑是更好的选择。
自定义类加载器有哪些好处?
隔离加载类。比如Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离不同目录下的类库。
扩展加载源。比如从数据库、网络、甚至是电视机机顶盒进行加载
防止源码泄漏。Java代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,加入还原加密字节码的逻辑。
如果使用自定义类加载器,需要注意一个细节。在做Java类型转换时,只有两个类型由同一个类加载器所加载,才能进行类型转换,否则转换时会发生异常。