2016-11-5

Class 生命周期

1. 加载(loading)

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

2. 链接(Linking)

链接包括: 验证(Verification)、准备(Preparation)、解析(Resolution)

  • 验证(Verification) 确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束要求,保证这些信息被当作代码不会危害虚拟机的自身安全。

  • 准备(Preparation) 准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。

这时候进行内存分配的仅包括类变量 这里所说的初始值“通常情况”下是数据类型的零值。

假设一个类变量的定义为:public static int value = 123; 那变量 value 在准备阶段过后的初始值为 0 而不是123,因为这时尚未开始执行任何java方法,而把value赋值为123的 putstatic 指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。

  • 解析(Resolution) 解析阶段是java虚拟机将常量池内的符号引用替换为直接引用的过程
  1. 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。 符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。

  2. 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。 直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

什么是符号? 比如方法,变量,在解析之前仅作为一个符号/指针指向, 并不可以使用,若想要能用调用需要真正加载到内存 定位到其位置。

3. 初始化

在该阶段,才真正意义上的开始执行 java 程序代码,执行类构造器,并且在Java虚拟机规范中有明确的规定

在下面5种情况下必须对类进行初始化: 遇到 new、getstatic、putstatic、invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

4. 使用

5. 卸载

满足如下条件:

  1. 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

什么时候加载 Class?

加载阶段,Java虚拟机规范中没有进行约束。

什么时候初始化 Class?

初始化阶段,Java虚拟机规范中有严格的约束。

下面5种情况必须立即进行初始化:

  1. 创建类的实例,也就是new一个对象;获取或者赋值类的静态变量、静态非字面值常量(静态字面值常量除外)。
  2. 调用类的静态方法。
  3. 通过反射调用(Class.forName(“xxx”))。
  4. 初始化一个类的子类(会首先初始化子类的父类)。
  5. 启动程序所使用的main方法所在类。

上面的5中情况称为主动引用,除此主动引用之外还有被动引用。

被动引用有如下3种常见情况:

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  2. 定义对象数组和集合,不会触发该类的初始化。
  3. 类A引用类B的静态常量不会导致类B初始化(注意静态常量必须是字面值常量,否则还是会触发B的初始化)。

序列化

在什么情况下需要使用到 Serializable 接口呢?

  1. 当想把的内存中的对象状态保存到一个文件中或者数据库中时候;
  2. 当想用套接字在网络上传送对象的时候;
  3. 当想通过RMI传输对象的时候

serialVersionUID

serialVersionUID 的取值是Java运行时环境根据类的内部细节自动生成的; 如果对类的源代码作了修改, 再重新编译, 新生成的类文件的serialVersionUID的取值有可能也会发生变化; 类的serialVersionUID的默认值完全依赖于Java编译器的实现, 对于同一个类, 用不同的Java编译器编译, 有可能会导致不同的serialVersionUID, 也有可能相同; 为了提高serialVersionUID的独立性和确定性, 强烈建议在一个可序列化类中显示的定义serialVersionUID, 为它赋予明确的值; 显式地定义serialVersionUID有两种用途: a. 在某些场合, 希望类的不同版本对序列化兼容, 因此需要确保类的不同版本具有相同的serialVersionUID; b. 在某些场合, 不希望类的不同版本对序列化兼容, 因此需要确保类的不同版本具有不同的serialVersionUID;

简而言之 用作序列化的版本标识, 确定是否兼容

通过 ObjectOutputStream 调用 writeObject()方法就可以将对象序列化 调用 readObject()方法就可以将对象反序列化

注意一点

a 当一个父类实现序列化, 子类自动实现序列化, 不需要显式实现Serializable接口; b 当一个对象的实例变量引用其他对象, 序列化该对象时也把引用对象进行序列化; c static,transient 后的变量不能被序列化

强/软引用

从JDK 1.2版本开始, 把对象的引用分为4种级别, 从而使程序能更加灵活地控制对象的生命周期; 这4种级别由高到低依次为: 强引用, 软引用, 弱引用和虚引用;

强引用

只要引用存在, 垃圾回收器永远不会回收

Object obj = new Object();

而这样 obj对象对后面new Object的一个强引用, 只有当obj这个引用被释放之后, 对象才会被释放掉, 这也是我们经常所用到的编码形式;

软引用

非必须引用, 内存溢出之前进行回收, 可以通过以下代码实现

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null

这时候sf是对obj的一个软引用, 通过sf.get()方法可以取到这个对象, 当然, 当这个对象被标记为需要回收的对象时, 则返回null; 软引用主要用户实现类似缓存的功能, 在内存足够的情况下直接通过软引用取值, 无需从繁忙的真实来源查询数据, 提升速度; 当内存不足时, 自动删除这部分缓存数据, 从真正的来源查询这些数据;

弱引用

第二次垃圾回收时回收, 通常用于调试

弱引用是在第二次垃圾回收时回收, 短时间内通过弱引用取对应的数据, 可以取到, 当执行过第二次垃圾回收时, 将返回null; 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾, 可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记;

虚引用

垃圾回收时回收, 无法通过引用取到对象值, 可以通过如下代码实现 虚引用是每次垃圾回收的时候都会被回收, 通过虚引用的get方法永远获取到的数据为null, 因此也被成为幽灵引用; 虚引用主要用于检测对象是否已经从内存中删除;

Java 自举

参考 动态编译class

Java的编译器是javac, 但是, 在很早很早的时候,Java的编译器就已经用纯Java重写了, 自己能编译自己, 行业黑话叫”自举”; 从Java 1.6开始, 编译器接口正式放到JDK的公开API中, 于是, 我们不需要创建新的进程来调用javac, 而是直接使用编译器API来编译源码;

用法

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int compilationResult = compiler.run(null, null, null, '/path/to/Test.java');

ServiceLoader

ServiceLoader 是Java提供的一套SPI(Service Provider Interface,常译:服务发现)框架,用于实现服务提供方与服务使用方解耦 TODO