上一篇简单记录了Java class文件的格式,它以标准的二进制形式来表现Java类型。本篇咱们来看下当二进制的类型数据被导入到和Java虚拟机中时,到底会发生什么。咱们以一个Java类型(类或接口)的生命周期(从进入虚拟机开始到最终退出)为例来讨论开始阶段的装载、链接和初始化,以及占Java类型生命周期绝大部分时间的对象实例化、垃圾收集和对象finalize,而后是Java类型生命周期的结束(从虚拟机中卸载)java
Java虚拟机经过装载
、链接
和初始化
一个Java类型,使该类型能够被正在运行的Java程序所使用。网络
装载
---就是把二进制形式的Java类型读入Java虚拟机中链接
---就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。链接分为三个子步骤:
初始化
---给类变量赋予正确的初始值。总体流程以下:数据结构
如图所示,装载、链接和初始化这三个阶段必须按顺序进行。惟一例外的就是链接阶段的第三步(解析),它能够在初始化以后再进行。app
在类和接口被装载和链接的时机上,Java虚拟机规范对具体实现提供了必定的灵活性。可是规范对于初始化的时机有着严格的规定。全部Java虚拟机实现必须在每一个类或接口首次主动使用时初始化
。下面6种情形符合主动使用的情形。dom
除以上6种情形外,全部其余使用 Java 类型的方式都是被动使用,它们都不会致使 Java 类型的初始化。jvm
第五条中任何一个类的初始化都要求它的超类在此以前完成初始化。然而对于接口来讲,这条规则并不适用。只有在某个接口所声明的非final字段被使用时,该接口才会被初始化。函数
首次主动使用时初始化
这个规则直接影响着类的装载、链接和初始化的机制。虚拟机实现能够自由选择装载、链接的时机。但不管如何,若是一个类型在它首次主动使用以前尚未被装载和链接的话,那它必须在此时被装载和链接,这样它才能被初始化。性能
装载动做由三个基本动做组成,要装载一个类型,Java虚拟机必须:学习
java.lang.Class
类的实例Java虚拟机并无说Java类型的二进制数据应该怎样产生。因此咱们能够想象这几种场景:ui
有了二进制数据后,Java虚拟机必须对这些数据进行足够的处理,而后才能建立类java.lang.Class
的实例对象。而装载步骤的最终产品就是这个Class类的实例对象,它成为Java程序与内部数据结构之间的接口。要访问关于该类型的信息,程序就要调用该类型对应的Class实例对象的方法。
前面讲过Java类型的装载要么由启动类装载器装载,要么由用户自定义的类装载器装载。而在装载过程当中遇到问题,类装载器应该在程序首次使用时报告问题。若是这个类一直没有被程序主动使用,那么该类装载器不该该报告错误。
当类型被装载后,就准备进行链接了。链接过程的第一步是验证:确认类型符合Java语言的语义,而且不会危及虚拟机的完整性。
在验证上,不一样虚拟机的实现可能不太同样。但虚拟机规范列出了虚拟机能够抛出的异常以及在何种条件下必须抛出它们。
而在装载
过程当中,也可能会作如下几种数据检查(虽然这些检查在装载期间完成,在正式的链接验证阶段以前进行,但在逻辑上属于验证阶段。检查被装载类型是否有任何问题的过程都属于验证):
Object
以外的每个类都有一个超类。在大多数虚拟机的实现中,还有一种检查每每发生在正式的验证阶段以后,那就是符号引用的验证。
前面讲过动态链接的过程包括经过保存在常量池汇总的符号引用查找被引用的类、接口、字段以及方法,把符号引用替换为直接引用。
当虚拟机搜寻一个被符号引用的元素(类型、方法)时,它必须首先确认该元素存在。若是该元素存在,还要进一步检查引用类型是否有访问该元素的权限。这些存在性和访问权限的检查在逻辑上属于链接的第一阶段,可是每每在链接的第三阶段解析
的时候发生。而解析
自身也可能延迟到符号引用第一次被程序使用的时候,因此这些检查设置可能在初始化以后才进行。
任何在此以前没有进行的检查以及在此以后不会被检查的项目都包含在内。
请注意,当须要查看其余类型时,它只须要查看超类型。超类须要在子类初始化
前被初始化
,因此这些类应该已经被装载了。而对于接口的初始化
来讲,不须要父接口的初始化
,可是当子接口被装载
时,父接口须要被装载
(它们不会被初始化,只是被装载
了,有些虚拟机实现也可能会进行链接
的操做)
CONSTANT_String_info
入口的 string_index项目必须是CONSTANT_Utf8_info
入口的索引)全部的Java虚拟机都必须设法为它们执行的每一个方法检验字节码的完整性。好比,不能由于超出了方法末尾的跳转指令而致使虚拟机的崩溃,虚拟机必须在字节码验证的时候检查出这样的跳转指令是非法的,从而抛出一个错误。
虚拟机的实现并无强求在正式的链接验证阶段进行字节码验证,因此虚拟机能够选择在执行每条语句的时候单独进行验证。然而Java虚拟机指令集设计的一个目标就是使得字节码流可使用一个数据流分析器一次验证,而不用在程序执行时动态验证,对速度的提高有很大帮助。
随着Java虚拟机装载了一个类,并执行了一些它选择进行的验证后,类就能够进入准备阶段了。在准备阶段,Java 虚拟机为类变量分配内存,设置默认初始值。但在达到初始化
以前,类变量都没有被设置为真正的初始值(代码里声明的)。准备阶段是不会执行 Java 代码的
在准备阶段,虚拟机给类变量的默认初始值以下表(有木有感受跟 C 的默认数据类型很像)
类型 | 默认初始值 |
---|---|
int | 0 |
long | 0L |
short | 0 |
char | "\u0000" |
byte | 0 |
boolean | false |
reference | null |
float | 0.0f |
double | 0.0d |
Java 虚拟机一般把 boolean 实现为一个 int,会被默认赋值为0(对应 false)
在准备阶段,Java 虚拟机实现可能也为一些数据结构分配内存,目的是为了提升运行程序的性能。这种数据结构好比方法表,它包含指向类中每个方法(包括从父类继承的方法)的指针。方法表能够在执行继承的方法时不须要搜索父类。
Java 类型通过验证和准备以后,就能够进入解析阶段了。解析的过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,并把这些符号引用转换成直接引用的过程。
本篇要从宏观角度看待生命周期,解析过程先简单描述下,后面作详细介绍
为了准备让一个类或接口被首次主动使用,最后一个步骤就是初始化。初始化就是为类变量赋予正确的初始值(就是代码里指定的数值)。
在 Java 代码中,一个正确的初始值是经过类变量初始化语句或者静态初始化语句给出的。
类变量初始化语句(组成:=、表达式):
class ExampleA{
static int size = (int) (3 * Math.random());
}
复制代码
静态初始化语句(组成:static 代码块):
class ExampleB{
static int size ;
static {
size = (int) (3 * Math.random());
}
}
复制代码
全部的类变量初始化语句和类型的静态初始化器都被 Java 编译器收集在一块儿,放到一个特殊的方法中。在类和接口的 class 文件中,这个方法被称为<clinit>
。一般的 Java 程序方法是没法调用这个<clinit>
方法的。这种方法只能被虚拟机调用,专门用来把类型的静态变量设置为它们的正确值。
初始化一个类包含两个步骤:
而接口的初始化并不须要初始化它的父接口;若是接口存在一个初始化方法的话,就执行此方法。
<clinit>
方法的代码并不会显式调用超类的<clinit>
。在Java虚拟机调用类的<clinit>
方法以前,它必须确认超类的<clinit>
方法已经被执行了。
<clinit>
方法前面讲到Java 编译器把类变量初始化语句和静态初始化代码块都放到 class 文件的<clinit>
方法中,顺序就按照在类或接口中声明的顺序。
以下类:
class ExampleB{
static int width ;
static int height = 4 * ExampleA.random();
static {
width = 9 * ExampleA.random() + 10;
}
}
复制代码
Java 编译器生成了以下<clinit>
方法
static <clinit>()V
L0
LINENUMBER 14 L0
ICONST_4
INVOKESTATIC hua/lee/jvm/ExampleA.random ()I
IMUL
PUTSTATIC hua/lee/jvm/ExampleB.height : I
L1
LINENUMBER 16 L1
BIPUSH 9
INVOKESTATIC hua/lee/jvm/ExampleA.random ()I
IMUL
BIPUSH 10
IADD
PUTSTATIC hua/lee/jvm/ExampleB.width : I
L2
LINENUMBER 17 L2
RETURN
MAXSTACK = 2
MAXLOCALS = 0
复制代码
并不是全部的类编译后都存在<clinit>
方法:
<clinit>
方法。<clinit>
方法。<clinit>
方法。下面的类是不会执行<clinit>
方法的。
class ExampleB{
static final int angle = 10;
static final int length = angle * 2;
}
复制代码
ExampleB 声明了两个常量angle
和 length
,并经过表达式赋予了初始值,这些表达式是编译时常量。编译器知道angle
表示十、length
表示20。因此在ExampleB被装载时,angle
和 length
并无做为类变量保存在方法区中,它们是常量,被编译器特殊处理了。
angle
和 length
做为常量,Java 虚拟机在使用它们的全部类的常量池或者字节码流中直接存放的是它们表示的int 数值。好比,若是一个类A
使用了 Example 的angle
字段,在编译时虚拟机不会在类 A
的常量池中保存一个指向Example类 angle
的符号引用,而是直接在类 A
的字节码流中嵌入一个值10
。若是angle
的常量值超过了 short
范围限制,好比 angle=40000
,那么类会将它保存在常量池的 CONSTANT_Integer中,值为40000。
而对于接口来讲,咱们能够关注下面这个代码
interface ExampleI{
int ketch = 9;
int mus = (int) (Math.random()*10);
}
复制代码
编译后的<clinit>
方法为
static <clinit>()V
L0
LINENUMBER 22 L0
INVOKESTATIC java/lang/Math.random ()D
LDC 10.0
DMUL
D2I
PUTSTATIC hua/lee/jvm/ExampleI.mus : I
RETURN
MAXSTACK = 4
MAXLOCALS = 0
复制代码
请注意,只有mus
被<clinit>
初始化了。由于ketch
字段被初始化为了一个编译时常量,被编译器特殊处理了。和类中的逻辑同样,对于其余引用到mus
的类,编译器会保存指向这个字段的符号引用;而对于引用ketch
的类,会在编译时替换为常量值。
主动使用
和被动使用
前面说过,Java 虚拟机在首次使用类型时初始化它们。只有6种活动被认为是主动使用:
使用一个非 final 的静态字段只有当类或者接口明确声明了这个字段时才是主动使用。好比,父类种声明的字段可能会被子类引用;接口中声明的字段可能会被实现者引用。对于子类、子接口和接口实现类来讲,这就是被动使用。而被动调用并不会触发子类(调用字段的类)的初始化。
示例:
class NewParent{
static int hoursOfSleep = (int) (Math.random() * 3);
static{
System.out.println("NewParent was initialized.");
}
}
class NewKid extends NewParent{
static int hoursOfCrying = 6+(int) (Math.random() * 2);
static{
System.out.println("NewKid was initialized.");
}
}
public class Test {
public static void main(String[] args) {
int h = NewKid.hoursOfSleep;
System.out.println(h);
}
static{
System.out.println("MethodTest was initialized.");
}
}
复制代码
输出以下:
MethodTest was initialized.
NewParent was initialized.
2
复制代码
从log看,执行Test
的main
方法只会致使Test
和NewParent
的初始化,NewKid
没有被初始化。
若是一个字段既是static
的又是final
的,而且使用一个编译时常量表达式初始化,使用这样的字段,也不是对声明该字段类的主动使用。
看下面的代码:
interface Angry {
String greeting = "Grrrr!";//常量表达式
int angerLevel = Dog.getAngerLevel();//很是量表达式,会打包进<clinit>方法,并且调用DOG的静态方法,主动使用。
}
class Dog{
static final String greeting = "woof woof world";
static{
System.out.println("Dog was initialized");
}
/** * 静态方法 */
static int getAngerLevel(){
System.out.println("Angry was initialized");
return 1;
}
}
class Example01{
public static void main(String[] args) {
System.out.println(Angry.greeting);
System.out.println(Dog.greeting);
}
static{
System.out.println("Example was initialized");
}
}
class Example02{
public static void main(String[] args) {
System.out.println(Angry.angerLevel);
}
static{
System.out.println("Example was initialized");
}
}
复制代码
Example01 只是引用了静态常量(常量表达式形式的初始化)Angry.greeting
和Dog.greeting
,因此编译时已经替换为了实际数值,不属于主动使用,不会初始化对应的类。
Example01 的输出:
Example01 was initialized
Grrrr!
woof woof world
复制代码
Example02引用了Angry.angerLevel
,虽然是静态常量,可是是经过方法调用的方式Dog.getAngerLevel()
初始化数值,属于主动使用Angry
。而Angry
调用了Dog
的静态方法getAngerLevel()
,属于主动使用Dog
。
Example02 的输出:
Example02 was initialized
Dog was initialized
Angry was initialized
1
woof woof world
复制代码
一旦一个类被装载
、链接
和初始化
。它就随时可用了。程序访问它的静态字段,调用它的静态方法,或者建立它的实例。
在Java程序中,类能够被明确或者隐含的实例化。明确的实例化一个类有四种途径:
new
操做符Class
或java.lang.reflect.Constructor
对象的newInstance()
clone()
方法java.io.ObjectInputStream
类的getObject()
方法反序列化另外存在下面几种隐含实例化类的方式:
以下代码示例:
class Example{
public static void main(String[] args) {
if (args.length != 2){
System.out.println("illegal args");
return;
}
System.out.println(args[0] + args[1]);
}
static{
System.out.println("Example was initialized");
}
}
复制代码
字节码内容:
public static main([Ljava/lang/String;)V L0 LINENUMBER 20 L0 ALOAD 0 ARRAYLENGTH ICONST_2 IF_ICMPEQ L1 L2 LINENUMBER 21 L2 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "illegal args"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L3
LINENUMBER 22 L3
RETURN
L1
LINENUMBER 24 L1
FRAME SAME
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 0
ICONST_0
AALOAD
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
ICONST_1
AALOAD
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 25 L4
RETURN
L5
LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
MAXSTACK = 4
MAXLOCALS = 1
复制代码
请关注 args[0] + args[1]
,编译器会建立StringBuilder
实例,经过 StringBuilder.append
链接,再经过 StringBuilder.toString
转成 String
对象
当 Java 虚拟机建立一个类的新实例时,无论明确的仍是隐含的,首先都须要在堆中为保存对象的实例变量分配内存,包括在当前类和它的超类中所声明的变量。一旦虚拟机为新的对象准备好了堆内存,它当即把实例变量初始化为默认初始值(虚拟机默认值)。随后才会为实例变量赋予正确的初始值(码农指望的)。
Java 编译器为它编译的每个类都至少生成一个实例初始化方法(构造函数)。在 Java class文件种,实例初始化方法被称为<init>
。针对源码中每个类的构造方法,Java 编译器都产生一个对应的<init>
方法。若是类没有明确声明构造方法,编译器默认产生一个无参构造方法。
构造方法代码示例:
class ExampleCons{
private int width = 3;
public ExampleCons() {
this(1);
System.out.println("ExampleCons(),width = " + width);
}
public ExampleCons(int width) {
this.width = width;
System.out.println("ExampleCons(int),width = " + width);
}
public ExampleCons(String msg) {
super();
System.out.println("ExampleCons(String),width = " + width);
System.out.println(msg);
}
public static void main(String[] args) {
String msg = "Test Constructor MSG";
ExampleCons one = new ExampleCons();
ExampleCons two = new ExampleCons(2);
ExampleCons three = new ExampleCons(msg);
}
}
复制代码
控制台输出:
ExampleCons(int),width = 1
ExampleCons(),width = 1
ExampleCons(int),width = 2
ExampleCons(String),width = 3
Test Constructor MSG
复制代码
而从字节码上看,全部的<init>
方法都默认执行了父类的无参构造方法:
// class version 52.0 (52)
// access flags 0x20
class hua/lee/jvm/ExampleCons {
// compiled from: Angry.java
// access flags 0x2
private I width
// access flags 0x1
public <init>()V
L0
LINENUMBER 39 L0
ALOAD 0
ICONST_1
INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> (I)V
L1
LINENUMBER 40 L1
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "ExampleCons(),width = "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
GETFIELD hua/lee/jvm/ExampleCons.width : I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L2
LINENUMBER 41 L2
RETURN
L3
LOCALVARIABLE this Lhua/lee/jvm/ExampleCons; L0 L3 0
MAXSTACK = 3
MAXLOCALS = 1
// access flags 0x1
public <init>(I)V
L0
LINENUMBER 43 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 36 L1
ALOAD 0
ICONST_3
PUTFIELD hua/lee/jvm/ExampleCons.width : I
L2
LINENUMBER 44 L2
ALOAD 0
ILOAD 1
PUTFIELD hua/lee/jvm/ExampleCons.width : I
L3
LINENUMBER 45 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "ExampleCons(int),width = "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 46 L4
RETURN
L5
LOCALVARIABLE this Lhua/lee/jvm/ExampleCons; L0 L5 0
LOCALVARIABLE width I L0 L5 1
MAXSTACK = 3
MAXLOCALS = 2
// access flags 0x1
public <init>(Ljava/lang/String;)V
L0
LINENUMBER 48 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 36 L1
ALOAD 0
ICONST_3
PUTFIELD hua/lee/jvm/ExampleCons.width : I
L2
LINENUMBER 49 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "ExampleCons(String),width = "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
GETFIELD hua/lee/jvm/ExampleCons.width : I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L3
LINENUMBER 50 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 1
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 51 L4
RETURN
L5
LOCALVARIABLE this Lhua/lee/jvm/ExampleCons; L0 L5 0
LOCALVARIABLE msg Ljava/lang/String; L0 L5 1
MAXSTACK = 3
MAXLOCALS = 2
// access flags 0x9
public static main([Ljava/lang/String;)V L0 LINENUMBER 54 L0 LDC "Test Constructor MSG" ASTORE 1 L1 LINENUMBER 55 L1 NEW hua/lee/jvm/ExampleCons DUP INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> ()V ASTORE 2 L2 LINENUMBER 56 L2 NEW hua/lee/jvm/ExampleCons DUP ICONST_2 INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> (I)V ASTORE 3 L3 LINENUMBER 57 L3 NEW hua/lee/jvm/ExampleCons DUP ALOAD 1 INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> (Ljava/lang/String;)V ASTORE 4 L4 LINENUMBER 58 L4 RETURN L5 LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
LOCALVARIABLE msg Ljava/lang/String; L1 L5 1
LOCALVARIABLE one Lhua/lee/jvm/ExampleCons; L2 L5 2
LOCALVARIABLE two Lhua/lee/jvm/ExampleCons; L3 L5 3
LOCALVARIABLE three Lhua/lee/jvm/ExampleCons; L4 L5 4
MAXSTACK = 3
MAXLOCALS = 5
}
复制代码
对于Java程序来讲,程序能够明确或隐含的为对象分配内存,可是不能明确的释放内存。咱们前面讲过,Java 虚拟机的实现应该具备某种自动堆储存管理策略,而大部分采用垃圾收集器。当一个对象再也不被程序所引用了,虚拟机必须回收那部份内存。
若是类声明了一个fianlize()
的方法,垃圾回收器会在释放这个实例占据的内存空间前执行这个方法。而垃圾收集器针对一个对象只会调用一次fianlize()
。若是执行fianlize()
期间对象被从新引用(复活了),随后又不被引用,垃圾收集器也不会再次执行fianlize()
方法。
垃圾收集器篇幅较长,本篇还是以生命周期为主线,后面详细记录
Java 类的生命周期和对象的生命周期很像。
类的垃圾收集和卸载之因此在虚拟机种很重要,是由于 Java 程序能够在运行时经过用户自定义的类装载器装载类型来动态地扩展程序。多有被装载嗯类型都在方法区占据内存空间。若是Java 程序持续经过用户自定义的类装载器装载类型,方法区的内存足迹就会不断增加。若是某些动态装载的类型只是临时须要,当他们再也不被引用以后,占据的空间能够经过卸载类型而释放。
使用启动类装载器装载的类型永远是可触及的,因此永远不会被卸载。只有使用用户自定义的类装载器装载的类型才会变成不可触及的。
判断动态装载的类型的 Class 实例是否能够触及有两种方式:
对于可触及,能够看下面这个类,Java 虚拟机须要创建起一个完整的引用(触及)链。
class MyThread extends Thread implements Cloneable{
}
复制代码
引用关系图形化描述以下:
从可触及的MyThread
对象指针开始,垃圾收集器跟随一个指向MyThread
类型数据的指针,它找到了:
从 Cloneable 的类型数据开始,垃圾收集找到了:
从 Thread 的类型数据开始,垃圾收集器找到了:
从 Runnable 的类型数据中,垃圾收集器找到了:
从 Object 的类型数据中,垃圾收集器找到了:
能够看到从一个 MyThread 对象开始,垃圾收集器能够触及和 MyThread 有关的全部信息。 PS:设计真滴是优秀啊
Java 类型和对象的生命周期就学习到这里。在类链接
和垃圾回收
部分只是作了简单介绍,后面会补上。
下面一篇详细介绍类的链接
过程-链接模型