JVM虚拟机-垃圾收集器

发布在 jvm

摘要

垃圾收集器(GC,Garbage Collector):自动化的内存管理工具,垃圾收集器主要做以下几种工作:

  • 在新生代中给新对象分配内存,并将老对象放到老年代中去管理。(Allocating objects to a young generation and promoting aged objects into an old generation.)
  • Java HotSpot VM在Java堆内存占用率超过默认阈值时触发标记阶段,标记阶段将通过并行标记的方式在老年代中寻找存活的对象。(Finding live objects in the old generation through a concurrent (parallel) marking phase. The Java HotSpot VM triggers the marking phase when the total Java heap occupancy exceeds the default threshold.)
  • 通过并行复制的算法压缩存活的对象从而恢复可用内存。(Recovering free memory by compacting live objects through parallel copying.)

    如何选择垃圾收集器才能使应用的性能达到最优?HotSpot团队都难以对这个问题给出完美的答案。一般而言,暂停应用程序进行垃圾回收的频率越低且每次垃圾回收的时间越短,应用程序的性能就会越高。但是拥有许多大数据对象的多线程高事务率的大型应用的垃圾回收很难同时满足以上两点。

    阿姆达定理(系统中对某一部件采用更快执行方式所能获得的系统性能改进程度,取决于这种执行方式被使用的频率,或所占总执行时间的比例。)说明,给定问题的并行加速度由问题的顺序执行部分限制其性能。其意为者部分工作并不能完美的并行化,只能串行的执行。在JAVA平台也受此定理的影响,JAVA SE 1.4之前并不支持并行垃圾回收,故垃圾收集器在多处理器下对性能的影响随程序的并行化而增长。

引言

小至简单的 public static void main 程序,大至拥有数百台服务器集群的大型WEB服务,全世界目前有几十亿部设备正在使用JAVA。为了支持这种多样化的部署,Java HotSpot虚拟机(Java HotSpot VM)提供了多个垃圾收集器,每个垃圾回收器被设计为满足不同的需求。JAVA应用程序可以根据自身运行的类型(调试版或生产版)与载体物理机的配置情况,自动的去选择合适的垃圾收集器。但当需求上需要我们写出更加高性能的应用程序时,JAVA的默认配置就很难再满足我们的需求。此时便需要作为开发或运维人员的你通过设置一些JVM参数,明确的指定垃圾收集器等方式,来达到性能需求上所指明的条件。

在阅读本文之前,我希望你已经读过我的上篇文章JVM虚拟机-垃圾回收算法,对常见的垃圾回收算法及其在HotSpot虚拟机下的实现有了解,如果说垃圾回收算法是理论基础,那么本文所介绍的垃圾收集器就是JVM垃圾收集的具体实现。另外希望你在工作或研究的过程中感受过JAVA默认的垃圾回收机制给你带来过性能上的压力,因为只有亲身的经历过,才能感受到了解垃圾收集器的机制的重要性。

本文会以JAVA8版本的HotSpot虚拟机为基础环境逐个的介绍每个垃圾收集器的实现原理并分析其优缺点,简略的探讨一下什么样的应用在什么样的环境下应该选用哪种垃圾收集器。本文的目标是能够帮助读者对应用程序进行JVM调优,做出高性能的应用。

本文着重对CMS和G1两款垃圾收集器进行介绍分析。

基础

HotSpot虚拟机提供了三种大类的垃圾收集器,每种垃圾收集器都有不同的特征和性能表现。简单描述如下:

  • 串行类垃圾收集器( serial collector):
    串行类垃圾收集器是最受争议的垃圾收集器,串行垃圾收集器使用单个线程完成所有的垃圾收集工作。其工作时不需要与其它的线程进行交互,所以垃圾回收的效率是很高的。串行类垃圾收集器尤其适合单CPU的硬件平台上使用,且在使用数据集小于100MB的多CPU硬件平台上也能取得很好的效果。HotSpot在Client模式下默认使用串行垃圾收集器,在其它环境下也可以通过使用 -XX:+UseSerialGC 参数来明确指定使用串行垃圾收集器。

  • 并行垃圾收集器(Parallel Collector):
    并行垃圾收集器又被称为吞吐量优先垃圾收集器(Throughput Collector),通常我们所指的吞吐量优先垃圾收集器指的都是适用于新生代的 Parallel Scavenge 垃圾收集器。并行垃圾收集器能够显著的减少多CPU环境下中型或大型应用垃圾收集所消耗的时间,HotSpot Server版本默认使用的使用的垃圾收集器就是并行垃圾收集器。通过启动虚拟机的时候指定参数 -XX:+UseParallelGC 可以手动指定年轻代使用吞吐量优先收集器。与年轻代相呼应的还有老年代并行垃圾收集器(Oracle 称其为 Parallel compaction 意为并行压缩),当使用参数 -XX:+UseParallelGC 时,JVM老年代的默认开启 Parallel compaction ,同时也可以通过参数 -XX:-UseParallelOldGC 来手动开启。

    • 并发垃圾收集器(Concurrent Collector):
      从字面上理解,并发垃圾收集器的意思是让用户线程和垃圾回收线程并发运行。其在大中型应用系统中并发垃圾收集器工作的过程中仅仅有很短的 ‘Stop the world’ ,且相对于并行垃圾收集器能够大幅度的减小系统响应时间(可能以牺牲系统吞吐量为代价)。JAVA8版本的HotSpot虚拟机下目前有两款并发收集器,CMS垃圾收集器(通过参数 XX:+UseConcMarkSweepGC 开启)和G1(通过参数 -XX:+UseG1GC 开启)。

探索

引用

本文是对JVM垃圾收集器的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java® Virtual Machine Specification · Java SE 8 Edition》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》
  3. 《Java Platform,Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》

    非原创声明

    本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

评论和共享

JVM虚拟机-垃圾回收算法

发布在 java, jvm

引言

C和C++ 程序员能够直接操作内存,凭自己的需要来决定何时去申请多大内存,何时去释放这块内存。他们甚至可以使用指针,来确定的去操作某块内存地址。作为后来者的JAVA远远不如他们强大,我认识的JAVA程序员可能有半数都从未去关注过内存。在使用JAVA进行业务开发时,我们不需要去关注对象到底存储在内存的哪个位置,也不需要关注这个对象到底占用了多大的内存空间,更不需要特意的为某个对象去释放内存空间。需要我们关注的,只有如何完成产品需求,在规定时间内交付合格的产品给客户。如果你对编程的看法跟上面所述的类似,或者你很认同“完成比完美更重要”这条工程师信条,那么这篇文章就不太适合你来阅读了。

内存控制给编程界造成了一个具有魔性的圈,圈内的人想出去,圈外的人想进来。就像C程序员早就受够了内存的申请与释放的折磨,受够了各种内存错误。而很多像你一样的JAVA程序员,立志要写出高性能应用的人,一直在用心地思索怎么才能写出性能更好的多线程应用,当这个追求达到一定层次的时候,你甚至已经开始在乎JAVA垃圾回收所消耗的时间了。由于JAVA自身的限制,我们没办法进入到内存控制的圈内,但我们可以通过设置JVM参数等方式,选择适当的垃圾收集器或设置垃圾回收线程对JAVA的垃圾回收机制进行优化。

概述

本文的重点是介绍JVM虚拟机下常用的垃圾回收算法的理论知识,并不介绍具体算法实现代码。在阅读本文之前,希望读者已经掌握了JVM的内存划分设计、对象引用的可达性分析算法与JAVA的四种对象引用级别(Strong Reference/Soft Reference/Weak Reference/Phantom Reference)等相关知识。在本文之后,我还会再去整理一些关于垃圾收集器等方面的知识。文中所涉及的代码或理论都已在JDK8中进行验证。

垃圾回收算法

标记-清除算法(Mark-Sweep)

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记-清除算法的工作过程如图1所示。

图1 标记-清除算法示意图

  • 优点: 算法简单清晰,其它垃圾收集算法都是据此算法的思想并对其不足改进而得到的。
  • 缺点: 1.效率低,标记和清除需要遍历内存,效率极低。2.回收内存之后产生大量的内存碎片,当内存碎片过多时,应用需要给大对象分配内存时无法分配连续内存。

复制算法(Copying)

将内存按容量划分为完全等大小的两块,每次只使用其中一块内存。当这一块内存用完后,将还活着的对象全部复制到另一块上面,然后把这一块上已使用的内存空间全部清理掉。其示意图如图二所示。


图2 复制算法示意图

  • 优点: 实现简单,运行高效。每次都是对整个半区进行内存回收,内存分配时无需考虑内存碎片等复杂情况。
  • 缺点: 浪费内存空间。在最坏的情况下,这种垃圾回收算法可能浪费了一半的内存空间。

    在JAVA应用中,90%之上的对象的生存时间都极短,所以JVM把内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中一块Survior空间。当需要进行垃圾回收时,将Eden和Survivor中还处于可达状态的对象一次性的复制到另一块Survivor空间中,最后清空Eden和刚刚使用的Survivor空间。当Survivor空间不够用时,还会将长时间存活的对象转存的老年代中。在HotSpot虚拟机中,默认的Eden和Survivor空间的比例是8:1,这样可以做到只浪费10%的内存。

标记-整理算法(Mark-Compact)

标记-整理算法的标记过程与标记-清除算法的标记过程类似,但在标记完成后并不是直接对可回收的对象进行清理,而是让所有正在存活的对象都向前端移动,然后直接清理掉边界以外的内存。其示意图见图3.


图3 标记-整理算法示意图

  • 优点: 节省内存空间,提升内存运用率,且不会产生内存碎片。
  • 缺点: 性能低。

分代收集算法(Generational Collection)

根据对象的存活时间把内存分为新生代和老年代,根据个代对象的存活特点,每个代采用不同的垃圾回收算法。

分代收集就是根据不同代的特性,使用最合适的垃圾回收算法进行垃圾回收。如新生代中,每次垃圾收集都会有大量的对象死去,只有极小部分对象存活,所以更适合复制算法。老年代中对象存活率高,且没有额外的内存空间为它进行分配担保,所以更适合用标记-清除或标记-整理算法。

HotSpot算法实现

枚举根节点(GC Roots)

在垃圾回收时,我们要想办法找出哪些对象是存活的,一般会选取一些被称为GC Root的对象,从这些对象开始枚举。在进行GC Root枚举时要求所有对象停下来,也就是JVM所称的“Stop the world”。所有的算法实现都会将虚拟机停下来的,否则分析结果的准确性将无法保证。

由于HotSpot采用准确式GC,该技术主要功能就是让虚拟机可以准确的知道内存中某个位置的数据类型是什么,比如某个内存位置到底是一个整型的变量,还是对某个对象的 reference。这样在进行 GC Roots 枚举时,只需要枚举 reference 类型的即可。在能够准确地确定 Java 堆和方法区等 reference 准确位置之后,HotSpot 就能极大地缩短 GC Roots 枚举时间,所以当执行系统停顿下来之后,虚拟机不需要遍历所有的根节点和上下文去确定GC Roots,而是存在着一个OopMap的数据结构来达到这个目的。

在类加载完成的时候,虚拟机就会把什么类的什么偏移上是什么类型的数据计算出来。在JIT编译的时候也会在特定位置记下在寄存器和栈中哪些位置是引用,GC在扫描时就可直接得到信息。

安全点(Safepoint)

Safepoint:会导致 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那么将需要大量的额外空间,这样对导致 GC 成本很高,所以 HotSpot 只在 “特定位置” 记录这些信息,这些位置被称为 安全点(Safepoint)。并非程序在任意时刻都可以停顿下来进行 GC,而只有程序到达 安全点(Safepoint) 以后才可以停顿下来进行 GC;所以安全点既不能太少,以至于 GC 过程等待程序到达安全点的时间过长,也不能太多,以至于 GC 过程带来的成本过高。

由于在 GC 过程中必须保证程序已执行,那么也就是说 必须等待所有线程都到达安全点上方可进行 GC。一般来说有两种解决方案可以选择:

  • 抢先式中断:不需要线程的执行代码去主动配合,当发生 GC 时,先强制中断所有线程,然后如果发现某些线程未处于安全点,那么将其唤醒,直至其到达安全点再次将其中断;这样一直等待所有线程都在安全点后开始 GC。现在几乎没有虚拟机使用这种方式。

  • 主动式中断:不强制中断线程,只是简单地设置一个中断标记,各个线程在执行时轮询这个标记,一旦发现标记被改变(出现中断标记)时,那么将运行到安全点后自己中断挂起;目前所有商用虚拟机全部采用主动式中断。

安全区(Safe Region)

安全点机制仅仅是保证了程序执行时不需要太长时间就可以进入一个安全点进行 GC 动作,但是当特殊情况时,比如线程休眠、线程阻塞等状态的情况下,显然 JVM 不可能一直等待被阻塞或休眠的线程正常唤醒执行,此时就引入了安全区的概念。

安全区(Safe Region):安全区域是指在一段代码区域内,对象引用关系等不会发生变化,在此区域内任意位置开始 GC 都是安全的。线程运行到Safe Region中的代码时,首先标记自己进入了安全区,然后在这段区域内,如果线程发生了阻塞、休眠等操作,JVM 发起 GC 时将忽略这些处于安全区的线程。当线程再次被唤醒时,首先他会检查是否完成了 GC Roots枚举(或这个GC过程),然后选择是否继续执行,否则将继续等待 GC 的完成。

引用

本文是对JVM垃圾收集算法的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java® Virtual Machine Specification · Java SE 8 Edition》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》
  3. 《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》

    非原创声明

    本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到 weiwei02@vip.qq.com
我的github: https://github.com/weiwei02/
我相信技术能够改变世界。

评论和共享

class文件介绍

发布在 java, jvm

非原创声明

本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

*.class文件介绍

一般来讲.class文件是.java文件在编译器编译后生成的jvm能够运行的文件,*.class文件又常被称为字节码文件。java在创始之初,就提倡“一次编写,处处运行的概念”,在当今编程圈中这个概念早已不是什么特例。java通过将开发人员所编写的java代码编译成class文件,然后由jvm虚拟机在执行时将不分平台的class文件中的字节码,再翻译成机器码,交给硬件执行。java就是靠jvm虚拟机的这个设计来实现与平台无关的特性的。class文件不但与硬件平台和操作系统无关,也和具体的编程语言无关,就目前来说,如函数式编程语言scala与Groovy都可以通过自己的编译器将源代码编译成class文件,在jvm上运行。
综合来讲,class文件有以下两点特性:

  • 与硬件和操作系统平台无关
  • 与源码所使用的编程语言无关

class类文件的结构

每一个class文件都唯一对应着java类或接口枚举等定义信息,但类不一定都定义在class文件中,类可能是由类加载器动态生成的。

class文件所以被称之为字节码据我猜测可能是因为class文件以8位(1字节)为单位进行存储的二进制信息。字节码中各个数据项目严格按照顺序紧凑的排列,中间没有任何分割符。在需要存储整型或浮点型这些大于8位的数据项目时,则会使用Big-Endian的字节序进行存储,将最高位字节放在地址最低位,最低位字节放在地址最高位。

class文件格式采用表来存储数据,表中有无符号数和表两种数据类型。对于这两种数据类型说明如下:

  • 无符号数: 无符号数是基本的数据类型,可以用来表示数字、索引引用、数量值或者按照UTF-8编码构成的字符串。如u1,u2,u4,u8分别代表1,2,4,8个字节字节的无符号数。
  • : 表是由多个无符号数或者其它表作为数据项所组成的复合数据结构,表用于描述层次关系和复合的数据结构,整个class文件就是一张表。通常表以 _info 结尾。如表1就是一个class的表示例。
类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 asscess_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods method_count
u2 attributes_count 1
attribute_info attributes attributes_count

表 1 class文件格式

class文件的顺序和格式必须严格实现按照上述规则,否则jvm将不能识别执行。

Magic Number 魔数

魔数是一个和文件后缀名相似的用于文件格式识别的约定,一般规定文件内容开头的前几个字节为文件的魔数。不同于文件后缀名很容易被用户以重命名的方式进行更改,文件的魔数作为识别手段可以更安全的确定文件的可用性。

class文件使用前4个字节作为魔数,来确定.class文件是否是一个能够被虚拟机识别的文件,其值是 0xCAFFEBABE 。

版本

class文件的第5-6个字节代表的可执行该class文件的目标虚拟机的最低次版本号(Minor Version),第7-8个字节是主版本号(Major Version)。java虚拟机可以运行比当前虚拟机版本号低的class文件,拒绝运行版本号不合法,或比自己版本高的class文件。JDK1.1的版本号是45,之后的每个大版本发布都把主版本号加1,如JDK1.2主版本号是46,JDK8的版本号是52。
JDK在编译java文件是可以通过 javac -target 1.6 ...命令来指定编译后的class文件可以在1.6的虚拟机版本上运行。

常量池

常量池是class文件中第一个表类型的数据项目,常量池是class文件中的资源仓库,是class文件结构中与其它项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一。

class文件里紧随版本之后的数据项是常量池,由于常量池的数量是不固定的,所以在常量池的数据项之前放置的有一个u2类型的数据,代表常量池的大小。常量池大小的初始值是1,如常量池的大小的数据如果显示的是10,就代表该class文件中有9个常量。
常量池中主要存放两大类常量:Literal(字面常量)、Symbolic Reference(符号引用常量)。字面常量类似于java中常量的概念,如文本字符串、final关键字所声明的常量等。而符号引用常量则是编译中的概念,主要包括以下三种类型的常量。

  • 符号常量

    • 类和接口的全限定名(Fully Qualified Name)
    • 字段名称和描述符(Descriptor)
    • 方法的名称和描述符

    class文件中不会保存各个方法或字段在内存中的布局信息,而是在虚拟机加载class文件时进行动态的连接。虚拟机在运行class文件时从常量池中获取对应的符号引用,再在创建类或者运行时解析连接到具体的内存地址当中。
    常量池中的每一个常量都是一个表,在JDK8中有14种表结构的常量表。如表 2 所示。

类型 标识 描述 
CONSTANT_utf8_info 1  UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度浮点型字面量
CONSTANT_Class_info 类或接口的符号引用
CONSTANT_String_info 字符串类型字面量
CONSTANT_Fieldref_info 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MothodType_info 16 标志方法类型
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

表 2 常量池数据项目类型表

这14种常量结构表开始的第一个字节都是一个u1类型的标识位,其值就是表2中每项常量表类型所对应的标识列的值,代表当前常量属于哪种常量类型。这14种常量类型各自有自己不同的表结构,详情如表 3 所示。

常量 项目 类型 描述
CONSTANT_Utf8_info tag u1 值为1
length u2 UTF-8编码的字符串占用的字节数
bytes u1 utf-8编码的字符串
CONSTANT_Integer_info tag u1 值为3
bytes u4 按照Big-Endian存储的int值
CONSTANT_Float_info tag u1 4
bytes u4 按照Big-Endian存储的float值
CONSTANT_Long_info tag u1 5
bytes u8 按照Big-Endian存储的long值
CONSTANT_Double_info tag u1 6
bytes u8 按照Big-Endian存储的long值double值
CONSTANT_Class_info tag u1 7
index u2 指向全限定名常量项的索引
CONSTANT_String_info tag u1 8
index u2 指向字符串常量的索引
CONSTANT_Fieldref_info tag u1 9
index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_Methodref_info tag u1 10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_InterfaceMethodref_info tag u1 11
index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_NameAndType_info tag u1 12
index u2 指向该字段或方法名称常量的索引值
index u2 指向该字段或方法描述符常量的索引值
CONSTANT_MethodHandle_info tag u1 15
reference_kind u1 值必须1~9,它决定了方法句柄的的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 对常量池的有效索引
CONSTANT_MethodType_info tag u1 16
description_index u2 对常量池中方法描述符的有效索引常量池在该处的索引必须是CONSTANT_Utf8_info的结构,表示方法的描述符。
CONSTANT_InvokeDynamic_info tag u1 18
bootstap_method_attr_index u2 对当前class文件中引导方法表的bootstap_methods[]数组的有效索引
name_and_type_index u2 对当前常量池的有效索引,常量池在此处必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述。

表 3 常量池中14中常量的结构总表

在表 3 中可以看到class文件所支持的所有的常量的结构信息,另外由于class中的类名、方法、字段都要用CONSTANT_Utf8_info型的常量来描述名称,所以CONSTANT_Utf8_info的最大长度也是java中类名、方法名或字段的最大长度65535,如果超出这个最大长度,便会无法编译。

访问标识

在class文件中位于常量池之后的是访问权限标识。class文件的访问标识是用于修饰这个class文件所代表的的类或接口本身的一些属性。访问标识是用一个u2类型的数据来代表的,如0x0001代表public,0x0020代表这是一个接口。具体的访问标识和标志值的对应关系见表4.访问标识是按位进行标识的,一个u2类型的数据有16位,目前其中的8位都被赋予了实际意义。

标识名称 标识值 意义
ACC_PUBLIC 0X0001 public的访问权限
ACC_FINAL 0x0010 如果该class文件代表一个类,那么该类是final修饰的(只能类才能拥有此标识)
ACC_SUPER 0x0020 使用invokespecial字节码指令的新语义
ACC_INTERFACE 0x0200 接口
ACC_ABSTRACT 0x0400 abstract(只有接口和抽象类有效)
ACC_SYNTHETIC 0x10000 本类不是用户代码所产生的类
ACC_ANNOTATION 0x2000 注解
ACC_ENUM 0x4000 枚举

表4 访问标识和标志值的对应关系表

类索引

类索引是一个u2类型的数据,用于确定这个类的全限定名。其索引值指向一个类型为CONSTANT_Class_info的常量。

父类索引

父类索引指向着当前类所继承的类的父类的全限定名,其类型信息与类索引相同。所有java类都有父类索引。

接口索引

java不支持多重继承,所以只有一个父类索引。但一个java类或接口可以实现多个其它接口,所以class的接口索引是一个数量并不固定的集合。
在声明接口索引之前,class文件首先会声明一个u2类型的数据代表接口索引的总数量,其后面紧跟者就是接口索引集合,如果该类没有实现任何接口,那么接口索引的总数量的值就是0。接口索引同样也指向常量池中的CONSTANT_Class_info类型的常量,使用CONSTANT_Utf8_info的字符串常量来表示接口的全限定名。

字段表集合

20170701 00:10:00 编辑到字段表集合

字段集合用于描述接口或类中所声明的全局变量。如public,static、final等词都是用来描述某个字段的修饰词。因为java中可用来修饰字段的修饰符有限,所以class文件对于字段的修饰符的表示是通过使用标识位来实现的。表5详细分析了字段表的数据结构。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attrobute_info attributes attributes_count

表5 字段表的数据结构

access_flags标识位中存放的是字段的修饰符,使用标志位进行标识区分。具体区分方式如表6字段访问标识表所示。

标识名称 标识值 含义
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static
ACC_FINAL 0x0010 final
ACC_VOLATILE 0x0040 volatile
ACC_TRANSIENT 0x0080 transient
ACC_SYNTHETIC 0x1000 该字段是否由编译器自动生成的
ACC_ENUM 0x4000 字段是否是enum

表6 字段访问标识对应表

name_index 和 descriptor_index 都是对常量池索引的引用,name_index引用的是字段的名称,descriptor_index引用的是字段和方法的描述符。

20170801 00:10:00 编辑到字段表集合

引用

本文是对class文件的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java Virtual Machine Specification》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到 weiwei02@vip.qq.com

from weiwei.wang 20170625

评论和共享

java内存模型

发布在 java, jvm

非原创声明

本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

JAVA内存模型

犹记得大学时操作系统课上,我们迷茫的眼神注视着带着厚眼镜教授向我们一遍遍的强调,一个程序最少有一个进程组成,进程是操作系统提供独立资源供应用程序运行的基本单位。另外老师向我们讲到,为了更好的提高计算机的并行计算能力,计算机科学家们又设计了线程。线程是比进程更小的单位,一个进程可以由多个线程组成。同时线程也是在得到cpu时间片时可运行的最小的单位。尤记得在这些理论基础下,我慢慢的学会了使用C在LINUX环境下使用多进程和多线程进行编程。这些并发API都是LINUX提供的标准API,程序在编译链接之后可以直接调用通过系统内核创建进程或线程,在类UNIX操作系统下这样去做可以让程序有更高的性能。但是这种编程方式所编写出来的代码是与操作系统绑定的,我在linux下明明可以完美运行的代码,在windows下连编译甚至都做不到。除非是用 windows API重新把与操作系统进行交互的那些代码给替换掉,否则就不要想着让程序去跨平台运行了。
在进行并发编程时相对于C,我更喜欢java的编程体验。java消除操作系统之间的差异,原生的对多线程应用提供了很好的支持,特别是在jdk1.5之后,jdk还提供了currency包,更好的让程序员们无需去关注并发的难点与细节,更专心的关注应用的业务需求实现。
在多线程编程中最长遇到的问题就是线程安全问题,其中又以内存中的数据安全问题最为常见(这个数据安全指的是发生脏读、幻读等并发编程中会遇到的错误)。为了消除不同硬件和操作系统对内存操作的差异,在硬件设备和操作系统的内存模型之上,java虚拟机规范定义了一种java内存模型(JAVA Memory Model,简称JMM),将内存分为了工作内存和主内存。JMM主要定义了JVM中在内存中操作变量的规则和细节,用来解决在并发竞争对变量操作时可能会发生的各种问题。JMM完全兼容CPU的多级cache机制,并且支持编译器的代码重排序。
注意:JMM内存模型的概念不同于jvm中6大内存区域的概念,两者不可强行混为一谈。

主内存和工作内存

JMM规定了所有的全局变量都存于主内存(Main Memory)中,一般情况下这些全局变量都会被保存到堆里。每个线程都有自己的工作内存(Working Memory),工作内存中会保存当前线程所需用到的主内存中全局变量的拷贝和自己的局部变量。一般情况下工作内存指的是栈内存,从物理上来讲,工作内存一般情况下都会工作于cpu的cache里。一个线程对全局变量的任何操作必须在自己的工作内存中进行,不允许直接操作工作内存。不同线程不能访问对方的工作内容,只能通过主内存进行数据交换。

工作内存在对主内存中变量做拷贝时,如果变量是基本类型的,则会拷贝其值;如果是引用类型的,那么仅仅会拷贝其引用

JMM内存操作

JMM定义了8种主内存与工作内存之间的具体交互操作,虚拟机在实现JMM时必须保证每种内存操作都是原子的。表1详细列出了JMM的8种内存操作。

指令 操作名 作用区域 描述
lock 锁定 主内存 把一个变量标识为一个线程独占的状态
unlock 解锁 主内存 把一个处于锁定状态的变量释放出来
read 读取 主内存 把一个变量的值从主内存读取到工作内存中,以便于随后的load指令使用
load 载入 工作内存 把read指令从主内存中读取的变量值放入到工作内存的变量副本中
use 使用 工作内存 将工作内存中一个变量的值传递给执行引擎
assign 赋值 工作内存 把一个从执行引擎接收到的值赋给工作内存的变量
store 存储 工作内存 把一个工作内存中变量的值传送到主内存中,以便于随后的write指令使用
write 写入 主内存 把store指令从工作内存传出的变量的值写入到主内存的变量中

表1 JMM内存操作指令表

20170702 01:48 编辑到JMM内存操作指令表

JMM规定了在使用以上8种内存操作时必须遵守以下规则:

  • read,load或store,write必须成对出现。如:执行read操作从主内存读取一个变量后,必须在工作内存使用load载入这个变量。两者之间的顺序不可错,但两者之间可以穿插其它指令。同理,在工作内存中对一个变量使用了store指令,必须在主内存中使用write指令进行写入。
  • 不允许一个线程丢弃它最近做的assign操作。变量在工作内存中改变了之后必须将这个变化同步到主内存中。
  • 不允许一个线程没有发生过assign操作就将数据从工作内存同步到主内存。
  • 只能在主内存中新建全局变量,不能在工作内存中直接使用一个未被初始化的变量。可以使用assign和load指令在工作内存对一个变量进行初始化操作。
  • 一个变量在同一时刻只能被一个线程执行lock操作。
  • 如果对一个变量执行lock,那将清空工作内存中此变量的值。在执行引擎使用这个变量之前,需要重新load或assign重新初始化变量的值。
  • unlock只能解锁被本线程锁定的变量。
  • 对一个变量执行unlock之前,必须将此变量同步回主内存。

volatile 关键字

volatile是java语言所提供的关键字,使java最轻量级的内存同步的措施。与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

java中锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。
volatile 变量具有 synchronized 的可见性特性,但是不具备锁的原子特性。所以即便volatile没有不一致的问题,但volatile变量在并发的运算下并不是原子操作,所以依然可能会有安全问题。

package com.github.weiwei02.jvm.jmm.volatile_test;

/**
* volatile 线程安全性测试
* @author Wang Weiwei <email>weiwei02@vip.qq.com / weiwei.wang@100credit.com</email>
* @version 1.0
* @sine 2017/7/2
*/
public class VolatileTest {
public static volatile int race = 0;
public static final int THREAD_COUNT=20;

public static void main(String[] args) {
    Thread threads[] = new Thread[THREAD_COUNT];
    System.out.println(race);
    for (int i =0; i < THREAD_COUNT; i++){
        threads[i] = new Thread(() -> {
            for (int j = 0; j < 1000 ; j++) {
                increase();
            }
        });
        threads[i].start();
    }


    //等待所有累加线程都结束
    while (Thread.activeCount() > 1){
        Thread.yield();
    }

    //等待所有线程执行完毕之后,打印race的最终值
    System.out.println(race);
}

private static void increase() {
    race++;
}
}

代码1 volatile 线程安全测试

程序的执行结果如图2所示。
volatile 线程安全测试
图2 volatile 线程安全测试

这个程序执行的结果应该是20000,可实际执行所得到的结果只有18439.并且多次执行本程序都会得到不一样的结果。所以会出现这种情况的原因是 race++ 这个操作并不是原子操作,volatile关键字只能保证在使用race变量时,工作线程能够从主内存中拿取最新的race的值,并不能保证进行过++操作之后写回到主内存期间其它线程没有对race变量进行修改。
volatile关键字只能保证可见性,只有在以下两种条件下,我们可以认为volatile是线程安全的;

  1. 运算结果并不依赖变量的当前值,或者能够保证只有一个线程修改变量的值。
  2. 变量不需要与其它的变脸共同参与不变约束。

    当不符合这两种条件时,想要保证volatile变量的原子性,只能通过 机制来实现。

    volatile变量的第二个重要作用是禁止编译器进行指令重排序。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。从硬件层面上来讲,是指CPU允许多条指令不按程序规定的顺序分开发送到个相应的电路单元来处理。CPU必须要正确的处理指令的依赖情况,不能将指令任意的排序。比如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3将地址B中的值减去3。在这一系列指令中,指令1,2之间是有依赖关系的,不能进行重排序。但指令3的执行顺序可以改到指令1之前也可以放到指令1之后,只要能够保证在执行完毕这三条指令程序中A,B的值是正确的即可。volatile在某些情况下性能高于锁,由于虚拟机对锁实行有多种消除和优化,我们很难衡量volatile会比锁快多少。

原子性、可见性和有序性

20170803 23:11:51 原子性、可见性和有序性
JAVA内存模型的核心问题时为了解决多线程应用中的原子性、可见性与有序性的问题,保证了这三点才能使多线程应用有数据安全性可言。

  • 原子性 (Atomicity)
    由java内存模型来直接保证原子性的操作包括read、load、assign、use、store和write。对于一个变量的基本读写操作都是具有原子性的。如果需要对多种这几种基本读写操作的操作集合保证原子性,可以使用lock和unlock。
  • 可见性(Visibility)
    可见性是指当变量的值被一个线程修改了之后,其它线程能够立即知道这个修改。JAVA内存模型时通过变量内容修改后立刻同步到主内存,读取变量的值之前先从内存刷新这种方式来保证可见性。JAVA中除了volatile之外还有synchronized、final两个关键字能够实现内存可见性。synchronized的可见性是由”对一个变量执行unlock操作之前,必须先把此变量同步会主内存中(执行store,write操作)”这条规则获得的。final的可见性是由于被final修饰的变量在构造器中一旦被初始化完成,并且构造器没有吧this的引用传递出去(this引用是一件非常危险的事,其它线程可能通过这个引用访问到初始化一半的对象),,那在其它线程中就能看见final变量的值。
  • 有序性
    如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句指的是“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句指的是“指令重排序”现象和“工作内存与主内存同步延迟”现象。

先行发生原则

20170804 02:02:23 先行发生原则

先行发生原则(Happens-before)是JAVA内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,操作A造成的影响就能被操作B观察到。JAVA只能怪的先行发生原则无需任何同步协助,可以直接编码使用。以下列出JAVA中的先行发生原则,如果两个操作之间的关系不在下面的规则中,且无法通过下面的规则推导出来,虚拟机就能随意的对他们进行指令重排序。

  • 程序次序规则(Program Order Rule)
    :在一个线程内,按照程序的代码顺序,书写在前的操作先于书写在后面的操作发生。准确的说,应该是控制流顺序而不是程序代码顺序,因为需要考虑分支、循环等结构。

  • 管程锁定规则(Monitor Lock Rule)
    : 一个unlocak操作先行发生于后面对同一个锁的lock操作。

  • volatile变量规则(Volatile Variable Rule)
    :对一个volatile的写操作,先行发生于后面对这个变量的读操作。

  • 线程启动规则(Thread Start Rule)
    :线程的start方法先行发生于此线程的每一个动作。

  • 线程终止规则(Thread Termination Rule)
    : 线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.jion()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  • 线程中断原则(Thread Interruption Rule)
    :对线程的interrupt()方法的调用先行发生于被中断的线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 对象终结规则(Finalizer Rule)
    :一个对象的初始化完成(够构造函数执行结束)先行发生于它的fnalize()方法开始。

  • 传递性(Transitivity)
    :如果A操作先行发生于B操作,B操作先行发生于C操作,就可以得出A先行发生于C的结论。

    时间先后顺序与先行发生原则之间基本没有关系,所以当我们需要衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。

    20170804 23:59:46 先行发生原则

引用

本文是对class文件的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java Virtual Machine Specification》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》

关于

原文链接 https://weiwei02.github.io/2017/07/02/jvm/001-java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/
我的github: https://github.com/weiwei02/
我相信技术能够改变世界。

评论和共享

  • 第 1 页 共 1 页
Copyrights © 2017 weiwei02. All Rights Reserved. github空间地址: https://weiwei02.github.io/ 国内空间地址: https://weiwei02.cording.me/
作者的图片

weiwei02

技术,改变世界


软件工程师


北京,海淀
国外主站 国内主站