深入理解Java虚拟机(一)

深入理解Java虚拟机(一)

类加载机制

在 Java 代码中,类型(类,接口,枚举)的加载、连接(验证,准备,解析)与初始化过程都是在程序运行期间完成的,提供了更大的灵活性,增加了更多的可能性

类加载器深入剖析

Java 虚拟机与程序的生命周期

在如下几种情况下,Java 虚拟机将结束生命周期

  • 执行了 System.exit() 方法
  • 程序正常执行结束
  • 程序在执行的过程中遇到了异常或则错误而异常终止
  • 由于操作系统出现了错误,导致 Java 虚拟机进程结束

类的加载、连接与初始化

  • 加载:查找并加载类的二进制数据
  • 连接
    • 验证:确保被加载的类的正确性
    • 准备:为类的静态变量分配内存,并将其初始化为默认值
    • 解析:把类中的符号引用转化为直接引用
  • 初始化:为静态变量赋予正确的初始值
  • 使用(类的实例化):
    • 为新的对象分配内存
    • 为实例变量赋默认值
    • 为实例变量赋予正确的初始值
    • Java 编译器为它编译的每一个类都至少生成一个实例初始化方法,在 Java 的 class 文件中,这这实例初始方法被称为 <init> ,对源代码中的每一个类的构造方法,java 编译器都产生一个 <init> 方法

Java 程序对类的使用方式可以分为两种:

  • 主动使用
    • 创建类的实例
    • 访问某个类或接口的静态变量,或则对该静态变量赋值
    • 调用类的静态方法
    • 反射(如 Class.forName("com.cuzz.Test")
    • 初始化一个子类
    • Java 虚拟机启动时被标明为启动类的类
  • 被动使用

所有的 Java 虚拟机实现必须在每个类或接口被 Java 程序首次主动使用时才初始化他们

我们来看一段代码

public class MyTest01 {

public static void main(String[] args) {
System.out.println(Child1.str);
}
}

class Parent1 {
public static String str = "hello world";
static {
System.out.println("Parent1 static block");
}
}

class Child1 extends Parent1 {
static {
System.out.println("Child1 static block");
}
}

输出

Parent1 static block
hello world

对于静态代码块,只有定义该字段的类才会被初始化,这个 Child1.str 是子类调用父类的静态字段,所以子类不会被初始化,父类才会被初始化,这是对 Parent1 的主动使用,对于这个例子只是用了 Child1 的名字,并没有主动使用 Child1 这个类

我们在来看看有没有被加载到虚拟机中,在 VM options : -XX:+TraceClassLoading 在运行

...
[Loaded com.cuzz.jvm.classloader.Parent1 from file:/E:/project/learn-demo/demo-10-jvm-lecture/out/production/classes/]
[Loaded com.cuzz.jvm.classloader.Child1 from file:/E:/project/learn-demo/demo-10-jvm-lecture/out/production/classes/]
Parent1 static block
hello world
[Loaded java.lang.Shutdown from E:\deployer\jdk8\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from E:\deployer\jdk8\jre\lib\rt.jar]

发现这两个类已经被加载到虚拟中

再看一个例子

public class MyTest01 {

public static void main(String[] args) {
System.out.println(Child1.str2);
}
}

class Parent1 {
public static String str = "hello world";
static {
System.out.println("Parent1 static block");
}
}

class Child1 extends Parent1 {
public static String str2 = "welcome";
static {
System.out.println("Child1 static block");
}
}

输出

Parent1 static block
Child1 static block
welcome

当我们初始一个子类,我们会先初始化父类,所以会线输出父类的静态代码块

如果我们加上 final 变为常量

/**
* @Author: cuzz
* @Date: 2019/1/25 19:16
* @Description:
*/
public class MyTest02 {
public static void main(String[] args) {
System.out.println(Parent2.str);
}
}
class Parent2 {
public static final String str = "hello world";
static {
System.out.println("Parent2 static block");
}
}

输出

hello world

常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中(也就是说会存入MyTest02这个类中),本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化

注意:这里指的是将常量存放到了 MyTest02 的常量池中,之后 MyTest02 与 Parent2 就没有任何关系了,甚至我们可以将 Parent 的 class 文件删除

我们进入 classes 目录下使用:javap -c com.cuzz.jvm.classloader.MyTest02 命令反编译一下

Compiled from "MyTest02.java"
public class com.cuzz.jvm.classloader.MyTest02 {
public com.cuzz.jvm.classloader.MyTest02();// (1)
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 (2) // String hello world (3)
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
  1. 是构造方法
  2. ldc 助记符表示将 int,float 或 String 类型的值从常量池中推送至栈顶
  3. 可以看出Parent2.str 已经转化为 hello world

注:当 int 取值-15采用iconst指令,取值-128127采用 bipush 指令,取值-3276832767采用 sipush 指令,取值-21474836482147483647采用 ldc 指令

我们在看一个例子

public class MyTest03 {
public static void main(String[] args) {
System.out.println(Parent3.str);
}
}
class Parent3 {
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("Parent3 static block");
}
}

输出

Parent3 static block
bee2f54d-8960-46d0-b5d7-02666fcf4a14

相比于上一个例子,我们发现输出了静态代码块,说明 Parent3 这个类被初始化了,当一个常量的值并非编译期间可以确定的,那么器值就不会放到调用类的常量池中,这是在程序运行时,会导致主动使用这个常量所在的类,会导致这给类初始化

再看一个例子

public class MyTest04 {
public static void main(String[] args) {
Parent4[] parent4s = new Parent4[1];
System.out.println("---------");
System.out.println(parent4s.getClass());
System.out.println(parent4s.getClass().getSuperclass());
System.out.println("---------");
int[] ints = new int[1];
System.out.println(ints.getClass());
System.out.println(ints.getClass().getSuperclass());
}
}

class Parent4 {
static {
System.out.println("Parent4 static block");
}
}

输出

---------
class [Lcom.cuzz.jvm.classloader.Parent4;
class java.lang.Object
---------
class [I
class java.lang.Object

Process finished with exit code 0

对于数组实例来说,其类型是由 JVM 在运行期动态生成的,表示为 [Lcom.cuzz.jvm.classloader.Parent4 这种形式,动态生成的类型,其父类型就是 Object

对于数组来说,JavaDoc 经常将构成的数组元素称为 Component,实际上就是将数组降低一个维度的类型

我们使用 javap -c com.cuzz.jvm.classloader.MyTest04 进行反编译

public class com.cuzz.jvm.classloader.MyTest04 {
public com.cuzz.jvm.classloader.MyTest04();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: iconst_1
1: anewarray #2 // class com/cuzz/jvm/classloader/Parent4
4: astore_1
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String ---------
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_1
17: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
20: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
23: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_1
27: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
30: invokevirtual #8 // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
33: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
36: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #4 // String ---------
41: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: iconst_1
45: newarray int
47: astore_2
48: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
51: aload_2
52: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
55: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
58: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
61: aload_2
62: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
65: invokevirtual #8 // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
68: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
71: return
}

里面有两个助记符

  • anewarray :表示创建一个引用类型的(如类、接口、数组)数组,并将其值压入栈顶
  • newarray:表示创建一个指定的原始类型(如int、float、char等)数组,并将其引用值压入栈顶

下一个例子

public class MyTest05 {
public static void main(String[] args) {
System.out.println(Child5.j);
}
}
interface Parent5 {
int i = 5;
}

interface Child5 extends Parent5 {
int j = 55;
}

编译之后我们把 Parent5.class 文件删掉,还能打印出 55,说明当一个接口在初始化时,并不要求其父接口都完成初始化,如果我们把 Child5.class 文件也删掉,也能打印出 55,原来接口中的修饰符默认为 public static final 说明接口中的值是一个常量,不需要加载到 JVM 中,也就没有初始化。

public class MyTest05 {
public static void main(String[] args) {
System.out.println(Child5.j);
}
}
interface Parent5 {
public static Thread thread = new Thread() {
{
System.out.println("Parent5 static block");
}
};
}

class Child5 implements Parent5 {
public static int j = 55;
}

此时也也是输出 55 ,也没有初始化 Child5 接口 Parent5

下一例子

public class MyTest06 {
public static void main(String[] args) {
Singleton singleton = Singleton.newSingleton();
System.out.println(Singleton.counter1);
System.out.println(Singleton.counter2);
}
}

class Singleton {

public static int counter1;

private static Singleton singleton = new Singleton();

private Singleton() {
counter1++; // counter1 = 1
counter2++; // counter2 = 1
}

public static int counter2 = 0; // 此时又把值赋值为 0

public static Singleton newSingleton() {
return singleton;
}
}

此时输出

1
0

为什么会这样呢,准备阶段 counter1 和 counter2 的初始值都是 0 ,初始化阶段从上往下赋值,后面 counter2 又赋值为 0

我们再来回顾一下

public class MyTest09 {
static {
System.out.println("MyTest09 static block");
}
public static void main(String[] args) {
System.out.println(Child9.j);
}
}

class Parent9 {
public static int i = 9;
static {
System.out.println("Parent9 static block");
}
}

class Child9 extends Parent9 {
public static int j = 99;
static {
System.out.println("Child9 static block");
}
}

输出

MyTest09 static block
Parent9 static block
Child9 static block
99

我们多输出点信息

public class MyTest09 {
static {
System.out.println("MyTest09 static block");
}
public static void main(String[] args) {
Parent9 parent9; // 不会初始化
System.out.println("-------------");
parent9 = new Parent9();
System.out.println("-------------");
System.out.println(Parent9.i);
System.out.println("-------------");
System.out.println(Child9.j);

}
}

class Parent9 {
public static int i = 9;
static {
System.out.println("Parent9 static block");
}
}

class Child9 extends Parent9 {
public static int j = 99;
static {
System.out.println("Child9 static block");
}
}

输出结果

MyTest09 static block
-------------
Parent9 static block
-------------
9
-------------
Child9 static block
99

在看一个例子

public class MyTest12 {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = classLoader.loadClass("com.cuzz.jvm.classloader.CL");
System.out.println("--------------");
clazz = Class.forName("com.cuzz.jvm.classloader.CL");
}
}

class CL {
static {
System.out.println("CL static block");
}
}

输出

--------------
CL static block

说明调用 ClassLoader 类的 loadClass 方法加载一个类,并不是对类的主动使用,不会导致类的初始化,而通过 Class.forName 方法是通过反射机制,会对类初始化

类的加载

类的加载是指将类的 .class 文件中的二进制数据读入到内存中,将其运行时数据区的方法区内,然后在内存中创建一个 java.lang.Class 对象(规范中并未说明Class对象位于哪里,HotSpot 虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构

加载 .class 文件的方式

  • 从本地系统中直接加载 .class 文件
  • 通过网络下载的 .class 文件
  • 从 zip,jar 等归档文件中加载 .class 文件
  • 从专有数据库中提取 .class 文件
  • 将 Java 源文件动态编译为 .class 文件

类的加载器

类的加载器分类:

  • Java 虚拟机自带的加载器
    • 根加载器(Bootstrap)
    • 拓展类加载器(Extension)
    • 应用加载器(Application)
  • 用户自定义的类加载器
    • java.lang.ClassLoader 的子类
    • 用户定制类的加载方法

注意:类的加载并不需要等到某个类被首次主动使用时再加载它;JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载过程中遇到了 .class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类才报告错误,如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

看一个例子

public class MyTest07 {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("java.lang.String");
System.out.println(clazz.getClassLoader());

Class<?> clazz1 = Class.forName("com.cuzz.jvm.classloader.C");
System.out.println(clazz1.getClassLoader());
}
}

class C {

}

输出

null
sun.misc.Launcher$AppClassLoader@dad5dc

看看 getClassLoader 的文档

Returns the class loader for the class. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.

说明输出 null 说明 java.lang.String 是根加载器加载的

获取 ClassLoader 的途径

  • 获得当前类 ClassLoader

    clazz.getClassLoader()

  • 获得当前线程上下文的 ClassLoader

    Thread.currentThread().getContextClassLoader()

  • 获取系统的 ClassLoader

    ClassLoader.getSystemClassLoader()

  • 获取调用者的 ClassLoader

    DriverManger.getCallerClassLoader()

类的验证

类的验证的内容:

  • 类文件的结构检查
  • 语义检查
  • 字节码验证
  • 二进制兼容性的验证

类的初始化时机

当 Java 虚拟机在初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口

  • 在初始化一个类时,并不会先初始化它所实现的接口
  • 在初始化一个接口时,并不会先初始化它的父接口

因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化

JVM参数设置

非稳态选项使用说明:

  • -XX:+<option> 启用 option
  • -XX:-<option> 不启用 option
  • -XX:<option>=<number> 设定option的值为数字类型,可跟单位,例如 32k, 1024m, 2g
  • -XX:<option>=<string> 设定option的值为字符串,例如-XX:HeapDumpPath=./dump.core

Comments