java虚拟机

3 篇文章 0 订阅

1.第一趟扫描,对每一段将被当做类型导入的字节序列,class文件检查器都会确认他是否符合java class文件的基本结构。
 在这次扫描中,检查器将惊醒许多检查,例如每个class文件必须以四个同样的字节开始:魔数0xCAFEBABE。
 这个魔数的用处是让class文件分析器和容易分辨出某个文件有明显问题而加以拒绝。
 这个文件可以是被破坏了的class文件,或者是压根儿就不是class文件。
2.第二趟:类型数据的语义检查
3.第三趟:字节码验证
4.第四趟:符号引用的验证

 

装载,连接以及初始化,类装载器子系统除了要定位和导入二进制class文件外,还必须负值验证被导入类的正确性,
为类变量分配并初始化内存,以及帮助解析符号引用。这些动作必须严格按照以下的顺序执行:
1)装载--查找并装载类型的二进制数据。
2)连接--执行验证,准备,以及解析(可选)
   验证 确保被导入类型的正确性。
   准备 为类变量分配内存,并将其初始化成默认值。
   解析 把类型中的符号引用转换为直接引用。
3)初始化--把类变量初始化为正确初始值


sun jdk 1.2 中,启动类装载器将只在系统类的安装路径中查找要装入的类
而搜索CLASSPATH目录的任务现在交给了系统类装载器--他是一个自定义的类装载器
当虚拟机启动时就被自动创建。


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


java数据类型分为两种:
基本类型和引用类型,基本类型持有原始值,而引用类型的变量持有引用值
基本类型:
 1.整数类型:
   1.byte   8比特, 带符号,二级制补码
   2.short 16比特,带符号,二级制补码
   3.int   32比特,带符号,二级制补码
   4.long  64比特,带符号,二级制补码
   5.char  16比特,带符号,二级制补码
  2.浮点数类型:
   1.float 32比特
   2.double 64比特
  3.boolean(虽然java虚拟机也把boolean看做基本类型,但是指令集对Boolean只有有限的支持。当编译器
  把java源码编译为字节码时,他会用int或者byte来表示Boolean。在java虚拟机中,false是由整数0来表示的,所有的非零的整数都表示为TRUE)
  4.returnAddress
引用类型
 类类型
 接口类型
 数组类型

命名空间:每个类装载器都有自己的命名空间,其中维护由他装载的类型
所以一个java程序可以多次装载具有同一个全限定名的多个类型。这样一个全限定
名就不足以确认在一个java虚拟机中的唯一性。因此当多个类装载器都装载了
同名的类型时,为了唯一的标示该类型,还要在类型名称前加上类装载器的标示

????

对于一个运行的java程序而言,其中的每一个线程都有他自己的pc(程序计数器)寄存器
他是该线程启动时创建的,Pc寄存器的大小是一个字长,因此他既能够持有一个本地指针
,也能够持有一个returnAddress,当线程执行某个java方法时,PC寄存器的内容是下一条
将被执指令的地址,这里的地址可以是一个本地指针,也可以是在方法字节码中相对于该方法
起始指令的偏移量,如果该线程正在执行一个本地方法,那么此时PC寄存器的值是“undefined”

java栈
 每当启动一个新线程时,java虚拟机都会为他分配一个java栈,前面我们提到,java栈以帧为
 单位保存线程的运行状态,虚拟机只会直接对java栈执行两种操作:以帧为单位的压栈或出栈。
 某个线程正在执行的方法为该线程的当前方法,当前方法使用的栈帧称为当前帧,当前方法
 所属的类称为当前类,当前类的常量池称为当前常量池。在线程执行的一个方法时,他会跟踪当前
 类和当前常量池,此外,当虚拟机遇到栈内操作指令时,他对当前帧内数据执行操作。
 java方法可以以两种方式完成,一种通过return返回,称为正常返回,一种是通过抛出异常而异常终止。
 不管以哪一种方式返回,虚拟机都会将当前帧弹出java栈然后释放掉,这样上一个方法的帧就成为了
 当前帧了。
 java栈上的所有数据都是此线程私有的,任何线程都不能访问另一线程的栈数据,
 因此我们不需要考虑多线程情况下栈数据的访问
 同步问题。当一个线程调用一个方法时,方法的局部变量保存在调用线程的java栈的帧中,只有
 一个线程能总是访问哪些局部变量,即调用方法的线程。
 
 栈数据区,除了局部变量区和操作数栈外
 
 任何java虚拟机实现的核心都是她的执行引擎。在java虚拟机规范中,执行引擎的行为适用指令集来定义
 对于每条指令,规范都详细规定了当实现执行到该指令时应该处理什么,但是却对如何处理言之甚少。在
 前面的章节中提到过,实现的设计者有权决定如何执行字节码,实现可以采取解释,及时编译,或者直接用
 芯片上的指令执行,还可以是他们的混合,或者任何你想到的新技术。
 作为运行时实例的执行引擎就是一个线程。
 
 指令集  方法的字节码流是由java虚拟机的指令序列构成的,每一条指令包含一个单字节的
 操作码,后面跟随0个或者多个操作数。操作码表明需要执行的操作数。
 
 
 一个class文件中只能包含一个类或者接口。
 
 ClassFile表中各项简介如下:
 (1)magic(魔数)
 每个java class文件的前4个字节被称为他的魔数:0xCAFEBABE。魔数的作用在于,可以轻松的
 分辨出java class文件和非java class文件。
 (2)minor_version major_version
 class文件下面的4个字节包含主,次版本号。
 (3)constant_pool_count 和 constant_pool
 
 
 
 类型的生命周期
 类的装载,连接和初始化:
 在首次主动使用时初始化,这个规则直接影响装载,连接和初始化的机制,在首次主动使用
 其类型必须被初始化,然而在类初始化,然而在类型被初始化之前,他必须已经被连接,而
 在他被连接之前,他必须被装载,java虚拟机的实现可以根据需要在更早的时候装载和连接
 类型,没有必要一直要等到该类型的首次主动使用才去装载和连接他,无论如何,如果一个类型
 在他的首次主动使用之前还没有装载和连接的话,那他必须在此时被装载和连接,这样他才
 能被正确的初始化。
 
 
 装载》
 装载阶段,由三个基本动作实现:要装载一个类型 java虚拟机必须:
 (1)通过该类型的完全限定名,产生一个代表该类型的二进制数据流。
 (2)解析这个二进制数据流为方法区内的内部数据结构。
 (3)创建一个表示该数据类型的java.lang.Class类的实例。
 
 java类型要么由启动类装载器装载,要么通过用户自定义的类装载器装载。启动类装载器是虚拟机实现的
 一部分。他以与实现无关的方式装载类型,用户自定义的类装载器是类java.lang.ClassLoader的子类实例
 他以定制的方式装入类。
 
 验证:
 当类型被装载后,就准备进行连接了,连接过程的第一步是验证--确认类型符合java语言的语义
 并且他不会危及虚拟机的完整性。
 在验证上,不同的虚拟机实现拥有一些灵活性,虚拟机实现的设计者可以决定如何以及何时验证类型,
 java虚拟机规范列出了虚拟机可以抛出异常以及在何种条件下必须抛出他们。不管虚拟机可能遇到了
 什么麻烦,都应该有错误和异常可以抛出。
 另一种可能在装载时进行的检查是,确保除了Object之外的每一个类都有超类。在装载是检查超类的原因
 是当虚拟机装载一个类时,需要确保该类的所有超类都已经被装载。
 
 那么正式的验证阶段做那些检查呢?
 首先确保个各类之间二进制兼容的检查:
 (1)检查final类不能有子类。
 (2)final方法不能被覆盖。
 (3)确保子类和父类之间没有不兼容的方法。
 (4)检查字节码的完整性。
 
 
 准备
 随着java虚拟机装载了一个类,并执行了一些他选择的验证之后,累就可以进入准备阶段了。
 在准备阶段,java虚拟机给类变量分配内存,设置默认值。在准备阶段,java虚拟机实现可能
 也为一些数据结构分配内存,目的是提高运行程序的性能。这种数据结构的例子如方法表,它包含
 向类中的每一个方法的指针,方法表可以使得继承的方法执行时不再需要搜索超类。
 
 
 解析
 类型经过了连接的前两个阶段--验证和准备--之后,他就可以进入第三个连接阶段了--解析。解析过程就是
 在类型的常量池中寻找类,接口,字段和方法的符号引用。把这些符号引用替换成直接引用的过程。
 
 
 初始化
 为了准备让一个类或者接口被首次主动使用,最后一个步骤就是初始化,也就是为
 类变量赋予正确的初始值,这里的正确初始值指的是程序员希望这个类变量所具备的的初始值,
 正确的初始值是喝在准备阶段赋予的默认初始值对比而言的,前面说过,根据类型的不同
 ,类变量已经被赋予了默认初始值,而正确的初始值是根据程序员制定的主观计划而生成的
 
 初始化一个类包含两个步骤:
 (1)如果类存在直接超类的话,且直接超类还没有初始化,就先初始化超类。
 (2)如果类存在一个类初始化方法,就执行这个方法。
 当初始化一个类的直接超类的时候,也是需要包含这两个步骤,因此,第一个被出示的类永远是object,
 然后是被主动使用的类的继承树上的所有的类,超类总是在子类之前被初始化。
 
 初始化接口并不需要舒适化他的父接口,因此初始化一个接口只需要一步,如果接口存在一个接口初始化方法的话,
 就执行此方法。
 java虚拟机必须确保初始化过程被正确的同步,如果多个线程需要初始化一个类,仅仅允许一个线程来执行初始化,
 其他线程需要等待,当活动线程完成了初始化的动作,他必须通知其他的等待线程。
 
 
 主动使用和被动使用
 在前面讲过,java虚拟机在首次主动使用类型时初始化他们,只有6种活动被认为是主动使用:创建类的新实例,
 调用类中的声明的静态方法,操作类或者接口中申明的非敞亮静态字段,
 调用java api中特定的反射方法,初始化一个类的子类,以及制定一个类作为java虚拟机启动时的初始化类。
 
 
 对象的生命周期
 一旦一个类被装载,连接和初始化,他就随时可以使用了。程序可以访问他的静态字段,调用它的静态方法,
 或者创建他的实例。
 本节会讨论类的实例化和初始化,即对象生命起始阶段的活动,还要讨论垃圾回收和终结,即对象生命尽头的活动,
 
 在类装载器之间具有了委派关系,首先发起装载要求的类装载器不必是定义该类型的装载器,在java的术语中
 要求某个类装载器去装载一个类型,但是却返回了其他类装载器装载的类型,这种类装载器被称为是那个类型的
 初始类装载器;而实际定义那个类型的类装载器被称为该类型的定义类装载器。
 
 
 class文件把他所有的引用符号保存到一个地方--常量池,每一个class文件有一个常量池,每一个被java虚拟机装载的
 类或者接口都有一份内部版本的常量池,被称作运是行常量池。运行时常量池是一个特定于实现的数据接口。数据结构
 映射到class文件中的常量池,因此,当一个类型被首次装载时,所有来至于类型的符号引用都被装载到类型的运行常量池。
 在程序运行的某个时刻,如果某个特定的符号引用将要被使用,他首先要被解析。,解析过程就是根据符号引用查找实体,
 再把符号引用
 替换成一个直接引用的过程,因此所有的符号引用都保存在常量池中,所以这个过程被称作常量池解析。
 
 记住,java虚拟机为每一个装载的类和接口保存一份独立的常量池,当一条指令引用常量池的第五个元素的时候,
 他指向的是当前类的
 常量池中的第五个元素,即定义java虚拟机当前正执行的方法的类。
 
 
 连接不仅仅包括把符号引用替换成直接引用,还包含检查正确性和权限,在上面介绍过,
 检查符号引用的存在性和访问权限就是在解析
 的时候完成的,比如,java虚拟机吧getstatic指令解析为其他类中的字段时,java虚拟机会检查确认是否符合下列条件:
 (1)那个其他类存在。
 (2)该类有访问那个其他类
 (3)那个其他类中存在名字相符的字段
 (4)那个字段的类型和期望的类型相符
 (5)本类有权访问那个字段
 (6)那个字段的确是一个静态类变量,而不是一个实例变量。
 虽然java虚拟机的实现选择何时解析符号引用的自由,但不管怎么样,都应该给外界一个迟解析的印象。
 对于特定的java虚拟机来说
 不管何时解析,都在程序执行过程中第一次实际试图访问一个符号引用的时候才能抛出错误,
 用这种方式对于用户来说看上去是一个迟解析
 如果java虚拟机使用早解析,在早解析的过程中,发现某个class文件无法找到,他不会抛出错误,
 知道程序访问到这个class文件的某些东西
 时才能抛出错误,如果程序不使用这个类,·错误永远不会抛出。
 
 
 解析和动态扩展
 除了简单的在运行时连接类型之外,java程序还可以在运行时连接那个类型。java体系结构允许动态的扩展java程序,
 这个过程包括运行
 包括运行所使用的类型,装载他们,使用他们。
 通过传递类型的名字到java.lang.Class的forname方法,或者使用用户自定义的类装载器的loadClass方法,
 可以动态的扩展java程序,用户自定义的
 类装载器可以从java.lang.ClassLoader的任何子类创建。两种方法都可以使运行中的程序去调用在源代码中没有提及的而在程序运行中所决定的类型
 
 动态扩展java程序最直接的方式就是使用java.lang.Class的forname()方法,他有两种重载的方式
 动态扩展java程序的另一种方式就是使用用户自定义的类装载器的loadClass()方法。两个Loadclass方法都接受装载类型的全限定名装入String类型
 的name的参数。loadclass()的语义和forname()是一样的。如果loadclass()方法已经用String类型的name参数传递的全限定名装载了类型,他会返回
 这个已经被装载的类型的Class类型,否则该方法会试图用某种用户定制的方式来装载请求的类型,如果类装载器用定制的方式成功的装载了类型,loadclass
 会返回该类型的一个实例,否则方法会抛出classnotfoundexception异常。
 
 双参数版本的loadClass()中,Boolean类型的resolve参数表示是否在装载时执行该类型的连接。前面一章介绍了连接的过程包含三个步骤:校验被装载的类型
 准备,解析类型中的符号引用,其中第三步是可选的。如果resolve参数是true,loadclass()方法会确保再返回该类型的实例class之前,装载并连接该类型。
 
 使用Class中的forname或者是classloader中的loadclass取决于用户的需要,如果没有特别要使用类装载器的需求,或许应该使用forname,因为forname是动态扩展
 最直接的方式,另外如果需要请求的类型在装载时就初始化的话,就不得不用class中的forname
 然而类装载器可以满足一些class forname无法满足的需求,如果需求一些特定的装载类型的方法,比如从网络下载,从数据库中提取,从加密文件中提取,甚至
 动态的创建他们,这是就需要一个类装载器。创建用户自定义的类装载器,其中一个重要的原因就是能够以定制方式把类的全限定名转换成一个java class
 文件格式的字节数组,使用类装载器而非forname的其他理由和安全性相关,上面我们提到,每个类的装载器都有独立的命名空间,这就为不同的命名空间中装载的
 类型提供了一层安全防护,可以编写一个java程序,类型无法看见不在统一命名空间的其他类型,类装载器负责把装载的代码放到保护域中,也就是说,如果
 安全上需要包含一种定制方式把类型装载到保护域中,就需要使用类装载器。
 
 
 类装载器和双亲委派模型
 每一个用户自定义的类装载器在创建时被分配一个双亲类装载器,如果没有显式的传递一个双亲类装载器给用户自定义的类装载器的构造方法 ,系统类装载器
 就被指定为双亲,或者在调用用户
 
 
 常量池解析
 解析CONSTANT_Class_info入口
 在所有的常量池入口类型中,解析起来最复杂的就是CONSTANT_Class_info了,这种入口类型用来表示指向类(包括数组)和接口的符号引用
 有几个指令:比如new 和anewArray,直接使用CONSTANT_Class_info入口
 1.数组类
  如果一个CONSTANT_Class_info入口的name_index项指向的CONSTANT_Utf8_info字符串是由一个左方括号开始的,比如
  “{I”,那么他指向的是一个数组类。内部使用的数组名字的每一维使用一个方括号,然后是元素的类型。如果元素类型由
 一个L开头,比如Ljava.lang.Integer,那么数组是一个关于引用的数组;否则元素类型是一个基本类型,比如“I”表示int,"D"表示double.数组就是一个基本类型组成的
 数组。
 指向数组类的符号引用的最终解析结果是一个Class实例,表示该数组类。如果当前类装载器已经被记录为被解析的数组类的初始类装载器,就使用相同的类
 否则,虚拟机执行下面的步骤:如果数组的元素类型是一个引用类型,虚拟机用当前的类装载器解析元素类型。
 
 
 
 2.非数组类和接口
 如果CONSTANT_Class_info入口的name_index项指向一个并非由左括号开始的CONSTANT_Utf8_info字符串,那么这是一个指向非数组类或者接口的符号引用。解析这种类型
 的符号引用分为多步。
 要解析任何指向非数组类或者接口的符号引用,java虚拟机都要执行相同的基本步骤。下面我们用步骤1a和步骤1b来说明,在步骤1a,类型被装载。在步骤1b,
 检查访问类型的权限,虚拟机执行步骤1a的精确方式取决于该引用类型是被启动类装载器装载,还是被用户自定义的类装载器。
 这一部分还要说明步骤2a到步骤2d,他们描述了如何连接和初始化新解析的类型。对于将要被连接和初始化的类型来说,这些步骤
 步骤1a:装载类型或者任何超类型
  解析非数组类或者接口的基本要求是确认类型被装载到了当前的命名空间。作为第一步虚拟机必须确认被引用类型是否已经被装载到当前的命名空间
  为了做出决定,虚拟机必须查明是否当前类装载器被标记为该类型的初始装载器,对于每一个类装载器,java虚拟机需要维护一个列表
  其中记录了所有的类装载器是一个初始类装载器的类型的名字。每一张这样的列表就组成了java虚拟机内部的命名空间
  在解析过程中,虚拟机使用这些列表来决定是否一个类已经被一个特定的类装载器装载过。
  如果虚拟机发现希望装载的全限定名已经在当前命名空间被列出来了,他将只使用已经被装载的类型,。首先检查命名空间是否包含了希望装载的全限定名。虚拟机保证每一个
  类装载器都只装载一个确定的名字。
  
  如果希望装载的类型还没有被装载到当前的命名空间,虚拟机吧类型的全限定名传递给当前类装载器,java虚拟机总是
  要求当前类装载器,就是发起引用的类型的定义装载器,也就是运行常量池包含正在本解析的CONSTANT_Class_info入口
  的类装载器。
  一旦被引用的类型被装载了,虚拟机仔细检查他的二进制数据,如果类型是一个类,并且不是Object,虚拟机根据类的数据得到他的直接超类的全限定名,虚拟机接着会查看
  超类是否已经被装载到了当前的命名空间,如果没有,先装载超类,一旦超类被装载了,虚拟机会检查的超类的二进制数据,找到他的超类,一直重复到Object。
  
  当虚拟机调用超类的时候,他实际上只是解析了另一个符号引用,为了确定一个类的超类的全限定名,虚拟机会查看class的super_class域。这个域给出了一个CONSTANT_Class_info入口
  的索引,作为指向超类的符号引用
  
  在Object返回的路上,虚拟机会再次检查每一个类的数据,看他们是否直接实现了任何接口,如果是这样,他会确保那些接口是否被装载
  ,对于每一个虚拟机装载的接口,虚拟机会检查他们的数据,看他们是否直接扩展了其他的接口,如果是这样,虚拟机会确认超接口是否已经被装载。
  
  一旦一个类型被装载到当前命名空间中,而且通过递归,所有该类型的超类和超接口也都被成功的装载了,虚拟机就会创建新的class实例来代表这个类型,如果定义类型的字节是由用户自定的类装载器
  确定或者生成的,然后传递给defineClass方法,他会返回这个class的实例,或者如果用户自定义的类装载器通过findsystemclass调用委派启动类装载器来装载,他也会返回一个实例,直到
  loadclass从上面的两种方式接收到返回的实例才会返回这个class实例,如果用户自定义的类装载器委派给另一个用户自定义的类装载器,那么当被委派的用户自定义类装载器的loadclass方法返回时,他会收到
  class实例,知道从委派的类装载器中收到class实例,发起委派的类装载器才会从自己的loadclass中返回这个class实例,
  
  
  步骤1b:检查访问权限
  随着装载结束,虚拟机检查访问权限,
  
  步骤2:连接并初始化类型和任何超类
  超类必须在子类之前被初始化,如果虚拟机因为主动使用一个类而且正在解析该类的引用,他必须确认他的所有超类都被初始化。
  注意的是,只有超类是必须被初始化的,超接口是不必的。
  步骤2a:检查类型
  步骤2b:准备类型
  在准备阶段虚拟机为类变量以及随实现不同而有差别的数据结构分配内存。
  
  步骤2d:初始化类型
  在这个时刻,类已经被装载,校验,准备好了,,经过漫长的过程,类型终于准备好了初始化,初始化包含两个步骤:
  如果类型拥有任何超类,初始化类型的超类是按照至上而下的顺序进行的,如果类型有一个类初始化方法。,那么也在此时执行
  
  
  解析CONSTANT_Fieldref_info入口
  
  
  装载约束
  
  
  
  垃圾收集
    垃圾收集暗示着程序不再需要的对象就是垃圾,可以被丢弃,更精确,更新的说法是,“内存回收”,当一个对象不再被程序所引用,他所使用的
   堆空间可以被回收,以便被后面的新对象所使用,垃圾收集器必须能断定那些对象是不再被引用的,并且能够把他们所占据的堆空间释放出来,
   在释放不再被引用的对象的过程中,垃圾收集器运行将要被释放的对象的终结方法。
    除了释放不再被引用的对象,垃圾收集器还要处理堆碎块,堆碎块是在正常程序运行过程中产生的,新的对象分配了空间,不再被引用的对象被释放,
   所以堆内存的空闲位置位于活动对象之间,请求分配新对象时可能不得不增加堆空间的大小,虽然可以使用的总空闲空间是足够的。这是因为,堆中没有连续
   的空闲空间放得下新的对象,在一个虚拟内存系统中,增长的堆需要的额外分页空间会影响运行程序的性能,在内存较小的嵌入式系统中,碎块导致虚拟机产生
   不必要的内存不足错误。
    垃圾收集把用户从释放占用内存的重担中解放出来,知道何时明确的释放内存是非常重要技巧,把这项工作交给java虚拟机有几个好处:首先可以提高生产率。
   在一个不具有垃圾收集机制耳朵于艳霞变成,你可能需要花费好多时间来解决难以捉摸的内存问题。当使用java变成时,你就可以更加有效的利用这些时间,
   垃圾收集的第二个好处是能帮助程序保持完整性,垃圾收集是java安全策略的一个重要部分,java程序员不可能因为食物或者故意的错误的释放内存导致java虚拟机
   崩溃
    使用垃圾收集堆,有一个潜在缺陷是他加大了程序负担,可能影响程序性能,java虚拟机必须跟踪那些对象被正在执行的程序锁引用,并且动态的终结并释放不再
   使用的对象,和明确释放不再被使用的内存比起来,这个活动会需要更多的CPU时间,并且在垃圾收集环境下,程序员对安排cpu时间来释放无用对象缺乏控制。
   
   
   垃圾收集算法
    任何垃圾收集算法必须做两件事,首先,他必须检测出垃圾对象,其次,他必须回收垃圾对象所使用的堆空间并还给程序。
   垃圾检测通常通过建立一个根对象的集合并且检查从这些根对象开始的可触及性来实现。如果正在执行的程序可以访问到的根对象和某个对象之间存在引用路劲,这个
   对象就是可触及的,对于程序来说,根对象总是可以访问的,从这些根对象开始,任何可以被触及的对象都被认为是活动的对象,无法被触及的对象都被认为是垃圾,
   因为他们不再影响程序的未来执行。
    java虚拟机的根对象集合根据实现不同而不同,但是总会包含局部变量中对象引用和栈帧的操作数栈。另外一个跟对象的来源是被加载的类的常量池中的对象引用,
   比如字符串,被加载的类的常量池可能指向保存在堆中的字符串,比如类名字,超类的名字,超接口的名字。字段名,字段特征签名,方法名或者方法特征签名。还有一个来源是
   传递到本地方法中的,没有被本地方法释放的对象引用,在一个潜在的根对象的来源就是,java虚拟机运行时数据区中从垃圾收集器的堆中分配的部分
    任何被根对象引用的对象都市可触及的,从而是活动的,另外,任何被活动的对象引用的对象都是可触及的,程序可以访问任何可触及的对象,所以这些对象必须保留在堆
   里面。任何不可触及的对象都可以被收集,因为程序没有办法来访问他们。
    在java虚拟机实现中,有些垃圾收集器可以区别真正的对象引用和看上去像合法对象引用的基本类型之间的差别。可是某些垃圾收集器依然选择不区分真正的对象引用和伪装品,
   这种垃圾收集器被称为保守的,因为他们可能不能总是释放每一个不在被引用的对象。对保守的收集器,有时候垃圾对象也被错误的判断为活动的,因为有一个看上去像是对象引用的
   基本类型引用了对象,保守的垃圾收集器使垃圾收集速度提高了,因为有一些垃圾被遗忘了。
    区分活动对象和垃圾的两个基本方法是引用计数和跟踪。引用计数垃圾收集器通过为堆中的每一个对象保存一个计数来区分活动对象和垃圾对象。这个计数记录下了对那个对象的引用次数
   。跟踪垃圾收集器实际上追踪从根节点开始的引用图,在追踪中遇上的对象以某种方式打上标记,当追踪结束时,没有被打上标记的对象就被判定为不可触及的,可以被当做垃圾收集。
   
   引用计数收集器
    引用计数是垃圾手机的早起策略。在这种方法中,堆中每一个对象都有一个引用计数。当一个对象被创建,并且指向该对象的引用被分配一个变量,这个对象的引用计数被置为1.当任何其他变量被
   赋值为对这个对象的引用时,计数加1,当一个对象的引用超过生命周期,或者被赋予了一个新值,对象引用的计数减1,任何引用计数为0的对象可以被当做垃圾收集,当一个对象被垃圾收集的时候,
   他引用的任何对象计数值减1,在这种方法中,一个对象呗垃圾收集后可能导致后续其他对象的垃圾收集行动。
    这个方法的好处是,引用计数收集器可以很快的执行,交织在程序的运行之中,这个特性对于程序不能被长时间打断的实施环境很有利,坏处就是,引用计数无法检测出循环。循环的例子中,
   父对象有一个对子对象的引用,子对象又反过来引用了父对象,这些对象永远都不可能计数为0,就算他们已经无法被运行程序的根对象所触及,还有一个坏处就是,每次引用计数的正价或者减少都
   带有来额外的开销。
    因为引用计数的方法固有的缺陷,这种计数现在已经不再为人所接受,现在生活中所遇到的虚拟机更有可能在垃圾收集堆中使用追踪算法。
    
   跟踪收集器
    跟踪收集器追踪从根节点开始的对象引用图,在追踪过程中遇到的对象以某种方式打上标记。总的来说,要么在对象本身设置标记,要么
   用一个独立的位图来设置标记,当追踪结束时,未被标记的对象就知道无法触及的,从而可以被回收。 
    基本的追踪算法被称作为标记并清除,这个名字指出垃圾收集过程的两个阶段,在标记阶段,垃圾收集器遍历引用树,标记每一个遇到的对象,
   在清除阶段,未被标记的对象呗释放,使用的内存被返回到正在执行的程序,在java虚拟机中,清除步骤必须包含对象的终结。
   
   压缩收集器
    java虚拟机的垃圾收集器可能有对付堆碎块的策略,标记并清除收集器通常使用的两种策略是压缩和拷贝。这两种方法都市快速的移动对象来减少
   堆碎块,压缩收集器把活动的对象越过空闲区滑动到堆的一端,在这个过程中,堆的另一端出现一个大的连续空闲区。所有被移动的对象的引用也被更新
   指向新的位置。
    更新被移动的对象的引用有时候通过一个间接对象引用层可以变得更简单,不直接引用堆中的对象,对象的引用实际上指向一个对象句柄表,
   对象句柄才指向堆中对象的实际位置。当对象被移动,只有这个句柄需要被更新为新位置。所有的程序中对这个对象的引用仍然指向这个具有新值的句柄
   而句柄本身没有移动,这种方法简化了消除堆碎块的工作,但是第一次对象访问都带来了性能损失。
   
   
   拷贝收集器
    拷贝垃圾收集器把所有的活动对象移动到一个新的区域,在拷贝的过程中,他们被紧挨着布置,所以可以消除原本他们的就区域的空隙。原有的区域被认为
   都是空闲区。这种方法的好处是对象可以在从根对象开始的遍历过程中随着发现而被拷贝,不再有标记和清除的区分。对象被快速拷贝到新区域,同时转向指针
   仍然留在原来的位置,转向指针可以让垃圾收集器发现已经被转移的对象的引用然后垃圾收集器可以吧这些引用设置为转向指针的值,所以他们现在指向对象的新位置。
    一般的拷贝收集器算法被称为停止并拷贝,在这个方案中,堆被分为两个区域,任何时候都只能使用其中的一个区域,对象在同一个区域中分配,知道这个区域被耗尽,此时
   程序执行被终止,堆被遍历,遍历时
   
   按代收集
    简单的停止并拷贝收集器的缺点是,每一次收集时,所有的活动对象都必须被拷贝,大部分语言的大多数程序都有以下的特点,如果我们全面考虑这些,拷贝算法的这个缺点是可以被改进的
    (1)大多数程序创建的大部分对象都具有很短的生命周期
    (2)大多数程序都创建一些具有非常长生命周期的对象,
    简单的拷贝收集器浪费效率的一个主要原因是,他们每次都把这些生命周期很长的对象来回拷贝,消耗了大量的时间。
    按代收集的收集器通过把对象按照寿命来分组解决这个效率低小的问题,更多的收集那些短暂出现的年幼对象,而非寿命较长的对象,在这个方法里,堆被划分为两个或者更多的子堆,每一个子
    堆为一代对象服务。最年幼的那一代进行最频繁的垃圾收集。因为大多数对象都是短促出现的,只有很小部分的年幼对象可以在他们经历第一次收集后还存活。如果一个最年幼的对象经历了好几次
    垃圾收集后仍然存活,那么这个对象就成长为寿命更高的一代,他被转移到另一个子堆中,年龄更高的每一代的收集都没有年轻的那一带来的频繁,每当对象在他所属的年龄层中变得成熟之后,
    他们就被转移到更高的年龄层中去。
    按代进行的收集技术除了可以应用于拷贝算法,也可以应用与标记并清除算法。不管在那种情况下,把堆按照对象年龄层分解都可以提高最基本的垃圾收集算法的性能。
    
    自适应收集器
     自适应收集器算法李永乐如下事实:在某种情况下某些垃圾收集算法工作的更好,而另外的一些收集算法在另外的情况下工作的更好。自适应算法监视堆中的情形,并且对应的调整为合适的垃圾收集
    技术。在程序调整的时候可能会调整某种简单的垃圾收集算法的参数,也可能快速转换到另一种不同的算法,或者把堆划分为子堆,同时在不同的子堆中使用不同的算法。
     使用自适应方法,java虚拟机实现的设计者不需要只选择一种特定的垃圾收集算法。可以使用多种技术,以便在每种技术最擅长的场合使用他们。
     
     
    火车算法
     垃圾收集算法和明确的释放内存比较起来有一个潜在的缺点,即垃圾手机算法中程序员对安排CPU时间进行内存回收缺乏控制。要精确的预测出何时进行垃圾收集,收集需要多长时间,基本上都是不可能的
     因为垃圾收集一般都会停止整个程序的运行来查找和收集垃圾对象,他们可能在程序执行的任意时刻暂停,并且暂停的时间也无法确定。这种垃圾收集暂停时间有时候长的让用户都注意到了,垃圾收集也可能
     使得程序对事件响应迟钝,无法满足实时系统的要求。如果一种垃圾收集算法可能导致用户可察觉得到的停顿或者使得程序无法适应实时系统的要求,这种算法被称为破坏性的。为了减少垃圾收集和明确释放
     对象之间的潜在差距,设计垃圾收集算法的一个基本目标就是是本质上的破坏性尽可能的减少,如果可能的话,尽可能消除这种破坏性。
     大道非破坏性的垃圾收集的方法是使用渐进式收集算法。渐进式垃圾收集器就是不试图一次性发现并回收所有的不可触及的对象,而是每次发现并回收一部分,因为每次度只有堆的一部分进行垃圾收集,因此理论
     上说每一次收集会持续更短的时间,如果有一个这样的支持渐进收集方法的垃圾收集器,每次可以保证不超过一个最大时间长度,就可以让java虚拟机适应实时环境,限时渐进垃圾收集器在用户环境中也令人满意
     因为这样的收集器可以消除用户可察觉叨叨的垃圾收集停顿。
      通常渐进式收集器都市按代收集的收集器,大部分调用中,都市收集堆的一部分,在本章的前面部分曾经提到,按代收集的收集器把堆划分为两个或者多个年龄层,每一个都拥有自己的子堆,凭经验可知,大部分
     对象都很快消亡,利用这一点,按代收集的垃圾收集器在年幼的子堆中比在年长的子堆中的活动更加频繁,因为除了最高寿的那个年龄层之外,每一个子堆中都可以给定一个最大尺寸,按代收集的收集器可以大体上
     保证在一个最大时间值内渐进的收集所有的对象,成熟对象空间无法给定最大尺寸,因为,任何在其他年龄层中不再合适的对象总要有个去处,他们没有其他地方可以去。
      火车算法的目的为了在成熟对象空间提供限定时间的渐进收集。
     
     车厢。火车和火车站
     火车算法把成熟对象空间划分为固定长度的内存块,算法每次在一个快中单独的执行。火车算大的这个名字来源于算法组织这些块的方式,每一个块属于一个集合,在一个集合
     内的块排了序,这些集合本身也排了序。为了更好的解析算法,把块叫做车厢,把集合叫做火车,使用这个比喻,成熟对象空间扮演火车站的角色。同一个集合中的块被排序,
     就如同一列火车中的车厢是有序的,成熟对象空间中的集合被排序,很像在火车站火车按照轨道1,轨道2,轨道3等排序。
      火车按照他们创建时的顺序分配号码。因此,在火车站,第一列火车会被拉进轨道1,称为火车1,到达的第二列火车被拉进轨道2,称为火车2,下一列到达的火车被拉进轨道3,称为火车
     火车3,一次类推,按照这样的计划,号码较小的火车总是更早的出现的火车,在火车内部,车厢总是被附加到火车的尾部,附加的第一节车厢被称为车厢1,这列车附加的下一节车厢被称为车厢2,
     因此在一列车内部,较小的数字表示更早出现的车厢,这个明明计划给出了成熟对象空间中块的总体顺序。
      显示了三列车,标记为1,2,3.火车1拥有四节车厢,标记为1.1到1.4。火车2拥有三节车厢,标记为2.1到2.3.火车3拥有5节车厢,标记为3.1到3.5
     这种标记车厢的方式是:火车号码加上一个点再加上车厢号码,用这种方式表示出了成熟对象空间中所有的块的总体顺序。车厢1.1在车厢1.2的前面,车厢1.2在车厢1.3的前面,以此类推,火车1的最后一节车厢
     总是在火车2的第一节车厢的前面,所以车厢1.4在车厢2.1之前,同理,车厢2.3也在3.1之前。火车算法每一次执行的时候,只会对一个块执行垃圾收集。也就是说,第一次火车算法执行时,他会收集块1.1.下一次
     执行时会收集块1.2.当他收集了火车1的最后一块,算法在下一次执行时收集火车2的第一块。
      对象从更年轻的年龄层的子堆中提出来进入成熟对象空间,不管何时从年轻年龄层中提出,他们被附加到任何已经存在的火车中,或者专为容纳他们而创建的一例或多列火车中,也就是说,你可以想象新的对象可能
     两种方式到达火车站,要么被打包成车厢,挂到了除了号码最小之外的火车的尾部,要么作为新的火车开进车站。
      车厢收集:每一次火车算法被执行的时候,他要么收集最小数字火车中的最小数字车厢,要么收集整列最小数字火车,算法首先检查指向最小
     数字火车中任何车厢的引用,如果不存在任何来自最小数字火车意外的引用指向他内部包含的对象,那么整列最小数字火车包含的都市垃圾,可以被抛弃。
     这第一步使得火车算法可以一次收集大型的无法在一个块中容纳的循环数据结构。在下面将要描述的火车算法步骤中,这种大型的循环数据结构,保证可以在同一个火车中被销毁。
      假如最小数字火车被认为都是垃圾,火车算法归还火车中所有车厢中的对象并返回。本次火车算法结束,如果最小数字火车并不是都是垃圾,那么算法把他的注意力放到最小数字车厢上
     在这个处理过程中,算法或者转移或者释放车厢中的任何对象,算法首先把所有被最小数在车厢外部的车厢引用的对象转移到其他车厢去,当进行这个移动后,任何保留在车厢内的对象都
     是没有引用的,可以被垃圾收集,火车算法归还整节最小数字车厢占据的空间并且返回。
      保证整列火车中没有循环的数据结构的关键是算法如何移动对象,如果正被收集的车厢中有一个对象存在来自成熟对象空间以外的引用,这个对象呗转移到正在被收集的火车之外的其他车厢
     如果对象被成熟对象空间的其他火车引用,对象就被转移到引用他的那列戳车中去,然后转移过后的对象呗扫描,查找对原车厢的引用,发现的任何被引用的对象都会被转移到引用他的火车中区
     新被转移的对象也被扫描,这个过程不断重复,直到没有任何来自其他火车的引用指向正在被收集的那节车厢,如果接收对象的火车没有空间了,那么算法会创建新的车厢,并且附加到那列火车的尾部。
      一旦没有从成熟对象空间外部来的引用,也没有从成熟对象空间内其他货车来的引用,那么这节正在被收集的车厢剩余的外部引用都市来至同一列火车的其他车厢。算法把这样的对象转移到这节最小数字火车的最后一个车厢去
     然后这些对象被扫描,查找对原被收集车厢的引用,任何新发现的被引用对象也被移动到同一列火车的尾部,也被扫描,这个过程不断重复,知道没有任何形式的引用指向被收集的车厢,然后算法归还整个最小数字车厢占据的空间
     释放所有仍然在车厢中的对象,并且返回。
      因此,在每一次执行时,火车算法或者收集最小数字的火车中的最小车厢,或者收集整列最小数字的火车,火车算法最重要的方面之一,就是他保证大型的循环数据会被完整的收集,即使他们不能被放置在同一个车厢中,因为对象
     被转移到引用他们的火车,相关的对象会变得集中,最后成为垃圾的循环数据结构中的所有对象,不管多大,会被放置到同一列火车中去,增大循环数据结构的大小只会增大最终组成同一列火车的车厢数,因为火车算法在检查最小数字
     车厢之前,首先检查最小数字火车是否完成就是垃圾,他可以收集热河大小的循环数据结构。
      记忆集合和流行对象:火车算法的目标是为了提供限定时间内的按代收集的收集器中成熟对象空间的渐进式收集。因为块可以指定一个最大尺寸限度,并且每一次执行只收集一个块,大部分情况下,火车算法可以保证每次的执行时间在某个最长
     时间限度以内,不过,火车算法不能确保每一次执行都在最长时间限度之内,因为算法不仅仅是拷贝对象。
      为了促进收集过程,火车算法使用了记忆集合,一个记忆集合是一个数据结构,它包含了所有对一节车厢或者一列火车的外部引用。算法为成熟对象空间内每节车厢和每列火车都维护一个记忆集合。所以,一节特定车厢的记忆集合包含了指向车厢内对象的所有引用的集合。
     一个控的记忆集合显示车厢或者火车中的对象都不再被车厢或者火车外的任何变量引用,被遗忘的对象就是不可触及的,可以被垃圾收集。
      记忆集合是一种可以帮助火车算法更加有效的完成工作的技术。当火车算法发现一节车厢的记忆集合是空的时,他就知道车厢里面全是垃圾,可以立即归还这节车厢占用的所有内存,同样,当火车算法发现一列火车的记忆集合是空的时,他可以立即归还整列火车占用的内存,当火车
     算法把一个对象转移到另外一节车厢或者另外一列火车时,记忆集合中的信息有助于他高效的更新所有指向被移动对象的引用,他们就可以指向新的位置。
      虽然火车算法每次执行时需要拷贝的字节总数受限于块的大小,但是移动一个很受欢迎的对象所需要的工作几乎是不可能受限的。每次算法移动一个对象时,他必须遍历对象的记忆集合,更新每一个连接,以便连接指向新的位置,因为指向一个对象的连接数是无法限定的,更新一个被移动
     对象的所有连接需要的总时间长度也是无法限定的,也就是,在特定的条件下,火车算法仍然可能是破坏性的,不过,除了流行对象情况下不太适用外,火车算法在大部分情况下工作的非常好,为成熟多想空间内进行渐进的,非破坏性的垃圾收集提供了很好的方法。
     
     
     终结
      java语言里,一个对象可以拥有终结方法:这个方法时垃圾收集器在释放对象前必须进行,这个可能存在的终结方法使得任何java虚拟机垃圾收集器要完成的工作更加复杂。垃圾收集器必须检查他所发现的不再被引用的对象是否存在finalize方法。
     因为,存在终结方法时,java虚拟机的垃圾收集器必须每次在收集时执行一些额外的步骤。
     首先,垃圾收集器必须使用某种方法检测出不在被引用的对象,然后,他必须检查他检测出的不再被已用的对象是否声明了终结方法, 如果时间允许的话,可能在这个时候垃圾收集过程就着手处理这些存在的终结方法。
      当执行了所有的终结方法以后,垃圾收集器必须从根节点开始再次检查不再被引用的对象,这个步骤是必须的,因为终结方法可能复活了一些不再被引用的对象,使得他们再次被引用,最后,垃圾收集器才能释放哪些在第一次和第二次扫描中发现的都没有被引用的对象。
      为了减少释放内存的时间,在扫描到某些对象拥有终结方法和运行这些终结方法之间,垃圾收集器可能有选择的插入一个步骤,一旦垃圾收集器执行了第一遍扫描,并且找到一些不再被引用的对象需要执行终结,他可以运行一次小型的追踪,从需要执行终结的对象开始,任何
     满足如下条件的对象--从根节点开始不可触及,以及将要被终结的对象开始不可触及--这些对象不可能在执行终结方法时复活,他们可以立即被释放。
      如果一个带终结方法的对象不再被引用,并且他的终结方法运行过了,垃圾收集器必须使用某种方法记住这一点,而不能再次执行这个对象的终结方法。如果这个对象被他自己的终结方法或者其他对象的终结方法复活了,稍后再次不再被引用,垃圾收集器必须像对待一个没有
     终结方法的对象一样对待他。
      使用java编程时,必须记住的一点:是垃圾收集器运行对象的终结方法,因为总是无法预测何时对象会被垃圾收集,所以也无法预测对象的终结方法何时运行,应该避免编写这样的程序,即程序的正确性依赖于对象的终结方法所运行的时机。比如,如果不在被引用的对象的终结方法释放了一个以后
     程序将会用到的资源,这个资源将直到垃圾收集器运行了这个对象的终结方法后才能使用,如果程序在垃圾收集器有机会终结这个不在被引用的对象之前需要这个资源,这个程序将无法得到该资源。
     
     
     对象可触及性的生命周期
     在垃圾收集器看来,堆中的每一个对象都有三种状态之一:可触及的,可复活的,以及不可触及的。如果垃圾收起可以从根节点开始通过追踪触及到这个对象,他就是可触及的,每一个对象都市从可触及状态开始他的生命周期的,只要程序还保留至少一个可以触及的引用到该对象,他就一直保持可触及状态,一旦
     程序释放了所有到该对象的引用,然后这个对象就变成可复活状态。如果一个对象满足如下条件他就处于可复活状态,他在从跟节点开始的追踪图中不可触及,但是可能在垃圾收集器执行某些终结方法时触及。不仅仅是那些申明了finalize方法的对象,而是所有的对象都要经过可复活状态,前面的部分讲到了,通过
     再次触及对象,对象的终结方法可能复活对象本身或者其他对象,因为通过对象自己定义的finalize,或者其他对象的该方法,任何处于可复活状态的对象都可能再次复活,所以垃圾收集器就不能归还可复活对对象的内存。直到他确信不再有任何终结方法有机会吧这个对象复活,在执行所有可复活对象可能申明的finalize方法之后
     ,垃圾收集器会吧这些处于复活状态的对象或者转化为可触及的状态,或者前进到不可触及状态。
      不可触及状态标识着不但对象不再被触及,而且也不可能通过任何终结方法复活,不可触及的对象不再对程序的执行产生影响,可以自由的回收他们所占据的内存。
      
     
     引用对象
     可触及性的三个比较弱的形式涉及从新版本中引入的实体--引用对象,引用对象分装了指向其他对象的连接,被指向的对象称为引用目标,所有的引用对象都是抽象的java,lang.ref.Reference类的子类的实例,Reference类家族如图,包含了三个直接的子类,Softreference,weakreference,phantomreference;Softreference对象封装了对引用目标的软引用,
     weakreference对象封装了对引用目标的弱引用,而phantomreference对象封装了对引用目标的影子引用,强引用和弱引用之间的最基本的差别是,强引用禁止引用目标被垃圾收集,而弱引用不禁止。
      要创建一个软引用,弱引用或影子引用,简单的把强引用传递到对应的引用对象的构造方法中去,
     
     
      在新版中,对原来的三个不可触及性状态--可触及的,可复活的,不可触及的--扩从了三个新状态:软可触及的,弱可触及的,以及影子可触及的,因为这三个新状态表示了三种新的可触及性,
     
     
     
     
     栈和局部变量操作
     
     由于java虚拟机是基于栈的机器,几乎所有java虚拟机的指令都于操作数栈相关,绝大多数指令都会执行自己功能的时候进行入栈,出栈操作。
     
     常量入栈操作
      许多操作码执行常量入栈操作。操作码在执行常量入栈操作之前,使用如下三种方式指明常量的值:常量值隐式包含在操作码内部,常量值在字节码流中如同操作数一样紧随在操作码之后,或者从常量池中取出常量。
     一些操作码自行指明入栈的常量的类型和值,例如,iconst_1操作码告知java虚拟机向栈压入一个值为1的int类型数。java虚拟机为经常压入栈的各种不同类型的数据定义了一些这样的操作码,相对于从字节码流中取出操作数或者指向常量池
     的指令来说,上述这些指令都是冗余指令,但他们更有效率,因为这些指令在字节码流中仅仅占据一个字节的空间,他们提高了字节码的执行效率,并减少了字节码的尺寸,向栈中压入int和float类型值的操作码。
      操作码吧int和float类型压入栈,int和float都是一个字长的值,java栈中的每一个位置的长度都市一个字长,因此,每当一个int或者float类型被压入栈时,他都将占据一个位置。
      操作码将long和double类型值压入栈,long和double类型值是64位长度的值,每当一个long或者double类型的值被压入栈时,他都将占据2个位置。
     还有一个操作码能够将一个隐式申明的常量压入栈,
     
     通用栈操作
      尽管java虚拟机指令集中的大多数指令都只处理一种特殊的类型,但还是有一些指令可以进行类型无关的栈操作。
      
     把局部变量压入栈
      有几个操作码用于把int类型和float类型局部变量压入栈,一些操作码隐式的指向一个通常使用的局部变量位置。
     弹出栈顶部元素,将其赋给局部变量
      对于每个将局部变量压入栈的操作码而言,都存在相对应的弹出栈顶部元素并将其存储到局部变量中的操作码。执行弹出操作的
     操作码助记符可以通过吧执行压入栈操作的操作码助记符中的save改为load的方式来表示。
     
     转换操作码
     
      
      
     
  
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值