二、内存区域和内存溢出

运行时数据区域

  1. 程序计算器

    线程私有,当前线程锁执行的字节码的行号指示器,不会出现OOM。

  2. java虚拟机栈

    • 概念:

    线程私有,java方法执行的线程内存模型,每个方法呗执行时jvm会哦同步创建一个栈帧,用户储存:局部变量表,操作数栈,动态连接,方法出口等。

    • 出现异常:

    若线程请求的栈深度超过jvm所允许的深度,抛出 StackOverflowError

    栈扩展时无法申请到足够的内存,抛出OOutOfMemoryError

  3. 本队方法栈

    线程私有,服务于本地方法。

  4. java堆

    • 概念:线程共享,jvm启动时创建,存放对象的实例,随着即时编译器的进步,java对象也可能在栈上分配
    • 组成:新生代,老年代,永久代,Eden,Survivor… 把java堆细分只要是为了更好的回收内存,或者更快的分配内存
  5. 方法区

    • 概念:线程共享,储存被JVM加载的类型信息,常量,静态变量,即时编辑器编译后的代码缓存等数据。
    • 变化:JDK7的HotSpot,把原来放在永久代的字符串常量池,静态变量移动到了堆中,JDK8废弃了永久代的概念,使用了本地内存中的元数据区进行了替换

HostSpot虚拟机对象

  1. 对象的创建

    • 在常量池中价差类的符号引用

    • 改符号引用代表的类进行加载,解析和初始化

    • 对中分配内存

      • 指针碰撞:内存分配是规整的,每次分配内存时,使用指针位移的方式进行。
      • 空闲列表:jvm维护一个列表,记录堆内存那些内存块是可用的,每次分配完内存之后更新该列表。
      • 并发问题:jvm采用CAS保证更新操作的原子性,另一方式:把内存分配动作按照线程划分在不同的空间中进行。
    • 对象头设置

      • 对象是哪个类的实例,哈希码,GC分代年龄…;至此jvm视角,一个对象已创建完成
      • 从java程序的视角,构造函数尚未执行(Class文件中的) 字段默认是0值,此时程序对象还不能使用。
    • 执行构造函数() 方法

      • 此时刻对象才算完全构造出来
  2. 对象的内存分布

    • 对象头

      • 运行时数据

      官方称之为"Mark Word", 动态定义的数据结构,包括:哈希码,GC分代年龄,所状态标志,线程持有的锁,偏向线程id,偏向时间戳等等。

      • 执行类型元数据的指针

      通过这个指针确定该对象是哪个类的实例。

      • 如是数组,则还有个数组长度
    • 实例数据

    程序代码中所定义的各种类型的字段内容,无论是父类继承下来的字段还是本类自己的都会记录下来,按照默认的分配策略,相同宽度的字段总是分配到一起。

    • 对齐填充

    jvm要求对象的起始地址必须是8字节的整数倍,所以如果对象实例数据没有对齐的话,就需要通过对齐填充。

  3. 对象的访问定位

    • 通过栈上的reference数据来曹组偶对上的具体对象

    • 访问方式

      1. 使用句柄访问

        java堆中划分一块内存作为句柄池,reference保存的就是对象的句柄地址,而句柄保存的是实例地址和对应的类型数据地址

        **优势:**reference中存放的是稳定的句柄,对象被移动的时候,修改的是句柄中实例对象的指针,reference不必修改。

      2. 使用直接指针访问

        栈上直接存放的就是堆中对象的内存地址,这种方案就需要考虑类型数据如何存放。

        **优势:**访问速度快,节省了一次指针定位的开销。

OutOfMemoryError异常

  1. Java堆溢出

    -Xmx 最大对对内存; -Xms 初始堆内存

  2. 虚拟机栈和本地方法栈溢出

    -Xss 设置栈内存容量

  3. 字符串常量池

    String::intern()

    • jdk7之前,把首次出现的字符串实例对象复制到永久代中的字符串常量池中,并返回永久代的实例的地址。
    • jdk7以及后面的版本,字符串常量池移动达到了堆中,那么在需要在常量池中记录字符串首次出现的实例引用即可,并返回常量池中的地址。
  4. 方法区溢出

    • -XX:MaxMetaspacceSize 最大元空间,以字节为单位,
    • -XX:MetaspaceSize 初始元空间

三、垃圾收集区与内存分配策略

GC要解决的问题

  1. 哪些内存需要回收
  2. 什么时候回收
  3. 如何回收

哪些对象可以回收

  1. 引用计数法

    • 定义: 在对象中添加一个引用计数器,当该对象被引用一次,计数器就加1,引用失效一次,计数器减1,计数器为0 那么该对象就没有被使用
    • 有点:原理简单容易实现,判断效率也很搞。
    • 缺点: 无法解决对象之间互相引用的问题。
  2. 可达性分析算法
    • 定义:通过一系列称之为”GC Roots“ 的对象作为起始节点根据引用关系进行遍历,没有遍历达到的对象则列为可被回收范围。
    • GC Roots:
      • jvm栈中引用的对象,例如:方法中使用的参数,局部变量,临时变量等。
      • 方法去总静态属性引用的对象。
      • 方法去中常量引用的对象。
      • 本地方法栈中JNI引用的对象。
      • JVM内部的引用。流入:基本数据类型对应的class对象 系统类加载器。
      • 被同步锁持有的对象。
      • 反应jvm内部情况的jmxbean jvmti中注册回调,本地代码缓存等。
  3. 引用的分类
    • 强引用: 程序中普遍存在的引用赋值,只要存在这种关系,被引用的对象就不会被回收
    • 软引用:一些有用但非必须的对象,将要发生内存溢出异常前,会把这些对象列入回收范围进行二次回收。 尝尝被用来实现缓存技术。例如 图片缓存,网页缓存。
    • 弱引用:非必须对象,只能怪生存到下一次gc发生过为止。
    • 虚引用:无法通过一个个虚引用获取一个个对象的实例,唯一的目的一只是为了这个对象被gc时可以收到一个系统通知。
  4. finalize() 方法
    • finaliize() 何时可以被执行, 对象的fiinalize方法没有被复写; 对象的finalize方法已经被jvm调用过,这两种情况都视为 没有必要执行。
    • 对象经过可达性分析,进行第一次标记后,若需要执行finalize方法 则改对象被放置在F-Queue队列中
    • 稍后会被jvm自建的,低优先级的finalizer线程执行对象的finalize方法,在改方法中进行自救(例如吧自己的引用赋值给其他变量)
    • finalize方法是对象逃脱被gc的最后一次机会,随后收集器会对F-queue中的对象进行第二次小规模的标记。
    • 任何对象的finalize方法都只能被jvm自动调用一次。
  5. 方法去的GC
    • 废弃的常量,常量池中的对象没被引用,那么gc该区域的时候,判定要被回收的话,则会被清理掉。
    • 不在使用的类型:
      • 该类型所有的实例都被回收
      • 加载该类的类加载器已被回收
      • 改类对应的class对象没有被其他地方引用

垃圾收集算法

  1. 分代收集器

    • 在大多数程序运行的实际情况的经验上提出粗的该理论,建立在两个分代假说

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

      抢分代假说:熬过多次gc过程的对象就越难消亡。

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

    • gc收集器设计原则,把java堆分不同的区域,然后把回收的对象按照年龄分配到不同的区域中存储

  2. 标记-清除算法

    • 首先标记出所有需要回收的对象,同一回收掉所有被标记的对象。
    • 优点:实现简单。
    • 缺点: 执行效率不稳定,内存空间碎片化问题。
  3. 标记-复制算法

    • 内存分为两个大小相等的区域,每次只是使用其中一块,某一块使用完了之后,就把还存活的对象负债到另一块区域上,清理掉已经使用的那块。
    • 优点:实现简单。运行高效;
    • 缺点:浪费内存空间。
  4. 标记-整理算法

    • 把所有的存活对象都移动到内存空间的一端,然后直接吃力掉边界意外的对象
    • 优点:解决了标记-请出去的内存碎片化问题 和 标记 - 复制的 浪费内存的问题
    • 缺点: 算法相对复杂, 内存地址的移动, 增加的gc的停顿时间

HotSpot算法细节

  1. 根节点枚举

    • 根节点枚举的过程中,需要保证对象的引用关系,不会发生变化,存在 ”Stop The World“
    • OopMap
      • 为了解决避免遍历所有的执行上下文的引用位置,使用OopMap的结构存放对象的引用
      • 一旦类加载器动作完成时,HotSpot就会吧对象中的某偏移量对应的类型计算出来,使用OopMap保存这些对象的引用
  2. 安全点

    • 为了解决快速准备完成gc roots的枚举,引入了安全点, 安全区域

    • 用户线程执行到安全点才会暂停,安全点位置是为编译器自动插入,一般第具备 让程序长时间执行的特征, 例如 方法的调用 循环跳转 异常挑战等指令顺序复用的地方。

    • 用户线程达到安全点并暂停的方案

      • 抢先试中断:系统先把所有的用户线程都终端,然后筛选出不在安全点上的线程恢复,然后过一会再对他进行中断,直到到达安全点。
      • 主动式中断:gc收集器需要中断线程的时候,设置一个标记,用户线程现在执行的过程中不断的轮询这个标记,发现标记为真则执行到最近的安全点上中断挂起。

      ​ HotSpot使用内存保护陷阱的方式,通过一条汇编指令完成轮询和触发线程中断

  3. 安全区域

    • 安全区域能去报在某一时间代码片段之中,引用关系不会发生变化,例如 用户线程处于sleep状态
    • 当用户线程要离开安全区域时,需要检查jvm是否已经完成了根节点美爵等需要暂停用户线程场景
  4. 记忆集和卡表

    • 为了解决对象跨代引用带来的问题,引入了记忆集,用户记录非收集区域指向手机区域的指针集合的抽象数据结构
    • 卡表 针对记忆集 这种定义的一个具体实现。
    • HotSpot中设计了card_table字节数据这个卡表 大小为512字节,数组的每个元素标识为内存区域中的一块特定大小内存块
    • 只要这个特定的内存区域中的对象的字段存在跨代指针,则把对应数组元素值标记为1, 改元素称之为变脏,gc时,只要扫码下卡表中变脏的元素,然后找出对应的内存区域中的对象加入到gc roots 中进行扫描
  5. 写屏障

    • 为了解决如何维护卡表元素状态
    • 这里的写屏障可以看做jvm层的引用类型字段赋值,这个动作的aop切面,引用对象的复制前后都在写屏障的覆盖范畴内
  6. 并发的可达性分析

    • 为了解决并发比那里对象图,降低用户线程的停顿,引入三色标记
    • 按照 是否被gc收集器访问过 这个条件标记为三种颜色

    白色: 为收集器访问过

    黑色: 已经被收集器访问过,并且这个对象的所有引用都已经被扫描过

    灰色:已经被收集器访问过,但这个对象至少存在一个引用没有被扫描过

    • 对象消失问题,即原本已经被gc扫描过的对象引用发生了变化 两个条件

      • 赋值器插入至少一条黑色对象到白色对象的引用
      • 赋值器删除了全部从灰色对象到白色对象的直接或者间接引用。
    • 解决对象小时问题,破坏这两个中任何一个条件即可。

    增量更新:黑色对象一旦新插入了指向被色对象的引用之后,他就变成灰色对象。

    原始快照(SATB):无论引用关系删除与否,都会按照刚开始扫描时的对象图快照来进行搜索。

垃圾收集器

  1. Sarial收集器

    单线程工作的,收集新生代内存的收集器 使用标记 - 复制算法

    实现简单,搞笑,但是用户线程停顿时间较长。

  2. ParNew收集器

    其实是一个多线程版本的serial收集器 支持多线程并行收集,使用标记-复制算法。

    目前仅有他能跟CMS收集器配合

  3. Parallel Scavenge收集器

    达到一个可控的吞吐量,支持多线程;吞吐量= 运行用户代码时间/(运行用户代码时间 + 运行垃圾收集的时间)

    通过具体的参数可以进行精确控制吞吐量,使用 标记复制算法

  4. Serial Old 收集器

    是serial收集器的老年代版本,使用标记-整理算法

  5. Parallel Old 收集器

    是parallel Scavenge 的老年代版本 支持多线程并行手机 使用标记-整理算法。

  6. CMS收集器

    Concurrent Mark Sweep 以获取最短回收停顿时间为目标的收集器

    步骤: 初始标记,单线程执行,暂停用户线程,标记一下,gc roots 能直接关联的对象 速度很快;

    ​ 并发标记:多线程并发从gc roots的指尖管理的对象遍历整个对象图的过程

    ​ 重新标记:修正并发标记过程中已经被标记却发生变动的对象

    ​ 并发清除: 并发清除掉已经被标记的对象

    优点: 并发收集 低停顿

    确定: 对处理器资源比较敏感,跟用户线程存在竞争关系;

    ​ 无法处理 浮动垃圾, 只能等到下次gc 时被回收

    ​ 采用 标记- 清除算 会产生内存空间的碎片化。

  7. G1收集器

    • 概念:

      面向局部手机的设计思路和基于 Region 的内存布局形式,追求应用的分配速率,而非把java堆一次性清理干净,是垃圾收集器技术发展史上的里程碑

      按照职责分离的原则,在jdk10 中提出了统一垃圾收集器接口 jvmgl

      停顿时间模型: 能够支持在一个长度为m毫秒的时间片刻内,垃圾收集所用的时间不超过n毫秒的目标

      Region:把连续的java堆划分为多个大小相等独立区域,每个Region都可以根据需要 扮演Eden区,Servivor区,老年代 收集器可针对不同的角色region使用不同的策略进行垃圾回收

      Humongous:储存打对象,合并多个额region存放这些大对象, 只要超过region的一半可判定为大对象, 每个regison大小可通过参数设定。

      G1会根据统计每个region的回收的价值和收益 根据用户设定的收集停顿时间,优先处理回收价值最大的那些region

    • 待解决的问题

      跨region引用对象的处理, 记忆集和卡表解决

      并发标记阶段如何保证收集线程和用户线程互不干扰,采用原始快照算法实现。

      如何建立可靠的停顿预测模型: 通过衰减均值理论实现,region的统计状态月新越能决定他的回收价值。

    • 4个步骤

      初始标记: 单线程执行,暂停用户线程标记gc roots 直接关联的对象,并修改tams只针对的值

      并发标记:对线程并发从gc roots的直接关联的对象遍历整个对象图的过程

      最终标记: 短暂暂停用户线程,并行处理并发标记阶段留下的satb记录

      筛选回收: 更新region的统计数据,对各个region的回收价值和成本进行排序,根据设定的期望停顿时间来指定回收计划, 可选则多个region组成回收集,把其中存活的对象复制到空的region中,回收掉旧的region,这个过程需要移动存活对象,需要暂停用户线程。

    • 优点:

      可指定最大停顿时间。

      分region的内存布局,按收益动态确定回收的创新设计带来巨大优势。

      G1从整体看是基于“标记 - 整理”算法实现的,从局部(两个region)看有是局域 标记-复制算法实现的。

    • 缺点: g1收集器执行 更加消耗内存,额外负载也比较高。

  8. Shenandoah 收集器

    目标是能在任何堆大小下都能吧垃圾收集的停顿时间限制在10毫秒以内

    • 步骤

      初始标记:单线程执行,暂停用户线程,标记gc roots 直接关联的对象, 并修改 tams只针对的值

      并发标记:多线程并发从gc roots的直接关联的对象遍历珍格格对象图的过程。

      最终标记:短暂暂停用户线程,并行处理并发标记阶段留下的satb记录

      并发清理:清理掉那些整个区域中都不存在一个存货对象的region

      并发回收:多线程并发吧手机的存活对象赋值到为被使用的region中,使用读屏障和 BrooksPointers 的转发指针来处理用户线程还在改变引用的问题

      初始引用更新:短暂暂停用户线程,简历一个线程集合点,确保所有的并发回收阶段中手线程都已经完成分配给他们的对象移动任务。

      并发更新引用:多线程并发执行,真正把对中所有指向旧对象的引用修正到复制后的新地址。

      最终引用更新:暂停用户线程,修正存在于gc roots中的而引用, 暂停时间和gc roots 中的数量成正比。

      并发清理:珍格格回收集中的region 已无存活对象, 清理回收掉这些region即可。

  9. ZGC收集器

    基于region内存布局,不设置分代,使用了读屏障,染色指针和内存多重映射等技术实现的可并发标记-整理算法的 以低延迟为首要目的收集器。

    • 步骤:

      并发标记: 跟g1一样,需要经过初始标记,最终标记。

      并发预备冲分配:根据特定的查询条件得出收集过程清理那些region,把这些region组成 充分配集

      并发重分配:把重分配集中存活对象复制到新的region中,并为重分配集中的每个region维护一个转发表,记录从旧对象到新对象的转向关系。

      并发重映射:修正整个对中指向重分配集中旧对象的所有引用。

如何选择合适的收集器

  1. 收集器的权衡

    应用程序关注是什么? 例如数据分析 科学计算,那就关注吞吐量,若是客户端应用程序需要关注内存的占用。

    运行应用的基础设施?

  2. jvm收集器日志

    查看gc基本信息: < jdk9 使用 -XX:+PrintGC >= jdk9 使用 -Xlog:gc

    查看gc详细信息: < jdk9 使用 -XX:PrintGCDetails >=jdk9 使用 -Xlog:gc*

    查看gc前后堆,方法区可用容量变化: < jdk9 使用 -XX:+PrintHeapAtGc >= jdk9 使用 -Xlog:gc+heap=debuge

内存分配与回收策略

  1. 自动管理内存的目标

    自动给对象分配内存。

    自动回收掉无用对象的内存

  2. 对象的生命旅程

    对象优先分配在eden区域 : 大对数情况下,对象在新生代Eden区中分配,当Eden区域没有足够的空间时,则发送一次MinorGc

    大对象直接进入老年代: 创建的打对象,需要连续的内存空间,指定大于-XX:PretenureSizeThreshold参数的对象,直接在老年代进行分配

    长期存活的对象进入老年代: 诞生于Eden中的对象,经过Minor GC 一次之后对象的年龄就会+1(年龄保存到对象头中),当年龄增加到-XX:MaxTenuringThreshold参数(默认15)设置的值时,晋升到老年代。

    动态对象年龄判定”:在Survivor空间的相同年龄的对象大小总和大于单个Survivor空间的一半,则年龄>= 该年龄的对象直接进入老年代,无序关心年龄阈值。

    空间担保: 发生Minor gc之前, jvm先检查老年代的最大可用的连续空间是否大于新生代对象的总和或者是历次精神的平均大小,就会进行Minor GC, 否则直接Full GC

四、jvm性能分析工具

  1. jps: jvm进程状况工具 列出成正在运行的虚拟机进程 常用命令 jps -lvm
  2. jstat:jvm 统计信息监控工具 监视虚拟机各种运行状态信息的命令行工具,常用命令 jstat -gcutil pid 1000
  3. jinfo: jvm配置信息工具 实时查看和调整虚拟机参数,常用命令 jinfo - flags pid
  4. jmap: java 内存映像工具 生成堆转存快照,常用命令 jmap-dump: format=b, file= xxx.bin pid
  5. jhat: 堆内存框照分析工具 内置了一个web 服务器,生成的堆快照可以使用浏览器进行分析。
  6. jstack: java对战跟踪工具 生成当前时刻 jvm 的线程快照

五、调优案例分析与实战

java虚拟机管理大内存

  • 回收大块对内存导致的长时间停顿
  • 打内存必须要有64位java 虚拟机的支持
  • 必须保证应用程序的足够稳定
  • 相同的程序在64位虚拟机中消耗的内存比32位要大。

若干虚拟机独立部署应用

  • 节点竞争全局资源
  • 很难高效率利用某些资源池
  • 32位java虚拟机收到系统的内存限制
  • 大量使用本地缓存,造成内存浪费。

六、类文件结构

两种基础数据类型

  1. 无符号数

    基本的数据类型: 以u1, u2, u4, u8来分别表示1个字符 2 个字符 4 个字符 8 个字符的无符号数

  2. 多个无符号数或者其他表作为数据想构成的符合数据类型,便于区分,命名习惯性的以_info 结尾

class文件内容剖析

  1. 魔数与class文件的版本

    每个class文件的前4个字节成为魔数, 唯一的作用就是确定是否为一个能被虚拟机接受的的class文件,固定值为: 0xCAFEBABE

    次版本号:第5 6 两个字节,jdk1.2 - jdk12之间这个值一直为0,jdk12后又再次启用这个次版本号

    主版本号:第7 8 两个字符,应对jdk的版本号,每个jdk都对应一个版本号, jdk8 = 52

  2. Winhex可以打开16进制的class文件

  3. 常量池

    常量池容量计数值

    • 主版本号后面就是常量池的入口,放置的是一个u2类型的数据,从1开始计数,代表常量池常量数量
    • 把0项空出的目的在于如果后面某些指向常量池的索引值的数值在特定的情况下需要达到不引用任何一个常量池项目的含义,可以把索引值设置为0

    常量池

    • 可以比喻为class文件的资源仓库
    • 字面量: 接近java语言层面的常量的概念,如文本字符串,final修饰的常量
    • 符号引用
      • 被模块导出或者开放的包
      • 类和接口的全限定名
      • 字段的名称和描述符
      • 方法的额名称和描述符
      • 方法句柄和方法类型
      • 动态调用点和动态常量
    • 项目类型:
      • 常量池总每个项目都是一个表,戒指jdk13共有17中常量类型
      • 17中常量类型的表结构第一个字节都是u1类型的标志位 标记属于那种常量类型
      • 图示
  4. 访问标志

    常量池结束之后,跟着两个字节是访问标志,类或者接口的访问信息,包括class还是interface,public类型 abstract类型,final类型

    • ACC_PUBLIC 0x0001 是否为public类型
    • ACC_FINAL 0x0010 是否被声明final
    • ACC_SUPER 0x0020 是否允许使用invokespecial指令新语义,1.0.2后比阿尼的次标记都为真。
    • ACC_INTERFACE 0x0200 标识是个接口
    • ACC_ABSTRACT 0x0400 是否为abstract类型
    • ACC_SYNTHETIC 0x1000 标识这个类并非用户代码生成
    • ACC_ACCOTATION 0x200 标识这是一个注解
    • ACC_ENUM 0x4000 标识是个枚举
    • ACC_MODULE 0x8000 标识是个模块
  5. 类索引,父类索引,接口索引集合

    • 类索引 用于确定这个类的全限定名
    • 父类索引 用户却抵挡这个类的父类的权限淡定名 除了object类之后其他所有的类的父类索引都不为0
    • 接口索引集合 入口有一项u2类型的数据为接口计数器,若该类没有实现任何接口,则该计数器为0, 后面的接口集合不占用任何字节。
    • 类素银和父类索引个使用一个u2诶膝盖的索引值表示,各自指向仪个乐行为CONSTANT_Class_info的类描述符常量,通过改常量中的索引值可以找到定义在CONSTANT_Uft8_info 类型的常量中全限定名字符串
  6. 字段表

    用户描述接口或者类中声明的变量,包括类级别的变量和实际级别的变量。

    • 字段表结构, 包括字段的作用域(private protected public) 实例变量还是了变量(static)是否可被序列化(translent), 可变性(fianl) 并发可见性(volatile),字段类型(基本类型,对象,数组),字段名
    • 字段表结合中不会列出父类和父接口中集成而来点的字段,但有可能出现原本java的代码中不存在字段。
  7. 方法表集合

    用与描述类中的方法的相关描述

    依次包括 访问标志 名称索引,描述符索引 属性表集合 等

    方法里面的java代码 编译为字节码指令之后 存放在方法属性中一个名为 Code 的属性里面

    如果没有重写父类的方法,在方法表集合中就不会出现来自父类的方法信息,但是有可能出现 有编辑器自动添加的方法,例如:类构造器 clinit() 方法 实例构造器 init() 方法

  8. 属性表集合

    每个属性 他的名称都要从常量池中引用一个constant_uft8_info 类型的常量来表示,属性值的构造则是完全自定义,通过一个u4长度的属性说明属性值占用的位数即可

    • Code属性

    java 程序中方法体中的代码被被编译后,最终的字节码指令存储在code属性中

    但是并不是所有的方法表都存在这个属性,例如 接口或这抽象类中的方法就不存在爱code属性

    • Exceptions属性

    在方法表与code属性平级的一项属性,作用是列列举所有的可能抛出的受检查异常,记载throws关键字后面列举的异常。

    • LinenumberTable属性

    描述java源代码行号和字节码行号(字节码偏移量)之间的关系。

    • LoccalVariableTable属性

    描述栈帧中局部变量表的变量和java源码中定义的变量之间的关系

    Jdk5 之后引入了泛型, 增加了这个属性,有于描述符中的泛型参数化类型被擦书,所以使用了字段的特征来完成泛型的描述

    • SourceFile属性

    用于记录生成这个class文件的源码文件名称

    • SourcecDebugExtension属性

    用户储存额外的代码调试信息

    • ConstantValue属性

    通知虚拟机自动给static的变量进行赋值

    • InnerClasses属性

    记录内部类和宿主类之间的关联

    • Deprecated属性

    属于标志性的不二属性,只有存在和不存在的区别,表示某个类 字段或者方法已不推荐使用

    • Synthetic属性

    表示某字段或者方法比你更不是有java源码生成的 而是编译自己添加的

    • StackMapTable属性

    Jdk6 新增 这个熟悉你在虚拟机类加载的字节码验证阶段被新类型检查验证其使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推到验证器

    • Bootstrapmethods属性

    jdk7新增,用户保存 invokedynamic 指令引用的印引导方法限定符

    • MethodParameters属性

    Jdk8 新增, 记录方法行参数名称和信息

字节码指令

  1. 定义

    指令由一个字节长度的,代表某种特定的擦操作含义的数字(操作码)以及个跟随气候的0至多个代表次擦操作所需的参数 构成

    jvm采用面向操作舒展而不是面向寄存器的架构,所以大多指令都不含操作数,仅有操作码,志林干的参数放在操作数栈中。

  2. 字节码与数据类型

    • 打赌偶数的指令都包含其操作对应的数据类型信息

      例如 iload 指令用于从局部遍历表中加载int型的数据到操作数栈中

      fload执行则是把float类型的数据加载到操作数栈

    • 操作码记助符中都有特殊的字符

      i代表对int类型的数据操作, l表示long s表示 short b表示byte c表示char f表示 float d表示double a 表示 reference

  3. 分类

    1. 加载和存储指令

      • 加载和存储指令用于吧数据在栈帧中的局部遍历表和操作哦舒展之间传递

      • 将一个局部变量加载达到操作数栈

        iload, iload_<n>, lload, lload_<n>, fload, fload_<n>, dload, dload_<n>, aload, aload_<n>
        
      • 将一个数值从操作数栈存储到局部变量表

        istore, istore_<n>, lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n>
        
      • 将一个常量加载到操作数栈

        pipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_ml, iconst_<i>, locounst_<l>, fcounst_<f>, dcounst_<d>
        
      • 扩冲局部变量表点的访问索引指令

        wide
        
      • 说明

        上面的指令中有一分部是<n> 实际上表示的是一组命令,例如 iload_<n> 代表了iload_0, iload_1 , iload_2, iload_3 这几个指令, 这种表示只是 iload 的这一种特殊形式,后面的数字其实表示的是 操作数 例如 iload_0 等价于 iload 0
        
    2. 运算符指令

      • 算数指令用于对两个操作数栈上的值进行某种运算,并把结果陈聪新存入操作数栈顶。

      • 加减乘除指令

        iadd, ladd, faddd, ddadd, isub, imul, idiv....
        
      • 求余取反位移

        irem, lrem, frem, drem, ineg, ishl
        
      • 按位或

        ior, lor
        
      • 按位与

        iand, land
        
      • 按位异或

        ixor, lxor
        
      • 局部变量自增

        iinc
        
      • 比较指令

        ddcmpg, dcmpl, fcompg, fcmpl, lcmp
        
    3. 类型转换指令

      • 可以吧两个不同类型数值类型进行互相转化

      • 宽化类型转换

        小范围类型向大范围类型安全的转化,无序显示转化指令

        int --> long, floag, double

        long -> float, double.

        fload -> double

      • 窄话处理转换

        必须显示地使用转化指令来完成 i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l, d2f

        可能导致转化结果产生不同的正负号,不同的数量级的情况,以及经度缺失。

    4. 对象与访问指令

      • 创建类实例指令

        new

      • 创建数组的指令

        newarray, anewarray, multianewarray

      • 访问类字段,和实例字段的指令

        getfield, putfield, getstatic, putstatic

      • 把一个数组元素加载到操作数栈的指令

        bdload, caload, saloadd, ialoadd, laload, faload, daload, aaload

      • 把一个操作数栈的值存储到数组元素中的指令

        bastore, castore, sastore, iastoore, fastore, dastoore, aastore

      • 取数组长度的指令

        arraylength

      • 检查类实例类型的指令

        instanceof, checkcast

    5. 操作数栈管理命令

      • 栈顶一个或两个元素出栈

        pop, pop2

      • 复制栈顶一个或两个元素,并把复制的元素压入栈顶。

        dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2

      • 将栈顶的两个元素互换

        swap

    6. 控制转移指令

      • 有条件或无条件地从指定位置指令的已下条指令开始执行

      • 条件分支

        ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_ifmpgt, if_icmple, if_icmplt, if_ifmpgt, if_icmple, if_icmpge, if_acmpeq, if_acmpne

      • 符合条件分支

        tableswitch, lookupswitch

      • 无条件分支

        goto, goto_w, jsr, jsr_w, ret

    7. 方法调用和返回指令

      1. invokevirtual

        用于调用对象的实例方法,根据对象的实际类型进行分派

      2. invokeinterface

        用于接口方法的调用,在运行时搜索一个实现了合格接口方法对象,找出适合的方法调用

      3. invokespecial

        用于调用一些特殊处理的实例方法,包括 实例初始化方法和私有方法 父类方法

      4. invokestatic

        调用类的静态方法

      5. Invokedynamic

        用不在运行是动态解析出调用点限定符所引用的方法

    8. 异常处理指令

      athrow 指令来完成实现, 在java虚拟机中处理异常catch采用异常表类完成

    9. 同步指令

      java语言中的synchronized语句块来表示的, 指令集中有monitorenter,和 moitorexit 两条指令来支持这语义的

      方法级别的同步是隐士的,无序通过字节码指令来控制, jvm 可以通过常量池中的方法表结构 acc_synchronized 访问标志符确定工艺个方法是否声明为同步,方法级别的同步与代码块级别低的都使用管城(monitor) 来实现

七、虚拟机类加载机制

类的生命周期

加载,验证,准备 , 解析,初始化,使用,卸载,其中验证,准备 解析统称为连接

6种情况必须立即对类进行初始化

  1. 使用如下字节码指令的时候

    遇到new,getstatic pustatic invokestatic指令时 类型没有进行初始化,则需要先触发其初始化阶段

    对应的java 代码中的使用场景

    ​ 使用 new 关键字实例化对象的时候

    ​ 读取或设置一个类型的静态字段的时候(被final修饰,已经编译期吧结果放入常量池的静态字段除外)

    ​ 调用一个类型的静态方法的时候

  2. 使用java.lang.reflect包的方法的对类型进行反射调用的时候。

  3. 当类型初始化的时候,发现父类还没有进行初始化,则需要先触发器父类的初始化

  4. 当虚拟机启动的时候,用户需要制定一个启动类,虚拟机会初始化这个主类

  5. 当s会用jdk7新加入的动态语言支持时, 部分方法句柄对应的类型没有进行初始化,则需要进行初始化

  6. 一个接口定义了jdk8新家的默认方法,(default修饰)时,如果这个接口对应的实现类发生初始化,那么这个接口要先被初始化

类加载的过程

  1. 加载

    • 通过一个类的全限定名来获取定义此类的二进制字节流
    • 通过字节流代表的静态储存接口准话为方法区的运行时数据结构
    • 在内存中生成一个代表这个类的class对象,作为方法区这个类的数据访问的入口
    • 注意 改阶段用户程序可以通过自定义类加载器的方式进行局部参与
  2. 验证

    确保class文件的字节流中包含的信息符合java虚拟机规范的约束要求

    1. 文件格式验证

      字节流是否符合class文件的个是你规范,并且是否能被当前版本的虚拟机处理

      是否魔数开头: 0xCAFEBABE

      主,次版本号是否在当前的java虚拟机接受范围内

      常量池中的常量是否偶不被支持的类型(检查常量的tag)

    2. 元数据验证

      对字节码描述信息进行语义分析并对元数据信息进行语义校验

      这个类是否有父类

      这个类是否集成了不允许被继承的类

      如果这个类是抽象类是否实现了其父类或接口中所要求的所有的抽象方法

    3. 字节码验证

      同构数据流分析和控制流分析,确定程序语义的合法性,符合逻辑性,对类的方法体进行校验分析

      保证在任何时刻操作数栈的数据类型与质量代码序列都能配合工作

      保证任何跳转指令都不会跳转到到方法体以外的字节码指令上

    4. 符号引用验证

      虚拟机把符号引用转化为直接引用的时候,这个动作会在解析阶段中发生,检查类型是否缺少或者禁止访问他依赖的某些外部类,方法,字段等资源

      符号引用中通过自费重描述的全限定名是否能找到对应的类

      在指定类中是否存在和服方法的字段描述符以及简单名称所描述的方法和字段

      符号引用的类,字段 ,方法的可访问性 是否可被当前类访问

  3. 准备

    类的静态变量 static 被分配内存并设置初始值的过程

    静态成员变量在准备阶段过后初始设置为零值(不同数据类型都对应各自的零值 如 boolean false, reference: null)

    静态成员变量被赋值为java代码中的值是在putstatic指令执行时完成,这个指令存放与类构造器()方法之中

    静态成员变量被final修饰,即讲台常量,那么在准备阶段就会直接赋值为java代码中的值

  4. 解析

    java虚拟机将常量池内的符号引用替换为直接引用的过程

    包括类或接口的解析,字段的解析,方法的解析

  5. 初始化

    在初始化阶段,则会根据程序编写指定的主观计划去初始化类变量和其他资源,简单的说其实就是执行类构造器() 方法的过程

    () 是编译器自动收集类中的所有类变量赋值动作和静态语句块中的语句的合并

    静态语句块中只能访问定义在他之前的变量,定义在他之后的变量只能进行复制操作,不能访问

    jvm会保证子类的clinit() 方法执行之前 父类的 clinit() 方法已经执行完毕, 也就意味着父类定义的静态语句块要优先于子类的类变量赋值操作

    一个类中没有类变量的赋值也没有静态代码块,那么编译器可以不为这个类生成 clinit 方法

    接口中不能使用静态语句块,但仍然有变量初始化的复制操作,因此接口也会生成 clinit 方法

    jvm会保证一个类的clinit 方法只被执行一次 使用了 cas 同步锁机制。

类加载器

类加载阶段 通过一个类的全限定名来获取描述该类的二进制字节流,放在虚拟机外部去实现,得以让应用程序自己可以决定如何获取所需要的类, 例如 类层次划分 osgi 程序热部署, 字节码加密等

  1. 类与类加载器

    类加载器用于实现类的加载动作

    对于任意一个类,都必须有加载他的类加载器和这个类本身一起共同确立其在java虚拟机中的唯一性。

  2. 双亲委派模型

    要求除了顶层的启动类加载器之外, 其余的类加载器都应有自己的父类加载器,当一个类加载器收的到加载类的请求,先把请求委托给父类加载器,一直把请求发到底层,只有当父类加载器反馈自己无法加载这个类(他的搜索范围中找不到所需的类)时,子类加载器才会尝试自己完成加载

    好处:java中类随着他的类加载器一起举杯了一种带有优先级层次的关系

    3类系统类加载器

    • 启动类加载器 boootstrap classloader

      负责及爱在 $java_hone\lib 目录 胡哦哦这 Xbootclasspath 指定路径中农存放可被jvm识别的类库加载打动jvm内存中 jvm 按照文件名识别, 例如 rt.jar tools.jar

    • 扩展类加载器 Exensioon Classloader

      负责加载 java_home\lib\ext 目录中 或者 java.ext.dirs 变量指定的路径中的类库

    • 应用程序类加载器 application classloader

      负责加载用户类路径 classpath 上所有的类库

八、虚拟机字节码执行引擎

运行时栈帧结构

  1. 概念

jvm以方法作作为最基本的执行单元,栈帧则是用于支持虚拟机允许方法调用和方法执行背后的数据结构,每个方法的调用到执行结束,都对应着一个栈帧的入栈到出栈的过程

每个栈帧都包括: 局部变量表,操作数栈,动态连接,方法返回地址,额外的附加信息

  1. 局部变量表

一组变量值的存储空间,用于存放方法的参数和方法内部定义的局部变量,在编译器就确定了最大容量,在code属性点的max_loocals数据项中

包括方法的参数, 实例方法的隐藏传参数this, catch定义的异常,方法体中的声明的变量

变量槽

  • 局部变量表的分配内所使用的最小单位,长度不超过32位的数据类型 (byte,char,float,int,short, boolean, return , returnAddress) 每个局部变量占用一个变量槽
  • 64位的数据类型long和double放在两个连续的变量槽中,reference跟虚拟机的实现有关,32位点的栈32位,64位的还需要砍是佛偶开启指针压缩
  • 局部变量表哦中的变量槽可以重用,当pc计数器的值已经超过了某个变量的作用域,那么他对应的变量槽可以交个其他变量来重
  1. 操作数栈

    • 操作数栈的最大深度也在编译器就确定了最大深度,写入了code属性的max_stacks 数据项中
    • 32位数据类型所占的栈容量为1, 64位的数据类型占用的容量为2
  2. 动态连接

    • 每个站真逗包含一个执行运行时常量池中该栈帧所属方法的引用,为了支持在方法的的调用的过程中的动态连接
  3. 方法返回地址

    • 当前方法完成调用退出时,必须返回到方法被调用的地方,程序才能继续执行,方法返回时需要在栈帧中保存一些信息,用来帮助恢复他的上层主调用方法的执行状态。
  4. 附加信息

    • 允许增加一些规范里面没有描述的信息到栈帧中,例如 调试,性能手机的相关信息,跟虚拟机的实现有关。

方法的调用

  1. 解析

    所有方法调用的目标class文件中都有一个常量池中的符号引用,在类加载的解析阶段把斧蛤引用转化为了直接引用

    java源文件编译完成之后,就确定可唯一调用的版本,可以在类加载中的解析阶段吧符号引用直接解析为直接引用,包括静态方法, 私有方法, 实例构造器,父类方法,被final修饰的方法

  2. 分派

    分派的调用过程将会揭示一些多台的最基本的提现

    • 静态类型

      静态类型的变化仅仅在使用时候偶发生,最终静态类型实在编译器可知的

    • 实际类型

      变化的结果只有在运行期才能确定

    • 静态分派

      所有依赖静态类型来决定方法的执行版本的分派动作叫做静态分派,经典的应用就是在编译期进行的静态分派,进而通过参数确定使用哪个重载版本,并生成对应的字节码指令。

      虚拟机在重载时,通过参数的静态类型作为判断依据,静态类型在编译器可知的,所以在编一阶段,根据参数的静态类型决定了会使用哪个重载版本。

    • 动态分派

      在运行期间根据实际类型确定方法执行版本的分派过程称为动态分派。

      invokevirtual指令吧常量池中的方法的符号引用解析到直接引用上,并根据方法的接收者的实际类型来选择版本。

    • 单分派和多分派

      单分派:根据一个宗量对对表方法进行选择

      多分派:根据多与一个宗量对目标方法进行选择。

动态类型语言

类型检查的主体过程是在运行期而不是在编译器进行的,例如 javascript php python

那么在编译器就进行类型的检查过程的语言叫做静态类型语言, 例如 java c++

java.lang.invoke

出了单纯的依靠符号引用来确定的调用的目标方法这条路之外,提供一种新的动态确定目标方法的脊椎,成为 方法句柄 method handle

methodhandle 则设计为可服务于java虚拟机之上的语言, 包括java语言

invokedynamic指令

每个含有 invokedynsmic 指令的位置都被成为 动态调用点, 这条指令的代表符号引用是 constan_invokedynamic_info 常量

可以获取3项信息:

引导方法: 有固定的参数,并且返回值是callsite对象, 这个对象党代表了真正要执行的目标方法调用

方法类型

名称

字节码解释执行

传统编译过程: 编写源码程序 -> 词法分析 -> 语法分析 --> 抽象语法树 —> 中间代码 —> 生成器 --> 目标代码

解释执行过程: 编写的源码程序—> 词法分析-----> 语法分析 ------> 抽象语法书 ------> 指令流 ------> 解释器 ------> 解释执行

javac编译万传给你了程序代码经过词法分析, 语法分析到抽象语法书,再编译整个语法书生成线性字节码指令流的过程

javac编译器输出的字节码指令流,基本上是采用基于栈的指令集架构,大部分的字节码指令都是零地址指令,即他们的指令不带参数,依赖操作数栈进行工作

九、类加载及子系统案例

Tomcat 正统的类加载架构

  1. 解决的问题

    部署在同一个服务器上的两个web应用程序锁使用的java类库可实现相互隔离

    部署在同一个服务器上的两个web应用程序锁使用的javva类库可实现相互共享

    服务器需要保证吱声的安全不受部署的web应用程序影响

    支持jsp的web服务器

  2. 目录结构和类加载器

    /common目录, 可被tomcat和所有的web应用程序共同使用, CommonClassLoader

    /server目录,可被tomcat使用,对所有的web应用成都不可见,CatalinaClassLoader

    /shared目录, 被所有的web应用程序共同使用, 但对tomact自己不可见 SharedClassLoader

    /Webapp/WEB-INF目录,仅仅可被web应用使用 WebappClassLoader

    单独处理jsp文件 JasperLoader

  3. OSGI

    基于java语言的动态模块化规划

  4. 字节码生成技术

    javac javassist cglib asm

    动态代理, 代理类的处理罗家可以在原水方法进行环绕修饰, 记载调用原始方法之前或之后添加自己的代码逻辑

  5. Backport工具

    把高版本的jdk编写的代码放到第版本jdk环境中部署运行

十、前端编译与优化

代表性编译器产品

前端编译器, jdk 的javac eclipse jdt的增量式编译器 ECJ

即时编译器 HosSpot虚拟机的C1 C2 graal 编译器

提前编译器, JDK的Jaotc, GCJ, Ecelsior JET

javac 编译器

编译过程

  1. 准备阶段: 初始化促使华插入式注解处理器

  2. 解析与填充符号表的过程

    语法, 语法分析 构造出抽象语法树

    填充符号表, 产生符号地址和符号信息

  3. 插入式注解处理器的注解处理过程

  4. 分析与字节码生成的过程

    标注检查, 对语法的静态信息进项检查

    数据和控制流分析

    解语法糖: 语法糖能钱少哦代码量,增加程序的可读性,解语法糖就是在编译阶段还原回原始的基础语法结构

    字节码生成:把前面步骤所生成的信息转为字节码指令写到磁盘中,并进行了少量的代码添加和转化工作 例如 实例构造器 init() 和构造器 clinit()

java语法糖

  1. 泛型

    本质是参数化类型或者参数化多台的应用,“类型擦除式泛型”

    由于jdk5引入泛型时,java已经面世十余年,遗留点的代码规模非常大,为了保证java5之后引入泛型以前的编译点的class文件能够继续执行, 最终选择了直接把现有的类型原地泛型化,不添加新的类型

    • 类型擦除

      让所有的泛型化的实例类型 如ArrayList 自动成为arrayList 的子类型或者还原回他本身, 否则类型转换就是不安全的

    • 缺陷

      类型擦除实现了泛型直接导致了对原始类型数据支持成了麻烦,因为支持int long 与object之间的强制转换。

      运行期间无法获取泛型的类型信息

      带来了模棱两可的 模糊情况,例如方法的重载参数是两个不同的类型的list, 却不能被编译通过,因为类型被擦除了。

    • 结论

      从Signature属性的出现,可以看出所谓类型擦除,仅仅是对方法的code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息, 这也是我们能过反射手段获取到参数化类型的根本依据

    • 值类型与为类型的泛型

      Valhalla项目中规划了多种泛型的实现方案,其中包括具现化

      提供 值类型 的语言层面的支持

  2. 自动拆装箱

    == 运算在遇到算数运算符时会自动拆箱

    equals() 方法不处理数据转型的关系, 或者说数量类型一样并且值一样 才为真

  3. 条件编译

    条件为常量的if语句可以实现条件编译

十一、后端编译与优化

即时编译器

java程序最初都是通过解释器进行解释执行的,当时jvm发现某个代码块执行的特别频繁,就会吧这些代码任定位热点代码,为提供热点代码的执行效率,jvm将会吧这些代码编译成本到底机器码,完成这个任务的后端编辑器叫做即时编译器

  1. 解释器与编译器

    当程序要像循序启动和执行的时候, 适合使用解释执行,省去了编译的时间,立即执行

    程序启动之后, 随着时间的推移,编译器会把越来越多的热点代码编译成本地机器码,减少解释器的中间损耗,获取更高的执行效率

  2. 热点代码

    被多次执行的方法

    被多次执行的循环体

    不管是那种情况, 编译的目标对象都是整个方法体

  3. 热点探测判定

    基于采样的热点探测: 周期性的检查某个线程的调用栈顶,如果某个方法经常出现在栈顶,那这个方法就是热点方法

    基于计数器的热点探测: 为每个方法创建计数器,统计方法的调用次数,执行次数超过一定的阈值则认为他是热点方法

  4. 编译过程

    默认条件下,无论采用哪种编译执行方式,虚拟机在编译还未完成编辑之前, 都仍然按照解释方法继续执行代码, 而编译动作则交给编译线程中进行

  5. 提前编译器

    提前吧字节码编译为本地机器码,但这跟具体的硬件平台信息相关, 无法做到一次编译,到处运行的理念。

    ART使用体检编译,在android的时间里大放异彩, 干掉了即使编译器Dalvik

编译器优化技术

  1. 方法内联

    把目标方法的代码原封不动的复制到发起调用的方法中,避免发生真是的方法调用

    方法内联的条件

    • 被调用方法是否是热点代码
    • 被调用方法是否大小合适
    • 运行时方法是否可唯一确定
  2. 逃逸分析

    分析对象的动态作用域,当一个对象在方法里面被定义后,他可能被外不方法所引用(例如 调用参数传递到其他方法中) 这称之为方法逃逸

    当前线程中的对象赋值给其他线程中访问的实例对象, 这称之为 线程逃逸

    代码优化:

    如果能证明一个对象不会逃逸打动方法或者线程之外或者逃逸成都比较低,则可以为这个对象采取不同程度的优化。

    • 栈上分配

      如确定一个对象不会逃逸出线程之外,那么这个对象可以在栈上分配内存,这样对象所占用的内存空间就会随着栈帧的出栈而销毁

    • 标量替换

      • 标量: 若一个数据无法在分解成更小的数据来表示,(int,long, reference类型等)不能进一步拆分,那么这些数据称之为标量
      • 聚合量:相反地,一个数据可以进行被分解,那么这些数据称之为聚合量。
      • 标量替换: 把一个java对象拆散,根据程序的访问情况,把用到的成员比阿尼朗回复为原始类型来访问,这个过程为标量替换。
      • 如果一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象。
    • 同步消除

      如果一个对象不会逃逸存储线程,无法被其他线程访问,那么这个对象的读写肯定不会有竞争,那么对这个对象的同步措施就可以安全的消除。

  3. 公共子表达式消除

    如果一个表达式E已经被计算过了,并且从先前计算到现在E中所有的变量的值都没有发生改变,那么E的这次出现成为公共子表达式

  4. 数组边界检查消除

    在访问数组元素的时候,系统将自动会检查上下界限,编译器通过数据流分析可以得知操作元素的下表不会超过数据的范围,则可以进行数组上下界检查的消除。

Graal编译器

  1. jdk10增加jvmci,虚拟机编译器接口

    响应HotSpot的编译请求,并将该请求分发给java实现的就是编译器。

    允许编译器访问Hostpot中与即时编译相关的数据结构,包括类,字段 方法 性能监控数据等。

    提供HotSpot代码缓存的java端抽象,以便于部署编译完成的二进制机器码

  2. 代码中间层表示

    中间层表示也被等价的称之为理想层,从编译器内部来看整个过程, 字节码 ----> 理想图 ---->优化 ---->机器码的转变过程

  3. 代码优化与生成

    • 生成理想图

      理想图本省的数据结构:一组不为空的节点集合,他的节点都是用valueNode 的不同类型的子类节点表示

      字节码生成理想图:可以按照字节码解释器的思路去理解他。

    • 理想图的操作

      规范化:如何缩减理想图的规模,在理想图的基础上优化代码索要采取的措施,对于理想图的规范化不限于单个操作码范围内,很多都是立足于全局进行的。

      理想图转化为机器码:先生成低级中间表示LIR 然后在用Hotspot 同一后端来产生机器码

十二、java内存模型与线程

java内存模型

  1. 主内存和工作内存

    java内存模型规定了所有的变量都存在主内存中

    每条线程都有自己的工作内存,线程的工作内存中保存了呗改线程使用的变量的副本, 线程对变量的操作多发生生在工作内存中,线程之间的变量值的传递需要通过住内存来完成

  2. 内存间的交互操作

    一个变量如何从主内存拷贝到工作内存, 又如何从工作内存同步到内存中,java内存模型定了8种操作来完成

    • lock 锁定 作用与主内存的变量 把一个变量标志位一条线程独占的状态
    • unlock 解锁 作用于主内存的变量, 把一个处于锁定状态的变量释放出来,此时才可以被其他线程锁定
    • read读取 作用于主内存的变量,把一个变量从住内存中传输到工作内存中
    • load 载入 作用于工作内存的变量,把从主内存read过程来的变量放入工作内存变量副本中
    • use 使用 作用于工作内存的变量, 工作内存中的一个变量值传递给执行引擎
    • assign 赋值 作用于工作内存的变量, 把执行引擎接受的值赋值给工作内存的变量
    • store 存储 作用于工作内存的变量,工作内存中的一个变量的值传送到主内存中
    • write 写入 作用于主内存的变量, 把store 操作的变量放入主内存的变量中

    针对8中操作的规则

    • 不允许read 和load store 和write 操作之一单独出现
    • 不允许一个线程丢弃他最近的assign 操作
    • 不允许以个线程没有经过assign操作的值同步回主线程
    • 一个新的变量只能在主内存中产生
    • 一个变量同一个时刻只运行一条线程对其进行lock操作
    • 如果对一个变量执行lock 操作, 那将会清理掉工作内存中次变量的值
    • 如果一个变量实现没有被lock操作锁定,那么不允许使用unlock操作
    • 对一个变量执行unlock操作之前,必须先把此变量同步会住内存
  3. volatile型变量的特殊规则

    • 两项特性

      保证此变量对所有线程的可见性,当一个线程修改了这个变量的值,那么其他线程都可以立即得知

      禁止指令重排序

    • volatile规则

      工作内存中,每次使用被volatile修改的变量钱都先从主内存中刷新最新的值到本地内存中

      工作内存中,每次修改给volatile修饰的变量都必须立即同步回主内存中

      要求被volatile修改变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同

  4. 原子性,可见性,有序性

    • 原子性

      一系列操作是一个整体,要么成功,要么失败,在java内存模型中,大致可以认为,基本数据类型的访问,读写都是具备原子性 除了long和double的非原子性协定

    • 可见性

      当一个线程修改了共享变量,其他线程能够立即得知这个修改

      java内存模型中通过主内存作为传递媒介,不同的线程可修改或读取主流程的变量,而被volatile修改的变量的特殊之处在于,被修改的变量能立即同步到主内存,每次使用变量之前都从主内存中读取。

      java中能保证可见性的三个关键性, volatile synchronized, final

    • 有序性

      线程内表现为串行语义,指令重排序现象和工作内存与主内存延迟的现象

  5. 先行发生原则

    判断数据是否存在竞争,线程是否安全的非常有用的手段,说操作a现行发生于操作b,那么是说操作b发生之前,操作a产生的应先功能被操作b观察到

    • 程序次序规则

      在一个程序内,按照控制流顺序,书写在前面的操作现行发生于书写在后面的操作

    • 管城锁规则

      一个unlock操作操作先行发生于后面对同一个多的lock操作

    • volatile变量规则

      对一个volatile变量的写操作现行发生于后面对这个变量的读操作

    • 线程启动规则

      Thread对象的start()方法现行发生于此线程的每个动作

    • 线程终止规则

      线程中所有发生的操作都先行与线程的终止操作

    • 线程中断规则

      对线程的interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生

    • 对象的直接规则

      一个对象的初始化完成先行发生于他的finalize()方法的开始

    • 传递性

      操作a先行与操作b 操作b先行与操作c 那么可以得出操作a先行发生于操作c的结论

  6. java的线程

    java的每一个线程都直接映射到一个操作系统线程线程来实现的

    • 新建New

      新建: 创建后尚未启动

    • 运行 Runing

      运行:包括操作系统线程状态中的Runing和Ready,处于此状态的线程有可能正在执行,也有可能等待操作系统给他分配执行时间

    • 无限等待 Waiting

      无线等待:不会被分配处理器的执行时间,需要等待其他线程的显示地唤醒[包括 Object::wait(), Thread:: join() , LockSUpport::park()]

    • 限期等待Timed Waiting

      限期等待:不会被分配处理器执行时间,达到等待时间之后有系统自动唤醒[包括Thread::sleep(s), Object:wait(s), Thread:join(s), LockSupport:parkNanos(), LockSupport:parkUnitl()]

    • 阻塞 Blocked

      堵塞:等待这获取到一个排他锁, 这个时间将在另外一个是线程放弃这个锁的时候发生

    • 结束 Terminated

      已经终止的线程状态

十三、线程安全与锁优化

线程安全

  1. 线程安全的定义

    当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和胶体执行,也不需要进行额外的同步,在调用方也不需要进行其他的协调操作,调用这个对象可以获取正确的结果,那这个对象是线程安全的

  2. JAVA语言中的线程安全

    • 共享操作数据分为5类

      不可变 : 不可变的兑现过一定是线程安全的,例如 final关键字,被final修饰的string intger 就不可变

      绝对线程安全: 不管运行环境如何,调用者都不需要额外的同步措施

      相对线程安全: 通过意义上将的线程安全

      线程兼容: 对象本身并不是线程安全的, 但是可以通过在调用端正确的使用同步手段来保证对象在迸发环境中安全使用

      线程对立: 不管调用端是否采用了同步措施,都无法在多线程环境中迸发使用代码

    • 线程安全的实现方法

      • 互斥同步(悲观锁)

        • 同步

          多个线程迸发访问共享数据时,保证共享数据在同一时刻只被一条线程使用

          缺点: 无论共享的数据是否出现竞争,先任务有竞争的线程,进行加锁,浙江会导致用户状态和心态转换,维护所计数器和检查是否有呗堵塞等开销

        • 互斥

          实现同步的一种手段

        • 互斥是因,同步是过,互斥是方法,同步是目的

        • synchrionized

          最基本的互斥同步手段,javac编译之后生成monitorenter, monitorexit 两个字节码指令,这两个指令都需要制定一个reference类型的参数来指明要锁定的和解锁的对象

          执行monitorenter指令时,首先尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经持有了这个对象的锁,就吧锁的计数器的值增加一,而执行monitorexit指令时把锁的计数器减一,计数器值为0,锁就会被释放,如果获取对象锁失败,那当前线程就应当被阻塞等待,知道请求锁定的对象被持有他的线程释放为止。

          被synchronized修饰的同步块对同一条线程来说是可重入的。

          被synchronized修饰的同步块在持有所的线程执行完释放锁之前 会无条件的阻塞后面的其他的线程进入

        • lock接口

          ReentrantLock可重入锁

          • 等待可中断: 长时间等待锁释放的线程,可以选择放弃等待处理其他事情。
          • 公平锁: 多个线程在等待同一个锁时,必须按照申请锁的时间顺序来一次获取锁
          • 锁绑定多个条件: 值以个ReentrantLock对象可以同时绑定多个Condition对象

          ReentrantReadRriteLock

      • 非堵塞同步(乐观锁)

        • 先不管风险,直接进行操作,出现了冲突在进行一段时间不长行的重试,知道出现没有竞争的共享数据位置,不在需要把线程堵塞挂起。
        • CAS:比较并交换,硬件处理器直接通过一条指令完成,属于原子操作。
          • 需要三个参数:修改一个变量V的值为B, 先检查符合就的预期值A的话,则更新成功,负责更新失败。
          • ABA问题
      • 无同步方案

        如果一个方法本来就没有涉及到共享数据,那么他自然就不需要在添加任何的同步措施

        可重入代码:一个方法输入相同的数据,都能返回相同的结果,则该方法的返回值是可预测的。

        线程本地存储,共享数据的可见范围限制在一个线程内部,通过ThreadLocal类实现线程本地储存的功能

  3. 锁优化

    jdk6实现了各种所优化技术,当使用synchronized加锁时,在锁定对象之前,先进行了一些列的锁优化

    • 自旋锁

      自选等待避免了线程切换的开销,但是却要占用处理器的时间,如果占用时间过长反而带来的收益会降低,所以自旋有次数的限制,达到这个次数之后任然没有成功获得锁,就使用传统的方式挂起线程。

      JDK6对自旋锁做了优化,引入了自适应,意味着自旋的限制次所不在固定,jvm会根据同一个对象的上次获得锁次数和拥有这的运行状态判断,允许自旋次数的适当增加,另一方面,自旋很少成功获得锁,那在以后获取锁将有可能直接省掉自旋过程,以避免浪费处理器资源。

    • 锁消除

      jvm即时编译器在运行时, 对一些代码要求同步,但是对呗检测到的不可能存在的共享数据竞争的锁进行消除,锁消除的主要判定来源于逃逸分析的数据支撑。

    • 锁粗化

      如果jvm探测到有一串零碎的操作都是对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部。

    • 轻量级锁

      在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗

      加锁工作过程

      • 进入同步代码块的时候,若同步对象没有被锁定(对象投中所标记为是 01 ) 则jvm在当前线程的栈帧中简历一个名为所记录的空间,存储所对象的mark work 的拷贝。
      • jvm使用cas尝试吧对象的mark work更新为锁记录的指针
      • 更新成功 则表示改线程拥有了对象的锁,同时锁标记为改成了 00,表示对象处于轻量级锁状态
      • 如果更新失败,则表示至少有一条线程跟当前线程竞争这个对象的锁,检查对象的wark word是否执行当前线程的栈帧,如果是,说明当前的线程已经拥有了这个对象的锁,直接执行同步块的代码
      • 检查对象投若不是当线程的栈帧,说明锁对象已经被其他线程给占用了,必须要膨胀为重量锁,标记为改为 10, 此时mark word中存储的是重量级所的指针
    • 偏向锁

      消除数据在无竞争情况下的同步原语进一步提升程序的性能

      这个锁会偏向于第一个获取他的线程,如果在接下来的执行过程中,该所一直被其他的线程获取,则持有偏向锁的线程将永远不需要进行同步

深入理解java虚拟机脑图文档相关推荐

  1. 《深入理解Java虚拟机》(第二版)学习3:垃圾收集器

    垃圾收集器 如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现. 我们这里讨论的收集器主要是基于JDK 1.7 Update 14之后的 Hotspot VM . Serial 收 ...

  2. JAVA好书之《深入理解Java虚拟机》

    最近打算做好现有工作的前提下,扎实一下自己专业的技术知识,并将相关的经典书也记录一下.今天看了一些JVM相关的知识,这里面的经典是<深入理解Java虚拟机>,适合有点基础又想深入理解其中原 ...

  3. 深入理解Java虚拟机知乎_深入理解Java虚拟机(类文件结构)

    深入理解Java虚拟机(类文件结构) 欢迎关注微信公众号:BaronTalk,获取更多精彩好文! 之前在阅读 ASM 文档时,对于已编译类的结构.方法描述符.访问标志.ACC_PUBLIC.ACC_P ...

  4. 深入理解Java虚拟机(类文件结构)

    欢迎关注微信公众号:BaronTalk,获取更多精彩好文! 之前在阅读 ASM 文档时,对于已编译类的结构.方法描述符.访问标志.ACC_PUBLIC.ACC_PRIVATE.各种字节码指令等等许多概 ...

  5. 不会玩游戏的程序员不是好作家,《深入理解Java虚拟机》周志明来了!

    嘉宾:周志明.杨福川 采访.撰文:Satoh_AI 这次采访起源来自于我和豆瓣的一位读者有同样的好奇心,为什么网上搜不到周志明老师的更多信息?为什么"80后玩家"可以把本本书都维持 ...

  6. 深入理解java虚拟机 - jvm高级特性与最佳实践(第三版)_JVM虚拟机面试指南:年薪30W以上高薪岗位需求的JVM,你必须要懂!...

    JVM的重要性 很多人对于为什么要学JVM这个问题,他们的答案都是:因为面试.无论什么级别的Java从业者,JVM都是进阶时必须迈过的坎.不管是工作还是面试中,JVM都是必考题.如果不懂JVM的话,薪 ...

  7. 深入理解java虚拟机 (二) 第二版

    如何阅读本书 本书-共分为五个部分:走近Java.自动内存管理机制.虛拟机执行子系统.程序编译与代码优化.高效并发.各部分基本上是互相独立的,没有必然的前后依赖关系,读者可以从任何- -个感兴趣的专题 ...

  8. 深入学习理解java虚拟机--1.win10 下构建64位 openJDK8

    基于之前面试很多次被问到jvm运行原理及调优问题,以及jvm本身是技能提升不可逾越的一道坎,于是决定深入学习jvm,不久买了周志明的<深入理解java虚拟机--jvm高级特性与最佳实践>一 ...

  9. 对话《深入理解Java虚拟机》作者周志明:电竞选手成为Java大神之路

    声明:本文由"阿里云MVP团队"原创,转载经"阿里云开发者社区"授权.原文标题:<职业电竞选手的Java大神路:对话阿里云MVP周志明>. 销售超过 ...

最新文章

  1. 【VMCloud云平台】SCO(四)流程准备
  2. ios 使用gcd 显示倒计时
  3. python邮件图片加密_Python爬虫如何应对Cloudflare邮箱加密
  4. Oracle 同义词、DBLINK、表空间的使用
  5. linux gdb模式下无反应,Linux,GDB 嵌入式Linux的GDB远程调试的问题--断点没反应
  6. docker从入门到实践第三版pdf_测开日常积累--Docker入门到实践
  7. html 调用c#dll中的控件,C#实现反射调用动态加载的DLL文件中的方法和在窗体中加载DLL文件的用户控件...
  8. http和https简介、区别以及客户端到服务器https通讯步骤
  9. Ubuntu桌面显示或隐藏回收站等图标
  10. 几款百度竞价点击软件测评来一发
  11. 四面体 matlab,matlab生成四面体单元
  12. 33 - Guarded Suspension模式 等待唤醒机制的规范
  13. Oracle ERP 仓库(inventory) 词汇1
  14. 安全帽佩戴检测算法研究
  15. 自己做项目时整理的上传Excel表格
  16. 扬帆际海——怎么做跨境电商?
  17. 智能血压计方案/设计案列/APP/小程序
  18. 如何从零开始建站,四个步骤了解一下
  19. NVT平台PWM配置
  20. 计算机工程实践 课程大纲,《计算机专业》实习教学大纲.doc

热门文章

  1. 请收下,700+页PDF社区精化!
  2. 浏览器无法启动百度网盘应用的解决办法
  3. 特色英文短语[转帖]
  4. 反激式开关电源技术归纳(上)
  5. 并购当当是海航自编自导的一场大戏
  6. 大数据【企业级360°全方位用户画像】业务数据调研及ETL
  7. 程序人生 - DCT、AT、CVT 到底哪个好?
  8. 数据结构c语言版谭浩强pdf,谭浩强C语言_数据结构.pdf
  9. java设计模式——装饰模式
  10. 移相信号发生器 课程设计 电赛 正弦波发生 相位调节