sayi.github.com icon indicating copy to clipboard operation
sayi.github.com copied to clipboard

JVM(三)类和对象的生命周期

Open Sayi opened this issue 7 years ago • 0 comments

本文从虚拟机加载和执行层面探讨下类或者接口(下文统一使用类)、对象的生命周期。

命令行编译和执行代码

在初学Java的时候很多人都学过在没有IDE的情况下编译(javac)和执行(java)代码,后来你会彻底爱上IDE,它可以自动编译和管理依赖,你只需要运行代码就行了,我们这里先来回顾下如何通过命令行编译和执行代码,项目目录结构如下:

+-| com
|----+- deepoove
|-----------+- java8
|---------------+- def
|-------------------\- Apple.java
|---------------\- ClassLoaderTest.java

我们写个非常简单的类ClassLoaderTest,引用了Gson类和目录下面的Apple类:

package com.deepoove.java8;

import com.deepoove.java8.def.Apple;
import com.google.gson.Gson;

public class ClassLoaderTest {

  public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    System.out.println("Apple.class: " + Apple.class.getClassLoader());

    Apple[] array = new Apple[5];
    System.out.println(array.getClass() + ": " + array.getClass().getClassLoader());

    ClassLoader classLoader = Gson.class.getClassLoader();
    System.out.println("Gson.class classloader:");
    while (null != classLoader) {
      System.out.println("\t" + classLoader);
      classLoader = classLoader.getParent();
    }

    System.out.println("String.class: " + String.class.getClassLoader());
  }
}

接下来就可以进行编译了,我们进入com同级目录下执行javac编译命令:

javac -sourcepath . -classpath /Users/Sayi/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.5/de8829/gson-2.8.5.jar com/deepoove/java8/ClassLoaderTest.java

-sourcepath指定源代码路径,-classpath指定了依赖类的路径,命令执行成功后,会在ClassLoaderTest.java同级目录下,生成字节码文件ClassLoaderTest.class。

通过java命令可以执行字节码文件了:

$ java -classpath .:/Users/Sayi/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.5/de8829/gson-2.8.5.jar com/deepoove/java8/ClassLoaderTest
Apple.class: sun.misc.Launcher$AppClassLoader@2a139a55
class [Lcom.deepoove.java8.def.Apple;: sun.misc.Launcher$AppClassLoader@2a139a55
Gson.class classloader:
	sun.misc.Launcher$AppClassLoader@2a139a55
	sun.misc.Launcher$ExtClassLoader@4aa298b7
String.class: null

注意的是,在执行字节码时,classpath路径多了一个点和冒号: .:,这就和类加载机制有关系了,会尝试从当前目录和gson.jar寻找并加载类,java命令会把用户类路径存储在java.class.path属性,可以通过查看这个属性看到应用从哪些路径加载类,这个属性的配置可能来自于:

  1. 默认值是 '.', 表示当前目录或者子目录下寻找所有的字节码类文件
  2. 通过系统环境变量CLASSPATH改写默认值
  3. 通过-classpath 或者 -cp 改写默认值和CLASSPATH值

因为路径会被改写,所以在执行代码时 . 和 gson.jar 都需要在-classpath中指定。

字节码文件和javap

代码执行过程是首先从字节码加载类,然后从一个static main方法为入口执行。字节码文件是一个类的二进制表示,我们需要理解它的结构才能很好的阅读,同时也有很多框架提供了字节码操纵:cglib、javassist等,我们可以通过javap命令反编译字节码文件查看:

Classfile /Users/Sayi/reflections-example/bin/com/deepoove/example/Reader.class
  Last modified 2018-11-19; size 435 bytes
  MD5 checksum eb2714fb2d57cda321faf74167890254
  Compiled from "Reader.java"
public interface com.deepoove.example.Reader
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
   #1 = Class              #2             // com/deepoove/example/Reader
   #2 = Utf8               com/deepoove/example/Reader
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               read
   #6 = Utf8               (Ljava/io/File;)Ljava/util/List;
   #7 = Utf8               Signature
   #8 = Utf8               (Ljava/io/File;)Ljava/util/List<Ljava/lang/String;>;
   #9 = Utf8               url
  #10 = Utf8               MethodParameters
  #11 = Utf8               getCode
  #12 = Utf8               ()I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/deepoove/example/Reader;
  #18 = Utf8               SourceFile
  #19 = Utf8               Reader.java
{
  public abstract java.util.List<java.lang.String> read(java.io.File);
    descriptor: (Ljava/io/File;)Ljava/util/List;
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #8                           // (Ljava/io/File;)Ljava/util/List<Ljava/lang/String;>;
    MethodParameters:
      Name                           Flags
      url

  public int getCode();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush        100
         2: ireturn
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lcom/deepoove/example/Reader;
}
SourceFile: "Reader.java"

字节码二进制文件中并不是所有字段都是存在的,比如MethodParameters(需要在编译时加上-parameter参数)、LocalVariableTable(需要在编译时加上-g或者-g:vars)等, 如果有些功能依赖于字节码中的这些字段,那么就要确保在编译的时候加上合适的参数。

类的生命周期

类从字节码文件被加载开始到使用,直至最后被卸载是它完整的生命周期。

image

类在被Link前,一定会完全被Load;类在被Initialization前一定会完全被verification和Preparation,Resolution是一个可选的阶段。

下面对这些步骤作一个简单介绍。

Load 加载

Load涉及到通过一个binary name和类加载器将一个二进制数据(classFile)分配到方法区,构造Class对象的过程,这个过程是由ClassLoader和其子类实现的。

binary name是一个类的二进制名称,比如java.security.KeyStore$Builder$FileBuilder$1

在Load过程中,可能发生的异常有:

  1. ClassCircularityError:A class or interface could not be loaded because it would be its own superclass or superinterface
  2. ClassFormatError:二进制数据格式错误
  3. NoClassDefFoundError、ClassNotFoundException:找不到关联的Class的定义
  4. OutOfMemoryError:因为涉及到方法区的分配,所以可能会导致内存溢出

Link 链接

链接主要分为三步:verification、Preparation和Resolution,在链接的过程,涉及到内存分配,有可能会抛出OutOfMemoryError。

verification
verification是对字节码文件是否满足一些限制的验证,这一步可能会导致其它的类被Load,但是其他类不是必须要被verification。

当校验无法通过时,会抛出VerifyError异常。

Preparation
对类的静态字段进行创建和初始化默认值,注意的是,明确的对类的一个静态字段赋值是Initialization的一部分,不属于Preparation

Resolution(Optional)
Resolution是决定类对它引用的其它类或者接口符号引用的过程。可能会抛出如下异常:

  1. IllegalAccessError:没有权限引用
  2. InstantiationError:抽象类无法实例化
  3. NoSuchFieldError:找不到属性
  4. NoSuchMethodError:找不到方法
  5. IncompatibleClassChangeError:不兼容的类改变

Initialization 初始化

当调用了静态方法、字段的或者明确创建对象、利用反射API等场景都会触发类的初始化,执行类的静态初始化方法。

Unloading 卸载

一个类可能会被卸载,当且仅当它的加载器被回收,注意的是,bootstrap class loader加载的类不会被卸载。

对象的生命周期

对象从被实例化到使用,直至最后被垃圾回收是它完整的生命周期。

image

instantiation and initialization 实例化和初始化

一个对象可能会被显式或者隐式创建,比如new操作符,比如lambda表达式,字符串常量和装箱等。

对象创建都会涉及到内存分配。

Garbage collection and Finalization of Objects 回收

对象在不可达时会被垃圾回收标记,在第一次标记时会调用每个对象的protected void finalize() throws Throwable { }方法,这是被垃圾回收器自动调用的,在进入第二次标记时,这个对象将会被回收。

注意的是,finalize方法有且仅会被调用一次,我们也可以重写finalize方法,将当前对象重新挂到引用链时从而避免被垃圾回收,但是第二次不可达时则会直接被回收,不会再执行finalize方法。

Class.forName vs ClassLoader

我们已经知道了类从加载到初始化直至被卸载的流程,类加载过程是由ClassLoader实现,JDK还提供了Class.forName加载类获得Class对象,它们有什么区别呢?

数组加载

数组不是由类加载器加载的,它们是在需要的时候被虚拟机自动创建。但是每个数组类里面都有一个ClassLoader的引用,这个值和数组的元素类型的ClassLoader是相同的,如果是原生类型,则这个CLassLoader为空。

我们可以换个角度思考,由于CLassLoader是需要binary name,而数组其实是没有一个二进制名称的,所以无法通过ClassLoader加载,但是数组有一个代表数组类的名称,所以可以通过CLass.forName去加载:

Class<?> forName = Class.forName("[Lcom.deepoove.java8.def.Apple;");

上面这串代码就加在了Apple[]数组类型,由于没有指定维度,所以是无法创建实例的,数组初始化可以使用反射包下的java.lang.reflect.Array.newArray(Class<?>, int)来创建数组实例。

JDBC驱动类加载

学过JDBC的人都知道第一行代码就是加载JDBC驱动:

Class.forName("com.mysql.jdbc.Driver");

从含义上来说,它真的是加载驱动吗?如果只是加载这个类,只要这个类在用户路径下何须手动加载,即使要加载,ClassLoader就可以完成加载?

这里就涉及到类的生命周期和ClassLoader内在实现问题了,ClassLoader加载的类并不会进行Initialization初始化阶段,而Class.forName("")会进行类的初始化,调用Driver类相关的静态初始化方法,而这些初始化方法的代码才是加载JDBC驱动真正要做的事情,我们来看看Class.forName的源码:

public static Class<?> forName(String className) throws ClassNotFoundException {
  Class<?> caller = Reflection.getCallerClass();
  return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

private static native Class<?> forName0(String name, boolean initialize,
                     ClassLoader loader,
                     Class<?> caller) throws ClassNotFoundException;

看到native方法支持initialize和ClassLoader参数,initialize为true则会进行类的初始化。

扩展:JDBC 4.0 以后已经不需要通过Class.forName加载并初始化驱动类了,通过SPI机制实现了加载和初始化。

参考资料

总结

我们知道应用程序如何加载类以及初始化的时机,我们学到了字节码文件从加载到执行的整个过程,理解类和对象的生命周期,可以帮助我们更好的理解问题和优化我们的程序。

下一篇文章,我们研究下类加载机制。

Sayi avatar Dec 07 '18 08:12 Sayi