一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载,验证,准备,解析,初始化,使用,和卸载七个阶段,其中验证,准备,解析三个部分统称为连接。
加载
加载阶段Java虚拟机需要完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
第一点和第二点不需要解释,如果不明白,用心多读几遍。这里着重讲解下第三点
字节码被加载器加载至方法区后,是用C++的instanceKlass 描述 java 类。你问我为什么不直接用java描述这个类?这是因为HotSpot虚拟机不是用java语言实现的,而是用C++和C实现。
那Java和C++如何进行沟通?
这就要说到 C++的instanceKlass 的组成域,它的主要field有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
- _super 即父类
- _fifields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
其中 _java_mirror是java的类镜像,它持有该类在堆内存中的地址,堆内存中的类也持有instanceKlass在本地内存中的地址。加载这一步骤就是通过_java_mirror镜像得到类的信息。就是由 C++ 到 Java的一个转换。
扩展:我们通过类模版创建对象后,此时对象头中有8个字节对应 类模版(class)在堆内存的地址,如果想调用普通方法,或者 get,set 方法就要通过对象头中 类模版地址找到元空间中的instanceKlass,再通过instanceKlass获取 field,methods 信息。
注:如果这个类还有父类没有加载,先加载父类;加载和链接可能是交替运行的;
连接(验证,准备,解析)
验证:确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部要求,保证这些信息被当作代码运行后不会危害虚拟机的自身安全。
以魔数为例,修改魔数后报 ClassFormatError
HelloWorld字节码文件:
修改魔数:
运行报错信息:
准备:为 static 变量分配空间,设置默认值。不包括实例变量,实例变量将会在对象是实例化时随着对象一起分配在Java堆中。
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 fifinal 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 fifinal 的,但属于引用类型,那么赋值也会在初始化阶段完成
解析:将常量池中的符号引用解析为直接引用。在常量池中各种符号引用,但是这些引用的对象,在类中没有使用,它就不会进行解析和初始化。用到了才会去创建。有点类似于单例模式中的懒汉式。
Eg:
package com.zhao.JvmTest;
import java.io.IOException;
/*** 解析的含义 */
public class Demo9 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classloader = Demo9.class.getClassLoader();
// 这里主动加载了类C,而且 loadClass 方法不会导致类的解析和初始化
Class<?> c = classloader.loadClass("com.zhao.JvmTest.C");
//new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
通过HSDB命令“HackerZhao:Home apple$ java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB”得知
注释类加载器,放开使用new C();的代码
初始化
初始化:初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全
发生的时机
概括得说,类初始化是【懒惰的】
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会导致类初始化的情况
- 访问类的 static fifinal 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的loadClass方法
- Class.forName的参数2为false时
Eg:
package com.zhao.JvmTest;
public class Load3 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
// 1. 静态常量(基本类型和字符串)不会触发初始化
// System.out.println(B.b);
// 2. 类对象.class 不会触发初始化
// System.out.println(B.class);
// 3. 创建该类的数组不会触发初始化
// System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
// ClassLoader cl = Thread.currentThread().getContextClassLoader();
// cl.loadClass("com.zhao.JvmTest.B");
// 5. 不会初始化类 B,但会加载 B、A
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("com.zhao.JvmTest.B", false, c2);
// 1. 首次访问这个类的静态变量或静态方法时
// System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发
// System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
// System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
Class.forName("com.zhao.JvmTest.B");
}
}
class A{
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A{
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
小案例
Demo1:字节码角度分析,类的初始化时机
package com.zhao.JvmTest;
public class Load {
public static void main(String[] args) {
System.out.println(E.a); //不会初始化
System.out.println(E.b); //不会初始化
System.out.println(E.c); //会初始化
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;
}
反编译后字节码
{
public static final int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 10
public static final java.lang.String b;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String hello
public static final java.lang.Integer c;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 20
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #3 // Field c:Ljava/lang/Integer;
8: return
LineNumberTable:
line 14: 0
}
静态变量a和b在连接阶段已经赋上值了,不会让类初始化。c的值需要在类初始化后才能确定这个值是多少。(它有一个装箱操作,知道箱子是谁,不知道箱子里边装的是什么)
Demo2:内部类初始化的时机(单例模式的实现)
package com.zhao.JvmTest;
public class Demo10{
public static void main(String[] args) {
//Singleton.test(); //不会触发内部类 LazyHolder 的初始化
Singleton.getInstance(); //通过外部类调用内部类的初始化方法,才会触发内部类的初始化
}
}
class Singleton {
public static void test(){
System.out.println("test");
}
private Singleton() {
}
// 内部类中保存单例
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
static {
System.out.println("lazy holder init");
}
}
// 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
内部类初始化的线程安全是可以保证的,是由类加载器来保证的。
类加载器
jdk8有四种类加载器:
名称 | 加载哪的类 | 说明 |
Bootstrap ClassLoader(启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader(扩展类加载器) | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader (应用程序类加载器) | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
加载一个自定义类,类如User,应用程序加载器会先看扩展类加载器有没有加载过,如果没有就会找启动类加载器看是否加载过。如果也没有,就自己加载。
通过上边的表格可以得知启动类加载器加载的是lib目录下的类,这些类都是一些核心类,自己定义的类是无法加载的。接下来我会尝试通过启动类加载器加载自定义类。
如果类加载器调用 getClassLoader() 方法打印的是null,则代表它是启动类加载器,java是无法直接调用C++的,因此打印的是null。如果打印的是Ext ClassLoader,则代表它是扩展类加载器,如果打印的是App ClassLoader,则代表它是应用程序类加载器。
启动类加载器
自定义F类
package com.zhao.JvmTest;
public class F {
static {
System.out.println("bootstrap F init");
}
}
启动类
package com.zhao.JvmTest;
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.zhao.JvmTest.F");
System.out.println(aClass.getClassLoader());
}
}
终端输入命令
HackerZhao:classes apple$ java -Xbootclasspath/a:. com.zhao.JvmTest.Load2
-Xbootclasspath 表示设置 bootclasspath
其中 /a:. 表示将当前目录追加至 bootclasspath 之后
打印结果
扩展类加载器
自定义G类
package com.zhao.JvmTest;
public class G {
static {
System.out.println("ext G init");
}
}
启动类
package com.zhao.JvmTest;
public class Load1_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.zhao.JvmTest.G");
System.out.println(aClass.getClassLoader());
}
}
终端输入命令,将G类打成jar包
将my.jar包放至JavaHome/jre/lib/ext/目录下
运行结果
应用程序类加载器
还是上面的例子,只需把刚才加入的jar包删掉,运行,便可打印应用程序类加载器。较为简单这里就不演示了。
以上演示的就是双亲委派模式,当你想加载一个类的时候,需要看看自己的上级有没有加载过。
源码分析双亲委派
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
打破双亲委派模型
原本需要上级可以加载的就需要上级去加载,上级不能加载的自己才可以加载。但是有一种情况比如启动类加载一个类,这个类不在启动类加载路径下,需要应用程序类加载器才可以加载到。这时就需要打破双亲委派,在启动类中加载不是 JAVA_HOME/jre/lib 下的类。
Eg:DriverManager被启动类加载器加载
启动类加载器想加载驱动类,是做不到的,它可以加载DriverManager这个类,但它中的Driver类只能通过应用类加载器加载。
public class DriverManager {
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
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);
}
}
}
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器,jvm启动时默认把应用程序加载器赋值给当前线程
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
}
运行期优化
即时编译
package com.zhao.JvmTest;
public class JIT1 {
public static void main(String[] args) {
for (int i = 0; i < 400; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n", i, (end - start));
}
}
}
打印结果:
结果太长了,这里就不放上来了。结果从上到下时间越来越短。
原因是什么呢?
JVM 将执行状态分成了 5 个层次:
- 0 层,解释执行(Interpreter)
- 1 层,使用 C1 即时编译器编译执行(不带 profifiling)
- 2 层,使用 C1 即时编译器编译执行(带基本的 profifiling)
- 3 层,使用 C1 即时编译器编译执行(带完全的 profifiling)
- 4 层,使用 C2 即时编译器编译执行
profifiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的次数】等
即时编译器(JIT)与解释器的区别
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
- 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT 会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来)
刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果
方法内联
Eg
package com.zhao.JvmTest;
public class JIT2 {
// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印 inlining 信息
// -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
// -XX:+PrintCompilation 打印编译信息
public static void main(String[] args) {
int x = 0;
for (int i = 0; i < 500; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
x = square(9);
}
long end = System.nanoTime();
System.out.printf("%d\t%d\t%d\n", i, x, (end - start));
}
}
private static int square(final int i) {
return i * i;
}
}
System.out.println(square(9));
如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:
System.out.println(9 * 9);