一、Java 内存模型

二、垃圾回收

三、类加载流程

什么是类加载?

Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称作虚拟机的类加载机制。

类加载的时机

一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历如下七个阶段:

  • 加载(Loading);

  • 连接(Linking):

    • 验证(Verification);

    • 准备(Preparation);

    • 解析(Resolution);

  • 初始化(Initialization);

  • 使用(Using);

  • 卸载(Unloading),

其中验证、准备、解析三个部分统称为连接,发生顺序如下图所示:

《Java 虚拟机规范》并没有对“加载”这个阶段规定具体什么时候开始,但是“初始化”阶段被严格规定了有且只有下面六种情况必须立即对类进行“初始化”(而,加载、连接自然需要在此之前开始):

  • 遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令,如果类没用初始化,则先初始化;

    • 使用 new 关键字实例化对象;

    • 读取或设置一个类型的静态字段;

    • 调用一个类的静态方法;

  • 对类进行反射调用时,如果类没用初始化,则先初始化;

  • 当初始化类时,如果其父类还没有初始化,先触发父类的初始化;

  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;

  • 当使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStaticREF_putStaticREF_invokeStaticREF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化;

  • 当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

类加载的过程

加载

在加载阶段,Java 虚拟机需要完成以下三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流

  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;

  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入

验证

准备

解析

初始化

使用

卸载

方法分派

什么是方法分派?

就是确定调用谁的、哪个方法,需要针对方法重载的情况进行分析、针对方法覆写的情况进行分析。

先来看一个案例,有一个父类 SuperClass 和子类 SubClass:

public class SuperClass {
    public String getName() {
        return "Super";
    }
}

public class SubClass extends SuperClass{
    public String getName() {
        return "Sub";
    }
}

问题:下面的程序输出什么?

public class Question {
    public static void main(String[] args) {
        SuperClass obj = new SubClass();
        print(obj);
    }

    public static void print(SuperClass superClass) {
        System.out.println("Hello " + superClass.getName());
    }

    public static void print(SubClass subClass) {
        System.out.println("Hello " + subClass.getName());
    }
}

输出结果是什么?

答案是:Hello Sub。因为 Java 中的方法执行取决于运行时具体的实际类型,上述代码中 obj 实际是一个 SubClass,那么在调用 getName() 方法时,都是调用 SubClass 的 getName() 方法。

调用哪一个 print 方法呢?

如果有方法重载的情况,具体调用哪个方法取决于编译时声明的类型。上述案例中声明的类型是 SuperClass,那么 print() 方法调用的就是 print(SuperClass superClass),不会受到运行时的影响。

四、泛型

泛型编译后是什么?

在 Java 中泛型采用类型擦除实现,泛型类型编译时被擦除为 Object,不兼容基本类型。

为什么采用泛型擦除?

Java 的泛型从 JDK1.5 才出现,在此之前使用 List、ArrayList 时都是直接使用原始类型的。Java 是出名的兼容性好,为了向前兼容,也只能采用泛型擦除的方案。

采用泛型擦除还有个好处就是运行时内存负担小,Java 的类型在运行时都需要加载到方法区的,由于编译后泛型被擦除了,使用的是原始类型,运行时对方法区的内存负担自然就小。

Java 的泛型有哪些缺陷?

基本类型无法作为泛型实参

基本类型无法作为泛型实参,只能使用 Integer、Long 等,不可避免就有装箱拆箱的开销;

List<int> list1 = new ArrayList<>();     // ❌ 基本类型无法作为泛型类型
List<Integer> list2 = new ArrayList<>(); // ✔️ 需要使用装箱类型

泛型类型无法用作方法重载

泛型类型无法用作方法重载。原因很明显,因为编译后大家都是 List 的原始类型,两个方法的签名是一样的,当然无法重载。

public void print(List<Integer> list) {}
public void print(List<String> list) {}  // ❌ 二者无法重载

泛型类型无法作为真实类型使用

泛型类型无法作为真实类型使用。下面是 Java 中的泛型方法:

public <T> void genericMethod(T t) {}

其中的泛型参数 T t 编译完之后实际上是一个 Object:

public void genericMethod(Object a) {}

因此,在运行时并不知道其真实的类型是什么。所以 Java 中的泛型无法作为真实类型使用,有诸多的限制:

  • 无法实例化:T newInstance = new T();

  • 无法创建数组:T[] arr = new T[10];

  • 无法获取类对象:Class c = T.class;

  • 无法判断泛型:if (list instanceof List<String>),只能判断是否是 List,而不能判断内部的泛型类型

  • 可以用作其他泛型:List<T> list = new ArrayList<>();,因为最后都会被擦除。

静态方法无法引用类泛型参数

类型强制的运行时开销

虽然我们使用了泛型在写代码的过程中可以确定一个 List 中的具体类型,但是编译后 List 中存储的都是 Object,那么是怎么返回正确的类型呢?

List<String> list = new ArrayList<>();
list.add("Hello");
String val = list.get(0);

实际上是编译器在编译后做了类型强转:

invokeinterface 'java/util/List.get','(I)Ljava/lang/Object;'
checkcast 'java/lang/String'

因此,Java 的泛型会带来类型强转的运行时开销

泛型签名

其实泛型类型的签名信息在编译后被保留在字节码中,可以通过反射获取。

LOCALVARIABLE list Ljava/util/List; L1 L2 1
// signature Ljava/util/List<Ljava/lang/String;>;
// declaration: list extends java.util.List<java.lang.String>