栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JVM——常规基础知识整理

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JVM——常规基础知识整理

JVM学习笔记——内容整理源于《深入理解Java虚拟机》 二、Java内存区域与内存溢出异常 2.2 运行时数据区域

2.2.1 程序计数器(线程私有)

​ 程序计数器是一个较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

​ 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,称这类内存区域为“线程私有”的内存。

2.2.2 Java虚拟机栈(线程私有)

​ 虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

2.2.3 本地方法栈(线程私有)

​ 本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法服务。

2.2.4 Java堆(线程共享)

​ Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。

2.2.5 方法区(线程共享)

​ 方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

2.2.6 运行时常量池

​ 运行时常量池是方法区的一部分。Class文件中出了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

2.3 HotSpot虚拟机对象探秘 2.3.1 对象的创建

​ 对象的创建过程:

​ 当java虚拟机遇到一条字节码new指令时:

    检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,那必须先执行相应的类加载过程;

    为新生对象分配内存;

    将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前到TLAB分配时顺便进行;

    对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代信息等。

    执行初始化方法(构造函数)

为对象分配空间的两种方式:

    指针碰撞:假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲方向挪动一端与对象大小相等的距离。

    空闲列表:如果Java堆中的内存不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

解决对象创建过程中线程不安全的两种方案:

对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选方案:

    对分配内存空间的动作进行同步处理——实际上虚拟机时采用CAS配上失败重试的方式保证更新操作的原子性。把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一块小内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只用本地缓冲区用完了,分配新的缓冲区时才需要同步锁定。
2.3.2 对象的内存布局

对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据和对齐填充。

对象头部分包括两类信息:

    用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64比特,官方称它为"Mark Word" 。

    类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。

实例数据部分是对象真正存储的有效信息,即在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

2.3.3 对象的访问定位

对象访问方式是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种。

如果使用句柄访问的话,Java堆中将可能会划分出一块内存来做为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如下图所示。

如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如下图所示。

各自优势:

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针来访问的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销极少成多也是一项极为可观的执行成本。

2.4 实战:OutOfMemoryError异常 2.4.1 Java堆溢出

​ Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息"Java.lang.OutOfMemoryError“会跟随进一步提示”Java heap space"。

​ 要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具对Dump出来的堆转储快照进行分析。

首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏还是内存溢出。若是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。若不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。 2.4.2 虚拟机栈和本地方法栈溢出

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。如果虚拟机的栈内存运行动态扩展,当扩展栈容量无法申请到足够的内存是,将抛出OutOfMemoryError异常。

无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。

2.4.4 本机直接内存溢出

​ 由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。

三、垃圾收集器与内存分配策略 3.1 概述

Java堆和方法区这两个区域有很显著的不确定:

​ 一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。

3.2 对象已死?

​ 判断对象存活或死去的方法

3.2.1 引用计数算法

​ 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

​ Java虚拟机并非用的此算法判断对象是否存活

3.2.2 可达性分析算法

​ 当前主流的商用程序语言(Java、C#)的内存管理子系统,都是通过可达性分析算法来判定对象是否存活的。

​ 可达性分析算法的基本思路就是:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话就是从GC Roots到这个对象不可达时,则证明此对象不可能再被使用。

​ 在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如当前正在运行时的方法所使用到的参数、局部变量、临时变量等。

在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。

在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器。

所有被同步锁(synchronized关键字)持有的对象。

反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性“地加入,共同构成完整的GC Roots集合。

3.2.3 再谈引用

​ Java将引用分为强引用、软引用、弱引用、虚引用四种,这四种引用强度依次逐渐减弱。

强引用是最传统的”引用“的定义,是指在程序代码之中普遍存在的引用赋值,即类似”Object obj = new Object()“这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。软引用是用来描述一个还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。虚引用也称”幽灵引用“或”幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。 3.2.5 回收方法区

​ 方法区的垃圾收集主要回收两部分内容:废弃的变量和不再使用的类型。

​ 判定一个常量是否“废弃”还是相对简单(看是否虚拟机中有地方引用这个字面量),而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

该类的所有实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi,JSP的重加载等,否则通常是很难达成的。该类的对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 3.3 垃圾收集算法 3.3.1 分代收集理论

​ 弱分代假说:绝大多数对象都是朝生夕灭的。

​ 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

​ 多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

​ 跨代引用假说:跨代引用相对与同代引用来说仅占极少数。

3.3.2 标记-清除算法

​ 标记-清除算法如它的名字一样,算法分为”标记“和“清除“两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。

​ 该算法有两个缺点:

执行效率不太稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都对象数量增长而降低。内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 3.3.3 标记-复制算法

​ 标记-复制算法解决标记-清除算法面对大量可回收对象时执行效率低的问题。它将可用内存按容量划分为大小相等的两块,每次都只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都时针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法得代价是将可用内存缩小为原来的一半,空间浪费未免太多了一点。

3.3.4 标记-整理算法

​ 标记-整理算法其中的标记过程仍然和标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一段移动,然后直接清理掉边界以外的内存。

3.5 经典垃圾收集器 3.5.6 CMS收集器

​ CMS收集器整个过程分为四个步骤:

    初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。

    并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。

    重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

    并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

    其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。

优点:并发收集、低停顿。

缺点:

CMS收集器对处理器资源非常敏感由于CMS收集器无法处理“浮动垃圾”,有可能出现“Concurrent Model Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生CMS收集器是一款基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生 3.5.7 Garbage First (G1)收集器

G1是一款主要面向服务端应用的垃圾收集器。

G1收集器的Mixed GC模式:面向堆内存任何部分来组成回收集进行回收,衡量标准不再时它属于哪个分代,二十哪块内存中存放的垃圾数量最多,回收效益最大。

G1收集器的运作过程大致可划分为以下四个步骤:

    初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用运行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时间时有引用变动的对象。最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的STAB记录。筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
六、类文件结构 6.3 Class类文件的结构

​ Class文件是一组以字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用单个字符以上空间的数据项时,则会按照高位在前的方式分割成若干个字节进行存储。

​ 根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。后面的解析都要以这两种数据类型为基础。

​ 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

​ 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以”_info“结尾。表用于描述由层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表,这张表由下表的数据项按严格顺序排列构成。

6.3.1 魔数与Class文件的版本

​ 每个Class文件的头4个字节被称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。

​ 紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号,第7和第8字节是主版本号。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。

6.3.2 常量池

​ 紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。

​ 由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。

​ Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始。

​ 常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语音层面的常量概念,如文本字符串】被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:

被模块导出或者开放的包类和接口的全限定名字段的名称和描述符方法的名称和描述符方法句柄和方法类型动态调用点和动态常量 6.3.3 访问标志

​ 在常量池结束之后,紧接着的2个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final;等等。

6.3.4 类索引、父类索引与接口索引集合

​ 类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。类索引用来确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中。

​ 类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。对于接口索引集合,入口的第一项u2类型的数据为接口计数器,表示索引表的容量。

6.3.5 字段表集合

​ 字段表用于描述接口或者类中声明的变量。

6.3.6 方法表集合

​ 方法表的结构如同字段表一样,依次包括访问控制、名称索引、描述符索引、属性表集合几项。

​ 特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合。

6.3.7 属性表集合

    Code属性

    Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。

    Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

    Exceptions属性

    Exceptions属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。

    LineNumberTable属性

    LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必须的属性,但默认会生成到Class文件之中,可以在Javac中使用-g:none或-g:lines选项来取消或要求生成这项信息。

    如果选择不生成LineNumberTable属性,对程序运行产生的最主要影响就是当抛出异常时,堆栈中将不会西安市出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。

    LocalVariableTable及LocalVariableTypeTable属性

    LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的比那俩之间的关系,它也不是运行时必须的属性,但默认会生成到Class文件之中,可以在Javac中使用-g:none或-g:vars选项来取消或要求生成这项信息。

    如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,譬如IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。

    LocalVariableTypeTable属性使用字段的特征签名来完成泛型的描述。

    SourceFile及SourceDebugExtension属性

    SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。

    如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

    SourceDebugExtension属性用于存储额外的代码调试信息。一个类中最多只允许存在一个SourceDebugExtension属性。

    ConstantValue属性

    ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。

    InnerClasses属性

    InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义类内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。

    Deprecated及Synthetic属性

    Deprecated及Synthetic属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

    Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以用过代码中使用”@deprecated“注解进行设置。

    Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,在JDK5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位。

    StackMapTable属性

    StackMapTable属性在JDK6增加到Class文件规范之中,它是一个相当复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

    StackMapTable属性中包含零至多个栈映射帧,每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。

    Signature属性

    任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息。

    BootstrapMethods属性

    BootstrapMethods属性用于保存invokedynamic指令引用的引导方法限定符。

    MethodParameters属性

    MethodParameters属性的作用是记录方法的各个形参名称和信息。

    模块化相关属性

    Class文件格式也扩展了Module、ModulePackages和ModuleMainClass三个属性用于支持Java模块化相关功能。

    Module属性是一个非常复杂的变长属性,除了表示该模块的名称、版本、标志信息以外,还存储了这个模块requires、exports、opens、uses和provides定义的全部内容。

    ModulePackages属性是另一个用于支持Java模块化的变长属性,它用于描述该模块中所有的包,不论是不是被export或者open的。

    ModuleMainClass属性是一个定长属性,用于确定该模块的主类。

    运行时注解相关属性

    JDK5中,为了存储源码中注解信息,Class文件同步增加了RuntimeVisibleAnnotations、RuntmeInvisiableAnnotations、RuntimeVisibleParameterAnnotations和RuntimeInvisibleParameterAnnotations四个属性。到JDK8中,增加了RuntimeVisibleTypeAnnotations和RuntimeInvisibleTypeAnnotation两个属性。这六个属性不论结构还是功能都比较雷同,因此以RuntimeVisibleAnnotations为代表进行介绍。

    RuntimeVisibleAnnotations是一个变长属性,它记录了类、字段或方法的声明上记录运行时可见注解,当使用反射API来获取类、字段或者方法上的注解时,返回值就是通过这个属性来取到的。

6.4 字节码指令简介

​ Java虚拟机的指令由一个字节长度的,代表有某钟特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表次操作所需的参数(称为操作数)构成。由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。

6.4.1 字节码与数据类型

​ 对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助记符中没有明确指明操作类型的字母,例如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。

6.4.2 加载和存储指令

​ 加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括:

将一个局部变量加载到操作栈:iload、iload_、lload、lload_、fload、fload_、dload、dload_、aload、aload_将一个数值从操作数栈存储到局部变量表:istore、istore_、lstore、lstore_、fstore、fstore_、dstore、dstore_、astore、astore_将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_、lconst_、fconst_、dconst_扩充局部变量表的访问索引的指令:wide 6.4.3 运算指令

​ 算术指令用于对操作数栈上的两个值进行某种特定运算,并把结果重新存入到操作栈顶。大体上运算指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令。

​ 所有的算术指令包括:

加法指令:iadd、ladd、fadd、dadd减法指令:isub、lsub、fsub、dsub乘法指令:imul、lmul、fmul、dmul除法指令:idiv、ldiv、fdiv、ddiv求余指令:irem、lrem、frem、drem取反指令:ineg、lneg、fneg、dneg位移指令:ishl、ishr、iushr、lshl、lshr、lushr按位或指令:ior、lor按位与指令:iand、land按位异或指令:ixor、lxor局部变量自增指令:iinc比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp 6.4.4 类型转换指令

​ 类型转换指令可以将两种不同的数值类型相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

​ Java虚拟机直接支持(即转换时无须显示的转换指令)以下数值类型的宽化类型转换(即小范围类型向大范围类型的安全转换):

int类型到long、float或者double类型

long类型到float、double类型

float类型到double类型

与之相对的,处理窄化类型转换时,就必须显示地使用转换指令来完成,这些转换指令包括i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同数量级的情况、转换过程很可能会导致数值的精度丢失。

​ Java虚拟机将一个浮点值窄化转换为整数类型T(T限于int或long类型之一)的时候,必须遵循以下转换规则:

如果浮点值时NaN,那转换结果就是int或long类型的0。如果浮点值不是无穷大的话,浮点值使用IEEE754的向零舍入模式取整,获得整数值v。如果v在目标类型T(int或long)的表示范围之内,那转换结果就是v;否则,将根据v的符号,转换为T所能表示的最大或者最小正数。 6.4.5 对象创建与访问指令

​ 对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素,这些指令包括:

创建类实例的指令:new创建数组的指令:newarray、anewarray、multianewarray访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic把一个数组元素加载到操作数栈额指令:baload、caload、saload、iaload、laload、faload、daload、aaload将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore取数组长的指令:arraylength检查类实例类型的指令:instanceof、checkcast 6.4.6 操作数栈管理指令

​ 如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:

将操作数栈的栈顶一个或两个元素出栈:pop、pop2复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2将栈最顶端的两个数值互换:swap 6.4.7 控制转移指令

​ 控制转移指令可以让Java虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令)的下一条指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值。控制转移指令包括:

条件分支:ifeq、ifit、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne复合条件分支:tableswitch、lookupswitch无条件分支:goto、goto_w、jsr、jsr_w、ret 6.4.8 方法调用和返回指令

invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。invokeinterface指令:用于调用接口方法,它会在运行时搜素一个实现了这个接口方法的对象,找出合适的方法进行调用。invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。invokestatic指令:用于调用类静态方法(static方法)。invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法。并执行该方法。前面四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。

6.4.9 异常处理指令

​ 在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经不用了),而是采用异常表来完成。

6.4.10 同步指令

​ Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将它称为”锁“)来实现的。

​ 方法级的同步是隐式的,无需通过字节码指令来控制,它实现在方法调用和返回操作之中。

​ 同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义。

​ 编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须有其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。

七、虚拟机类加载机制

​ Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。

7.2 类加载的时机

​ 一个类型从被加载到虚拟机内存中开始,到卸载出内存为止2,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备、解析三个部分统称为连接。

7.3 类加载的过程 7.3.1 加载

​ 在加载阶段,Java虚拟机需要完成一下三件事情:

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

​ 一个数组类创建过程遵循以下规则:

如果数组的组件类型(指的是数组去掉一个维度的类型)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将被标识在加载该组件类型的类加载器的类名称空间上。如果数组的组件类型不是引用类型,Java虚拟机将会把数组C标记为与启动类加载器关联。数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为public,可被所有的类和接口访问到。 7.3.2 验证

​ 验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机的安全。

​ 验证阶段大致上会完成以下四个阶段的检验工作:文件格式验证、元数据验证、字节码验证和符号引用验证。

    文件格式验证,这个阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。元数据验证,这个阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求。字节码验证,这个阶段主要目的是通过数据流分析和控制流分析,确定程序语音是合法的、符合逻辑的。符合引用验证,这个阶段主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将会抛出一个java.lang.IncompatibleClassChangError的子类异常。

​ 验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行期没有任何影响了。

7.3.3 准备

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

7.3.4 解析

​ 解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。

​ 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定时已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。

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

7.3.5 初始化

​ 初始化阶段就是执行类构造器()方法的过程。

<​clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

()方法与类的构造函数(即在虚拟机视角中的实例构造器()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的()方法执行前,父类的()方法已经执行完毕。因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object。

​ 由于父类的clinit()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。

​ 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成()方法。但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的()方法。

​ Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行完毕方法。如果在一个类的方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

7.4 类加载器

​ Java虚拟机设计团队有意把类加载阶段中的”通过一个类的全限定名来获取描述该类的二进制字节流“这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为”类加载器“。

7.4.1 类与类加载器

​ 比较两个类是否”相等“,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

7.4.2 双亲委派模型

​ 站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。

​ 双亲委派模型的工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/764049.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号