深入Java虚拟机——读书笔记

书:深入JAVA虚拟机第二版

第一章

Java体系结构包括4个技术:

  1. Java程序设计语言
  2. Java class文件格式
  3. Java应用编程接口
  4. Java虚拟机
    在这里插入图片描述
    在这里插入图片描述
    一个Java程序可以有两种装载器:
  5. 启动类装载器(系统中唯一的),包括Java API
  6. 自定义装载器(多个)

每个类被装载时,Java虚拟机都会监视这个类。
当被装载的类引用了另外一个类时,虚拟机会使用装载这个类的类装载器装载被引用的类。

在这里插入图片描述
用户自定义的类装载器装载的类存放在不同的命名空间里,它们不能互相访问。

java class是文件可以运行在任何支持java虚拟机的硬件平台和操作系统上的二进制文件。
在这里插入图片描述
每个主机平台都实现了Java虚拟机和Java API, 使Java程序成为具有平台无关性的程序。

第二章 平台无关

为何要与平台无关

用Java创建的可执行二进制程序,能够不加改变地运行多个平台。
Java能够减少开发在多个平台上部署应用程序额成本和时间。

体系结构对平台无关的支持

  1. Java平台:Java程序被编译为可运行于Java虚拟机的二进制文件,并且假定Java API的class文件在运行时都是可用的。虚拟机运行程序,API给予程序访问底层计算机资源的能力。因此Java程序只要与Java平台交互,而无需担心底层的硬件和操作系统。
  2. Java语言:确保基本数据类型在所有平台的一致性,对平台无惯性提供了强力的支持。
  3. Java class文件:Java class文件可以在任何平台创建,也可以被任何平台的Java虚拟机运行。
  4. 可伸缩性:API可以分为 企业版、标准版、微型版。显示了Java平台可以向下伸缩,以适应不同的市场和嵌入式系统的需求。

影响平台无关性的因素

  1. 想要运行Java程序的计算机或设备必须部署Java平台。
  2. Java平台的版本:API更新频繁,使用Java平台新版本编写的程序不一定能在老版本上运行。
  3. JAVA方法是与平台无关的,但是本地方法不是。本地方法是由其它语言编写的,编译成和处理器相关的机器代码。本地方法保存在动态链接库中,即.dll(windows系统)文件中,格式是各个平台专有的。所以不要直接或间接调用不属于Java API的本地方法。
  4. 对虚拟机的依赖。Java虚拟机的实现有两条原则:
    1、不能依赖及时终结(finalzation)来达到程序的正确性。
    2、不能依赖线程的优先级(thread prioritization)来达成程序的正确性。
    所有Java虚拟机都有垃圾回收堆,但是不同的实现可能使用不同的垃圾回收技术。即在不同的虚拟机中,相同的Java程序的对象可能在不同的时间被垃圾回收(终结方法 finalizer)。在一些实现上,程序可能在垃圾回收调用释放资源的终结方法之前,就已经将有限的内存资源耗尽了。那么程序可能可以在一些虚拟机上运行,而其他实现上却不能。
    Java规范保证了在较高优先级的线程被阻塞时,较低优先级的线程将会运行。但是在较高优先级线程没有被阻塞时,并没有禁止较低优先级的线程运行。那么那么较低优先级的线程也可以有限得到CPU时间。

第三章 安全

Java为保护用户免受不可靠来源的恶意程序侵犯,提供呢了一个用户可配置的“沙箱”,限制不可靠程序的活动。
组成Java沙箱基本组件如下:

  • 类加载器结构
  • class文件检验器
  • 内置于java虚拟机的安全特性
  • 安全管理器及Java API

第四章 网络移动性

Java体系结构对网络移动性的支持:

  1. 平台无关性:使得在网络上传送程序更容易,不需要为不同的主机平台准备不同的版本
  2. 安全性:java安全性保证了网络上下载的class文件不会侵犯用户主机
  3. 时间:Java程序是动态链接、动态扩展的,用户不需要等待所有程序的所有class文件下载完毕,就可以开始运行程序了。Java程序从main()开始执行,其他的类在程序需要的时候才动态连接。如果某一个类没有被用到,这个类就不会被装载。
    Java的反射机制还可以让程序员决定何时装载程序的class文件。
    此外,为了减少在网络上传输的时间,class文件被设计得很紧凑,因为每条指令只占一个字节,所以叫“字节码”文件。

第五章 Java虚拟机

Java虚拟机是什么

当说“Java虚拟机时”,可能指三种不同的东西:

  1. 抽象规范
  2. 一个具体的实现
  3. 一个运行中的虚拟机实例

Java虚拟机的生命周期

当启动一个Java程序时,一个虚拟机实例就诞生了。当该程序关闭退出时,这个虚拟机也随之消亡。
如果同一台计算机上同时运行3个Java程序,将得到3个虚拟机实例。每个Java程序都运行于它自己的虚拟机实例中。

Java虚拟机实例通过调用某个初始类的main()方法来运行一个Java程序。
Java程序初始类中的main()方法,将作为该程序初始线程的起点,任何其他的线程都是由这个初始线程启动的。
在Java虚拟机内部有两种线程:守护线程非守护线程
守护线程是由虚拟机自己使用的,比如执行垃圾收集任务的线程。但是Java程序也可以把它创建的任何线程标记为守护线程。
Java程序中的初始线程——开始于main()的,是非守护线程。
只要任何非守护线程在运行,那么这个Java程序也将继续运行(虚拟机仍然存活)。当所有非守护线程都终止时,虚拟机实例将自动退出。假如安全管理器允许,程序本身也能够通过调用Runtime类或者System类的exit()方法来退出。

Java虚拟机的体系结构

虚拟机的内部体系结构:
在这里插入图片描述
类装载子系统:根据给定的全限定名来装入类型(类或接口)
执行引擎:负责执行那些包含在被装载类的方法中的指令。

每个Java虚拟机实例都有一个方法区以及一个堆,它们是由虚拟机实例中所有线程共享的。
当虚拟机装载一个class文件时,它从该字节码文件中解析类型信息,把类型信息放到方法区中。
当程序运行时,虚拟机把所有该程序创建的对象都放到堆中。

当一个新线程被创建时,它都将得到它自己的PC寄存器以及一个Java栈:如果线程执行的是Java方法,那么PC寄存器的值将总是指向下一条被执行的指令,而Java栈总是存储该线程中Java方法调用的状态——包括局部变量、传入的参数、返回值、运算的中间结果等。
而本地方法调用的状态,则一般存储在本地方法栈中。
在这里插入图片描述

栈桢包含了一个Java方法调用的状态。
当线程调用一个新的方法,虚拟机压入一个新的栈桢到该线程的Java栈中;当该方法返回时,这个栈桢被从Java栈中弹出并抛弃。

任何线程都不能访问另一个线程的PC寄存器或者Java栈。
在这里插入图片描述
如图5-3,线程在寄存器和栈中都在独立的空间里,线程3正在执行一个本地方法。

数据类型

在这里插入图片描述
当编译器把Java源码编译为字节码时,它会用int或byte来表示boolean。在Java虚拟机中,false是由整数0来表示的,所有非零整数都表示true。
returnAddress是Java虚拟机内部使用的基本类型,被用来实现Java程序中的finally子句。(第8章详解)

类装载器子系统

对于每一个被装载的类型,Java虚拟机都会为它创建一个Class类的实例来代表该类型,用户自定义的类装载器以及Class类的实例都存放在堆中,而装载的类型信息则位于方法区中。

方法区

关于被装载类型的信息存储在方法区中,该类型中的类(静态)变量同样是存储在方法区中。

由于所有线程共享方法区,所以他们对方法区数据额访问必须被设计为是线程安全的。比如,当两个线程都企图使用同一个类,而这个类还没有装载,那么应该只有一个线程去装载它,而另一个线程等待。

方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。

方法区也可以被垃圾收集,当一个类不再被引用时,Java虚拟机可以垃圾回收这个类,使方法区占用的内存保持最小。

类型信息

  • 这个类型的全限定名
  • 这个类型的直接超类的全限定名
  • 这个类型时类类型还是接口类型
  • 这个类型的访问修饰符
  • 任何直接超接口的全限定名的有序列表

除了上面的信息外,虚拟机还未每个被装载的类型存储以下信息:

  • 该类型的常量池:该类所用常量的有序集合,包括直接常量和对其他类型、字段、方法的引用。(常量:final关键词定义的)
  • 字段信息:类成员变量 (private String name)
  • 方法信息:方法名、返回值、参数、修饰符
  • 除常量外的类(静态)变量
  • 一个到类ClassLoader的引用:每个类被装载时,虚拟机必须追踪它是自定义装载类还是启动类装载类。如果是自定义装载类,那么虚拟机必须存储对该装载器的引用。虚拟机需要在动态连接的时候用到这个信息。
  • 一个到Class类的引用:对每个被装载的类,虚拟机会创建一个对应的Class类实例,Class类中的forName方法可以让用户得到然和已装载的Class实例的引用。

Java程序运行时创建的所有类实例或数组都在同一个堆中。

垃圾收集:垃圾收集器的主要工作就是自动回收不再被运行的程序引用的对象所占用的内存。

堆的设计由虚拟机的实现着决定。比如下面两种:
在这里插入图片描述
堆中必须有一个指向类数据的指针:因为当程序运行时需要转换某个对象引用为另一个类型时,虚拟机需要检查被转换的对象是否是被引用的对象或者它的超类型。此外,调用某个实例的方法时,不能根据引用的类型来决定调用的方法,而必须根据对象的实际类。所以堆内存必须访问方法区。

程序计数器(PC寄存器)

对一个运行的Java程序而言,每一个县都由它自己的PC(程序计数器)寄存器,它是启动线程时创建的。
PC寄存器能够持有一个本地指针,也能持有一个returnAddress。
当执行某个方法时,PC寄存器的内容总是下一条被执行指令的地址,该地址可以是一个本地指针,也可以是方法字节码中相对于起始指令的偏移量。
如果该线程在执行一个本地方法,那么PC寄存器的值是“undefined”。

Java栈

每启动一个新线程,都会分配一个Java栈。
虚拟机只会对Java栈执行两种操作:压栈或出栈。
每当线程调用一个Java方法时,虚拟机都会在该线程的Java栈中压入一个新帧。在执行这个方法时,它使用这个帧来存储参数、局部变量、中间运行结果等数据。

Java方法可以以两种方法完成:

  1. return返回的
  2. 抛出异常终止的

不管哪种方法完成,虚拟机都会将当前帧弹出Java栈然后释放掉。

Java栈上的所有数据都是线程私有的。

栈帧

由三部分组成:局部变量区、操作数栈和帧数据区。

当虚拟机调用一个Java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,根据此分配好栈内存,并压入栈中。

局部变量区包含对应方法的参数和局部变量。

操作数栈是虚拟机的工作区,大多数指令都从这里弹出数据,执行运算,然后把结果压回操作数栈。

帧数据区保存的信息用来支持常量池解析、正常方法返回以及异常派发机制。

本地方法栈

任何本地方法接口都会使用某种本机方法栈。
当调用本地方法时,虚拟机会保持Java栈不变,不再在线程的ava栈压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

执行引擎

运行中Java程序需的每一个线程都一个独立的虚拟机执行引擎的实例。
所有属于用户运行程序的线程,都是在实际工作的执行引擎。

第6章 Java class文件

Java class文件是什么

每一个Java class文件都对一个Java类或者接口作出了全面描述。
一个class文件只能包含一个类或者一个接口。
无论Java class文件在何种系统产生,无论虚拟机在何种系统上运行,对Java class文件的精确定义使得所有虚拟机欧能正确地读取和解释所有Java class文件。

class文件的内容

包含了关于类或接口的所有信息。
在这里插入图片描述

特殊字符串

在class文件中,全限定名中的点用斜线取代了。例如,java.lang.Object的全限定名表示为java/lang/Object。

字段名和方法名以简单名称(非全限定名)形式出现在常量池入口中。例如,一个指向类java.lang.Object所属方法String toString()的常量池入口有一个叫"toString"的方法名。

此外,指向字段和方法的符号引用还包含描述符字符串。
在这里插入图片描述
在这里插入图片描述

常量池

字段

在类或者接口中声明的每一个字段(类变量或者实例变量)都由class文件中的一个名为fieled_info的可变长度的表进行描述。

方法

在class文件中,每个在类和接口中声明的方法,或者由编译器产生的方法,都由一个可变长度的method_info表来描述。

第7章 类型的生命周期

7.1 类型装载、连接与初始化

在这里插入图片描述
装载:把二进制的Java类型读入Java虚拟机中
连接:把这种已经读入虚拟机的二进制格式的类型数据合并到虚拟机的运行时状态中去。
“验证”确保Java类型数据格式正确并且适于Java虚拟机使用;"准备"负责为该类型分配它所需的内存,比如为它的类变量分配内存。“解析”负责把常量池中的符号引用转化为直接引用,它可以在程序需要使用某个符号的时候再去解析它。
初始化:给类变量赋予适当的初始值。

何时会进行初始化?每个类或者接口主动使用时:

  1. 当创建某个类的新实例时(new、反射、克隆、反序列化)
  2. 当调用某个类的静态方法时
  3. 当使用某个类或者接口的静态字段时,或者对该字段赋值
  4. 当调用Java API中的某些反射方法时,比如类Class中的方法或者java.lang.reflect中的类的方法
  5. 当初始化某个类的子类时(要求它的超类已经被初始化了)
  6. 当虚拟机启动某个被表明为启动类的类(即含有main()方法的类)

7.1.1 装载

装载由三个动作组成,Java虚拟机必须:

  1. 通过该类的完全限定名,产生一个代表该类型的二进制数据流
  2. 解析这个二进制数据流为方法区内的内部数据结构
  3. 创建一个代表该类型的java.lang.Class类的实例

可能的产生“二进制数据流”的方式:

  • 从本地文件系统装载
  • 从网络上下载
  • 从ZIP或其他归档文件中提起Java Class文件
  • 从专用数据库中提取Java Class文件

有了二进制数据后,Java把这些数据解析为与实现相关的内部数据结构(见第五章),最后创建CLass对象。
Class对象是Java程序与内部数据结构之间的接口。要访问关于该类型的信息,程序就要调用该类型对应的Class类型实例对象的方法。
这样一个过程,把一个类型的二进制数据解析为方法区的内部数据结构、并在堆上建立一个Class对象的过程,这被称为“创建”类型。

7.1.2 验证

确定类型符合Java语言的语义,并且它不会危及虚拟机的完整性。

比如,在解析二进制数据流时,必须做一些检查,保证虚拟机不会崩溃。虽然这种检查在装载期间完成,但是逻辑上属于验证阶段。

7.1.3 准备

在准备阶段,虚拟机为类变量分配内存,设置默认初始值。但是在初始化阶段之前,类变量都没有被初始化为真正的初始值。(准备阶段不会执行java代码)
在准备阶段,虚拟机把类变量新分配的内存根据类型设置默认值:
在这里插入图片描述

7.1.4 解析

解析是在常量池中寻找类、接口、字段和方法的符号引用、把这些符号引用替换成直接引用的过程。
在java中,一个java类将会编译成一个class文件。在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People类引用org.simple.Tool类,在编译时People类并不知道Tool类的实际内存地址,因此只能使用符号org.simple.Tool(假设)来表示Tool类的地址。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类 的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。

7.1.5 初始化

为类变量赋予正确的初始值。此处的初始值是程序员希望这个类变量所具备的初始值。
在这里插入图片描述
所有的类变量初始化语句和静态初始化器都被Java编译器收集到一起,放到一个特殊的方法里。在Java class文件中,这个方法被称为"<clinit>"。这个方法只能被虚拟机调用,专门把类型的静态变量设置为它们的正确初始值。

初始化一个类包含两个步骤:

  1. 如果类存在直接超类的话,且直接超类还没有被初始化,就先初始化直接超类。
  2. 如果类存在一个类初始化方法,就执行此方法。

因此,第一个被初始化的类永远是Object,然后是被主动使用的类的继承树上的类。

初始化接口并不需要初始化它的父接口。

Java虚拟机必须确保初始化过程被正确地同步。如果多个线程需要初始化同一个类,仅仅允许一个线程来执行初始化,其他的线程需要等待。当活动的线程完成了初始化后,它必须通知其他等待的线程。

何时会进行初始化?每个类或者接口主动使用时:

  1. 当创建某个类的新实例时(new、反射、克隆、反序列化)
  2. 当调用某个类的静态方法时
  3. 当使用某个类或者接口的静态字段时,或者对该字段赋值
  4. 当调用Java API中的某些反射方法时,比如类Class中的方法或者java.lang.reflect中的类的方法
  5. 当初始化某个类的子类时(要求它的超类已经被初始化了)
  6. 当虚拟机启动某个被表明为启动类的类(即含有main()方法的类)

类中声明的字段可能被子类引用;接口中声明的字段可能被子接口或者实现了这个接口的类引用。这就被动使用——使用它们并不会出发它们的初始化。

此外,如果一个字段是 static final 的,使用这样的字段,就不是对声明该字段的类的主动使用,不会初始化该类。

7.2 对象的生命周期

一个类经过装载、连接(验证、准备、解析)和初始化,程序就可以访问它的静态字段,调用它的静态方法,或者常见它的实例。

7.2.1 类实例化

实例化一个类有四种途径:

  1. new
  2. 调用Class或者java.lang.reflect.Constructor对象的newInstance()方法
  3. 调用现有对象的clone()方法
  4. 通过java.io.ObjectInputStream类的getObject()方法反序列化

7.2.2 垃圾收集和对象的终结

对虚拟机的实现可以决定何时应垃圾收集不再被引用的对象——或者决定是否根部不收集它们。

如果类声明了一个名为finalize()的返回void的方法,垃圾收集器会在释放这个实例所占据的内存空间之前执行这个方法(被称为终结方法)一次。

在这里插入图片描述
此方法可以直接被程序所调用。
垃圾收集器最多只会调用一个对象的终结方法一次,在对象编程不再被引用的之后的某个时候,在占据的对象被重用之前。

7.3 卸载类型

就像对象不再被引用后可以被垃圾收集。同样,当程序不再引用类时可以卸载它们。
类的垃圾收集和卸载之所以重要,是因为Java程序在运行时可以通过用户的自定义装载器来动态地扩展程序。所有被装载的类型都在方法区占据内存空间。
启动类装载器装载的类型永远不会被卸载。
判断动态装载的类型的Class实例在正常的垃圾收集过程中是否可以触及有两种方法:

  1. 如果堆中还存在一个可触及的对象,在方法区中的它的类型数据指向一个Class实例,那么这个Class实例时可触及的。
  2. 如果程序保持对Class实例的明确引用,它就是可触及的。
    在这里插入图片描述
    通过一个可达的MyThread对象,垃圾收集器可以“触及”MyThread和它所有超类型的Class实例。

第8章 连接模型

Java的连接模型允许用户自行设计类装载器,通过自定义类装载器,Java程序可以装载在编译时并不知道的类或者接口,并动态连接它们。

8.1 动态连接和解析

class文件把它所有的引用符号都保存在了一个地方——常量池。

每一个class文件都有一个常量池,每一个被Java虚拟机装载的类或者接口都有一份内部版本的常量池,被称作运行时常量池。运行时常量池映射到class文件的常量池。

当程序需要使用某个特定的符号引用时,它首先要被解析。
解析过程是根据符号引用找到实体,再把符号引用替换成一个直接引用的过程。

连接不仅仅将符号引用替换成直接引用,还包括检查正确性和权限。
在这里插入图片描述

不同的虚拟机实现允许在程序执行的不同时间进行解析。

早解析:预先解析所有的符号引用,在这种情况下,程序在它的main()方法尚未被调用时就已经完全连接了。

迟解析:Java虚拟机只会在执行程序时第一次用到这个符号引用时才去解析它。

不论使用早解析、迟解析还是折中解析策略,都应该给用户迟解析的印象。只有在程序实际访问一个符号引用时才抛出错误。如果Java虚拟机使用早解析,发现某个class文件无法找到,它不会抛出对应的错误,而是在程序后来实际访问这个class文件的某些东西时才抛出错误;如果程序不使用这个类,错误永远不会被抛出。

8.1.1 解析和动态拓展

Java的体系结构允许动态拓展Java程序,这个过程包括运行时决定所使用的类型,装载它们,使用它们。

动态扩展Java程序最直接的方式是使用java.lang.Class的forName()方法。

public static Class forName(String className) throws ClassNotFoundException;

public static Class forName(String className, boolean initialize, ClassLoader loader) throws ClassNotFoundException;

三参数的forName方法时1.2版本加入的,String类型接收类型的全限定名。
如果initialize为true,那么类型会在forName()方法返回之前连接并初始化;如果为false,那么类型会被装载,可能会被连接但是不会被forName()方法初始化。
Classloader传递一个用户定制的类加载器的引用给forName()。如果使用默认的启动类装载器,只需传递null作为Classloader的参数。
单参数的forName方法总是使用当前的类装载器(执行forName()方法的类的类装载器),并且初始化该类型。

另一种方式是使用用户自定义类装载器的loadClass()方法。

protected Class loadClass(String name) throws ClassNotFoundException;
protected Class loadClass(String name,boolean resolve) throws ClassNotFoundException;

resolve参数表示是否在装载时执行该类型的连接。

这两种动态扩展的方式都将返回一个Class实例。

如果需要请求的类型在装载时就初始化的话,那么不得不使用forName()。
初始化是很重要的。比如JDBC驱动程序通常使用forName()调用装载的。因为每个JDBC驱动程序的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序所使用的,驱动程序类必须被初始化,而不是仅仅被加载。
如果一个驱动程序被加载了,而没有被初始化,那么类的静态初始化方法就无法执行。

8.1.12 直接引用

通常情况下,指向类型、类变量和类方法的直接引用可能是指向方法区的本地指针。

指向实例变量和实例方法的直接引用都是偏移量。实例变量的直接引用可能是从对象的映像开始算起到这个实例变量位置的偏移量。实例方法的直接引用可能是到方法表的偏移量。

第9章 垃圾收集

垃圾收集就是自动释放不再被程序所使用的对象的过程。

9.1 为什么要使用垃圾收集

“垃圾收集”更精确的说法应该是“内存回收”:当一个对象不再被程序所引用时,它所使用的堆空间可以被回收,以便被后续的新对象所使用。

在释放不再被引用的对象的过程中,垃圾收集器运行将被要被释放的对象的终结方法(finalizer)。

除了释放不再被引用的对象,垃圾收集器还要处理堆碎块。堆碎块是在正常的程序运行过程中产生的。
当新对象分配了空间,不再被引用的对象被释放,所有堆内存的控线位置介于活动的对象之间。请求分配新对象时可能不得不增大堆空间的大小,这是因为堆中没有连续的空闲空间放得下新的对象。

在一个虚拟内存系统中,增长的堆所需要的额外分页空间会影响程序运行的性能。在内存较小的嵌入式系统中,碎块导致虚拟机产生不必要的“内存不足”错误。

把垃圾回收交给虚拟机有两个好处:

  1. 提高生产率,减少工作中需要解决内存问题的时间。
  2. 帮助程序保持完整性。避免错误地释放内存而导致Java虚拟机崩溃。

使用垃圾收集堆的潜在缺陷是它加大了程序负担,可能影响程序性能。虚拟机必须追踪哪些对象被正在执行的程序所引用。

9.2 垃圾回收算法

如果正在执行的程序可以访问到的根对象和某个对象之间存在引用路径,这个对象是可触及的。
对于程序来说,根对象总是可以访问的。
从这些根对象开始,任何可以被触及的对象都被认为是“活动”的对象。无法触及的对象被认为是垃圾。

根对象根据实现不同而不同,但总会包含局部变量的对象引用和栈帧的操作数栈。

区分活动对象和垃圾的两个基本方法是 引用计数 和 跟踪。
引用计数垃圾收集器通过为堆中的每一个对象保存一个计数来区分活动对象和垃圾对象。这个计数记下了对那个对象的引用次数。
跟踪垃圾收集器实际上追踪从根节点开始的引用图。在追踪中遇上的对象以某种方式打上标记,当追踪结束时,没有被打上标记的对象就被判定是不可触及的,可以被当做垃圾收集。

9.3 引用计数收集器

当一个对象被创建了,并且指向该对象的引用被分配给一个变量,这个对象的引用计数被设置为1。
当任何其他变量被赋值为这个对象的引用时,计数加1。
当一个对象的引用超过了生存期或者被设置为一个新的值时,计数减1。
任何计数为0的对象可以被当作垃圾收集。

当一个对象被垃圾收集的时候,它引用的任何对象计数减1.
但是主流的java虚拟机没有采用引用计数算法,其中最主要的原因就是它很难解决对象之间互相循环引用的问题。

例子:

对象A和B互相引用,但除此之外,这两个对象再无任何引用,但是他们因为互相引用着对方,所以导致他们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收他们。

在这里插入图片描述
如上图所示,假设我们有两个类分别是A和B,A类中有一个字段是B类的类型,B类中有一个字段是A类类型,现在分别new一个A类对象和new一个B类对象,此时引用a指向刚new出来的A类对象,引用b指向刚new出来的B类对象,然后将两个类中的字段互相引用一下,这样即使下面进行a = null和b = null,但是A类对象仍然被B类对象中的字段引用着,尽管现在A类和B类独享都已经访问不到了,但是引用计数却都不为0.

9.4 跟踪收集器

跟踪收集器遍历对象引用树,标记每一个遇到的对象,然后清除没有被标记的对象。

9.5 压缩收集器

用于对付堆碎块的策略:将活动的对象越过空闲区滑动到堆的一端,在这个过程中,堆的另一端出现一个大的连续空闲区。所有被移动的对象的引用也被更新,指向新的位置。

9.6 拷贝收集器

拷贝垃圾收集器把所有的活动对象移动到一个新的区域。在拷贝的过程中,它们被紧靠着放置,所以可以消除它们之间原本存在的空隙。原有的区域被认为都是空闲区。

9.7 按代收集的收集器

停止并拷贝的缺点是,每一次收集时,所有活动对象都必须被拷贝。把生命周期很长的对象来回拷贝,很浪费时间。
按代收集收集器通过对象寿命来分组,更多地收集那些短暂出现的年幼的对象,而非寿命较长的对象。

9.8 自适应收集器

自适应能够根据算法的表现,监视堆中的情形,并对应地调整使用合适的垃圾收集技术。

9.9 火车算法

9.10 终结

一个对象可以拥有终结方法,这个方法时垃圾收集器在释放对象前必须运行。

给一个类加上终结方法,只需要简单地在类中声明一个方法:

class Example2 {

	protected void finalize() throws Throwable {
		//...
		super.finalize();
	}

}

当执行了所有的终结方法后,垃圾收集器必须从根节点开始再次检测不再被引用的对象(被称为第二次扫描)。
这个步骤是必要的,因为终结方法可能“复活”了某些不再被引用的对象,使它们再次被引用了。

如果一个带有终结方法的对象不再被引用,并且它的终结方法运行过了,垃圾收集器必须使用某种方法记住这一点,而不能再次执行这个对象的终结方法。

9.11 对象可触及性的生命周期

引用对象的目的是为了指向某些对象,这些对象任然随时可以被垃圾回收。

六种可触及状态:

  • 强可触及:对象可以从根节点不通过任何引用对象搜索到。垃圾收集器不会回收该对象的内存。
  • 软可触及:对象可以从根节点开始通过一个或多个(未被清除的)软引用对象触及。垃圾收集器可能回收收该对象的内存。
  • 弱可触及:对象可以从根节点开始通过一个或多个(未被清除的)弱引用对象触及。垃圾收集器必须回收收该对象的内存。
  • 可复活的:对象可能通过执行某些终结方法复活到这几种状态之一。
  • 影子可触及:不是上面任何状态的一种,对象已经被断定不会被任何终结方法复活(如果定义过了终结方法,它的终结方法已经被运行过了),并且它可以从根节点开始通过一个或多个(未被清除的)影子引用对象触及。
  • 不可触及:对象已经准备好被回收了。

第10章 栈和局部变量操作

待补充。

发布了40 篇原创文章 · 获赞 1 · 访问量 1078

猜你喜欢

转载自blog.csdn.net/weixin_44495162/article/details/103936548
今日推荐