目录

环境准备

安装JDK(略)

JDK、JRE和JVM的关系

存在一种包含关系
在这里插入图片描述
官方的图
在这里插入图片描述官网地址
上图是JDK7的版本,到了JDK8 的版本结构发生了变化
在这里插入图片描述
官网地址
最主要的变化是把JVM从Java SE API中扔了出去。

了解

JAVA语言

JAVA之父
詹姆斯.高林斯
JAVA前身
oak项目(用在嵌入式设备)。

  • 1995年5月 -> JAVA 1.0 (理念:Write one run anywhere)
  • 1996年1月-> jdk 1.0发布, 附带jvm (sun Classic VM)
  • 1997年2月->jdk 1.1发布, 内部类,反射, jar文件格式, jdbc, Javabeans,rmi
  • 1998年 ->jdk 1.2发布,三个方向:j2 SE,j2 EE, j2 ME;swing出现;jvm中内置了jit编译器。出现了Hotspot VM
  • 2000年5月 -> jdk 1.3发布, Timer,java2d
  • 2002年2月 ->jdk 1.4发布(里程碑版本,java走向成熟的版本), Struts, Hibernate, Spring 1.x可以在其上运行;新增了正则表达式、Nio、日志、xml解析器…
  • 2004年9月 ->jdk 1.5发布(又名老虎 tiger),自动装箱拆箱、泛型、注解、枚举、变长参数、增强for循环…; Spring 2.x发布(引入了注解)
  • 2006年 ->jdk 1.6发布(改名为jdk6);三个方向改名:java SE,java EE, java ME;提供脚本语言支持,提供了编译api以及http服务器api(webservice的时候用);!!!java开源;
  • 2009年-> jdk 7(1.7)发布(规划过lambda表达式,却没有出来);Oracle 74亿收购Sun;2011.7发布了jdk1.7的最终版本
    …2014年3月分->jdk 8(1.8)发布; 对于java虚拟机的影响之一:使用Meataspace代替持久代(PermGen space)。在jvm参数方面,使用-XX:MetaSpaceSize
    和-XX:MaxMetaSpaceSize代替原来的-XX:PermSize和-XX:MaxPermSize。

Java技术体系

  • Java程序设计语言
  • 各硬件平台上的Java虚拟机
  • Class文件格式
  • Java API
  • 第三方的java类库

虚拟机

  1. Classic VM
    Sun公司产品,已经被淘汰了。是第一款商用的java虚拟机。纯解释性的虚拟机。如果要编译,只能外挂编译器,但是外挂后所有的代码都被编译了,没有解释性执行的能力。
  2. Exact VM
    Exact Memory Management (准确式内存管理),虚拟机知道内存中某个位置的数据具体是什么类型的,有助于垃圾回收,也使得能够抛弃Classic VM基于Handler的对象查找方式。使得虚拟机的性能有了很大的提高。
    编译器和解释器混合工作,以及两级及时编译器
    只能在Solaris平台发布
    还没搞什么事情,就被HotSpot VM所取代了。
  3. HotSpot VM
    由小公司所设计,被Sun公司收购。
    热点代码探测技术;
  4. KVM
    kilobyte 简单、轻量,高度可移植
    在手机平台运行;诺尼亚手机上支持过。
  5. JRocket
    BEA 产品
    世界上最快的java虚拟机(自称)
    专注服务端应用,没有解释,只有编译
    优势:垃圾收集器;MissionControl服务套件(用于检测内存泄露)
  6. J9
    IBM 产品
    全名:IBM Technology for java virtual Machine(IT4j),J9是内部开发代码。
    类似于Hot Spot,是一个多用途的虚拟机。
  7. Dalvik(不是java虚拟机)
    是安卓系统的核心。与java有千丝万缕的关系。
  8. Microsoft JVM
    只能运行在windows平台上;主要目的就是用来运行java applet小程序。赔偿了10亿美元,然后下架了。
  9. 高性能的JVM
    Azul VM 和 Liquid VM
    是高性能的java虚拟机
    Azul VM 修改了Hot Spot,运行在自己的专用硬件上。
    Liquid VM 不需要操作系统的支持,本身就是一套操作系统。
  10. Taobao VM
    阿里的虚拟机。阿里自己深度定制,自己用着比较好。不是通用的产品。对硬件的依赖比较高,对gc的性能有很大的提高。优化了编译。使用了crc32指令,降低了本地方法调用的开销。

内存结构

在这里插入图片描述
在这里插入图片描述

程序计数器

  • 程序计数器是一块较小的内存空间,他可以看做是当前线程所执行的字节码的行号指示器。
  • 程序计数器处于线程独占区。
  • 如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,这个计数器的值为undefined。
  • 此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

  • 虚拟机栈描述的是Java方法执行的动态内存模型
  • 栈帧:每个方法执行,都会创建一个栈帧,伴随着方法从创建到执行完成。用于存储局部标量表、操作数栈、动态链接和方法出口等。
  • 局部标量表:存放编译器可知的各种基本数据类型,引用类型,returnAddress类型;局部变量表的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧中分配多少内存是固定的,在方法运行期间是不会改变局部变量表的大小的。
  • 大小:StackOverflowError(栈帧塞满了栈)、OutOfMemory(栈帧无限扩展,超过了虚拟机的可用内存)。

本地方法栈

  • HotSpot中的本地方法栈和虚拟机栈是合二为一的
  • 虚拟机栈为虚拟机执行java方法服务,本地方法栈为虚拟机执行native方法服务

堆内存

  • 存放对象实例(由于优化,不是所有的对象都在堆上,逃逸分析)
  • 垃圾收集器管理的主要区域
  • 新生代(1/3):伊甸园区(8/10),幸存区1(1/10),幸存区2(2/10);老年代(2/3)
  • 浅堆:一个对象结构所占用的内存大小;深堆:一个对象GC回收后,可以真实释放的内存大小

方法区(JDK 1.8 元数据区)

  • 虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据
    类的信息包括:类的版本、字段、方法、接口
  • 方法区和永久代:方法区不等价于永久代;永久代是Hot Spot虚拟机独有,为了简化方法区内存管理代码的产物。JDK1.7 就开始“去永久代”的工作了。 1.7把字符串常量池从永久代中剥离出来,存放在堆空间中。JDK1.8以后就变成了元数据区,元数据空间并不在虚拟机中,而是使用本地内存。
  • 垃圾回收在方法区的行为:很少进行垃圾回收,但是并不代表不存在;

直接内存(NIO)

  • 直接内存分配不会触发GC

对象的创建

在这里插入图片描述

给对象分配内存的方式

  • 指针碰撞(内存是连续的,分界指针分隔这已用和可用区域!分配时直接移动)
    在这里插入图片描述
  • 空闲列表(使用一个表来记录内存空间的使用情况)
    在这里插入图片描述
    选择哪种方式是由内存是否规则来决定的,而内存是否规则则由垃圾回收器是否具有内存整理压缩功能来决定。

线程安全性问题(内存分配)

在堆内存中,给每一个线程分配一块自己的专属区域(并不是特别大,可以通过虚拟机参数来设置)(称为本地线程分配缓冲(TLAB))。在专属区域满了以后要追加分配,这时候需要进行同步操作。

探究对象的结构

  • Header(对象头)
    (1) 自身运行时数据(Mark Word)(64位虚拟机占64位,32位虚拟机占32位)
    - 哈希值
    - GC分代年龄
    - 锁状态标志
    - 线程持有的锁
    - 偏向线程ID
    - 偏向时间戳
    在这里插入图片描述
    64位虚拟机Mark Word结构

biased_locklock状态
001无锁
101偏向锁
00轻量级锁
10重量级锁
11GC标记

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。

epoch:偏向时间戳。

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。
在这里插入图片描述
32位虚拟机Mark Word结构
(2) 类型指针:对象指向其类的元数据的一个指针,虚拟机通过该指针来确定对象是哪个类的实例;并不是所有的虚拟机实现都需要在对象数据上保留类型指针。
(3)数组对象特有的记录数组长度的字段。

  • InstanceData
    存储对象的有效信息(属性字段…);
    存储顺序受虚拟机分配策略的影响和字段在java源码中定义的顺序的影响;HotSpot默认的分配策略是相同宽度的字段被分配到一起。
  • padding(对齐填充)
    并不一定存在;因为对象的大小必须是8的整数倍。而对象头是8的整数倍。InstanceData可能不是,这时候就需要padding来进行填充了。

  • 偏向锁 :将对象头Mark的标记设置为偏向,并将线程ID携带入到对象头MARK。只要没有竞争,获得偏向锁的线程,在加来进入同步块时不需要做同步;当其他线程请求相同的锁时,偏向模式结束。在竞争激烈的场合,偏向锁会增加系统负担。
  • 轻量级锁:BasicObjectLock:嵌入在线程栈中的对象,组成(1、BasicLock,主要存放markOop_displaced_header,持有锁的对象的对象头;2、ptr to obj hold the lock)。
  • 自旋锁:当存在竞争时,如果线程可以很快获得锁,那么可以不再OS层面挂起线程,让线程做几个空操作。

用锁的思想

  • 减少锁持有时间:也就是减少锁住的代码量
  • 减小锁力度:ConcurrentHashMap
  • 锁分离:读写锁
  • 锁粗化:锁放在循环外
  • 锁消除:这是编译器的一种优化;例如在方法内使用StringBuffer,编译器没有检测到需要锁,那么就会去掉StringBuffer中的锁-XX:+EliminateLocks,需要和逃逸分析配合使用。
  • 无锁 CAS(Compare And Swap)

加锁的顺序:

  • 尝试偏向锁—>尝试轻量级锁—>尝试自旋锁–>尝试普通锁

对象的访问定位

  • 使用句柄
    指向了堆中的某一块区域(句柄池:保存了实例对象的地址);
    好处:栈中的引用地址不会改变,变的是句柄池中的地址。
  • 直接指针(HotSpot采用)
    从栈中直接指向堆中真实对象的内存地址。
    好处:速度快。

垃圾回收机制

概述

  • 如何判断对象为垃圾对象
    • 引用计数法
    • 可达性分析
  • 如何回收
    • 回收策略
      • 标记-清除算法
      • 复制算法
      • 标记-整理算法
      • 分代收集算法
    • 垃圾回收器
      • Serial
      • parnew
      • parallel
      • Cms
      • G1
  • 何时回收

判断对象是否存活

(1)引用计数法
在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就加1;当引用失效的时候,计数器的值就减1。
基本没有垃圾回收器使用该方法来判断对象是否存活,因为循环引用无法被回收。
(2) 可达性分析算法
从GC Root出发,判断是否可达。
哪些地方可以作为GC Root:

  • 虚拟机栈(栈帧中的局部变量表)
  • 方法区的类属性所引用的变量
  • 方法区中常量所引用的对象
  • 本地方法栈中引用的对象

(3)在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B;如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。
支配者被回收,被支配者对象也被回收)

垃圾回收算法

标记-清除算法

很基础很简单的算法;就是一个程序对垃圾对象进行标记,另一个程序清除对应的垃圾对象。
存在的问题:
(1)效率问题
(2)空间问题:会使的内存空间形成不连续的区块。

复制算法

对伊甸园区进行垃圾回收时,标记伊甸园区的垃圾,然后把不是垃圾的复制到幸存0区(或1区),清空伊甸园区;
对幸存0去(1区)进行垃圾回收时,标记幸存0区(1区)中的垃圾,把不是垃圾的复制到1区(0区),清空0区(1区)。
在复制算法中没有老年代,而是一个Tenured Gen;(?疑问待解决)
复制算法不适用于存活对象较多的场景,如老年代;最大的问题就是空间浪费。

标记-整理算法

(标记-整理-清楚)
针对老年代进行回收的一个算法。先标记垃圾,然后把垃圾和非垃圾整理(整合、合并),然后清除垃圾区域。

分代收集算法

复制算法+标记-整理算法
对新生代使用复制算法,对老年代使用标记-整理算法。

垃圾收集器-Serial

  • 最基本,历史最悠久的,效率高,可能会出现较长的停顿
  • 单线程垃圾收集器
  • 在桌面应用中还在使用
  • 新生代、老年代都使用串行回收;新生代使用复制算法,老年代使用标记-整理算法
    在这里插入图片描述

垃圾收集器-parnew

  • Serial的改进,垃圾收集的时候有多个线程
  • 老年代使用CMS垃圾收集器的时候,新生代只能使用Serial或者parnew
  • 新生代使用并行,老年代使用串行;
  • 多线程不一定快!!!!!!!!!!!!!
    在这里插入图片描述

垃圾收集器-parallel scavenge

  • 采用复制算法(新生代收集器)
  • 多线程收集器
  • 关注点:达到可控制的吞吐量;吞吐量:CPU运行用户代码的时间/CPU消耗的总时间;CPU消耗的总时间=CPU运行用户代码的时间+CPU执行垃圾回收的时间。
  • 相关参数:-XX:MaxGCPauseMillis垃圾收集器最大停顿时间(停顿时间短了,新生代的内存就会相应的变小,来满足垃圾收集器在指定的时间内完成垃圾收集。);
    -XX:GCTimeRatio 吞吐量大小
  • 有对应的老年代收集器

垃圾收集器-CMS(Concurrent Mark Sweep)

  • 对老年代进行收集
  • 工作过程
    (1) 初始标记:标记GCRoot能够直接到达的对象
    (2) 并发标记:接着(1)继续往下标记
    (3) 重新标记:对(2)中的标记进行修正
    (4) 并发清理
    在这里插入图片描述
  • 优点
    (1) 并发收集
    (2) 低停顿
  • 缺点
    (1) 占用大量的CPU资源
    (2) 无法处理浮动垃圾
    (3) 出现Concurrent Mode Failure
    (4) 空间碎片

垃圾收集器-G1

  • 优势
    (1)并行与并发;充分利用多核CPU
    (2)分代收集:没有新生代和老年代,而是把堆内存划分成了一个一个的region;并在回收的时候先根据remember set中记录的信息对region进行评估,对最有效的region进行回收。
    (3)空间整合(标记整理算法)
    (4)可预测的停顿
  • 步骤
    (1)初始标记
    (2)并发标记
    (3)最终标记
    (4)筛选回收
    在这里插入图片描述

内存分配策略

  • 优先分配到eden区域
  • 如果有大对象,可能会直接进入老年代
  • 长期存活的对象会被分配到老年代
  • 空间分配担保(当eden区的内存不够用的时候,向老年代去借)
  • 动态对象的年龄判断

逃逸分析与栈上分配

  • 逃逸分析:分析对象的作用域;当一个对象被定义在一个方法体内;如果该对象没有在方法的执行过程中引用外部的对象,那么该对象就没有发生逃逸;反之,该对象发生了逃逸。如果对象没有发生逃逸,就可以把这个对象分配到栈空间中,随着栈空间的回收一起被回收。
  • 针对的都是小对象,一般就是十几个bytes大小的对象。
  • 方法返回该对象,发生逃逸
  • 方法中为成员属性赋值,发生逃逸
  • 引用成员变量的值,发生逃逸

总结一张图

在这里插入图片描述

性能监控工具

jps

  • java process status
  • 能够显示本地虚拟机唯一id(lvmid local virtual machine id)(也就是该java应用的进程id吧)
  • jps -l 显示详细的主类信息或jar文件
  • jps -m 显示接收的参数
  • jps -v 显示接收的vm参数
  • jps -q 只显示进程号

jstat

  • 监控虚拟机的各种信息
  • 类装载,内存,垃圾收集,jit编译的信息
  • 详细使用参见官方文档

jinfo

  • 实时查看和调整虚拟机的各项参数
  • 打印java的system propertise(在windows系统中不支持)
  • jinfo -flag <name>:打印指定的JVM的参数的值
  • jinfo -flag [+|-]<name>:设置指定JVM参数的布尔值
  • jinfo -flag <name>=<value> 设置指定JVM参数的值

jmap

  • 导出堆内存(例:jmap -dump:format=b,file=xxx java进程号)
  • 查看堆中的统计信息(例:jmap -histo 2972 > c:\s.txt)

jhat(jvm heap analyse tool)

  • 对jmap导出的堆信息进行分析
  • 不常用!费内存!费cpu!基本用其他的可视化工具来代替了

jstack

  • 用于生成虚拟机当前时刻的线程快照
  • jstack -l 打印锁信息
  • jstack -m 答应java和native的帧信息
  • jstack -F 强制dump,当jstack没有响应时使用
  • java编程中有类似的功能:Thread.getAllStackTraces();

jconsole

  • 可视化的监控工具
  • 监控堆内存的变化
  • 监控线程的变化
  • 监控死锁

VisualVM(非自带)

认识类的文件结构

Class文件结构

  • Class文件是一组以字节(8位)为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有添加任何分隔符,整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
  • 当遇到大于一个字节的数据项时,则会按照高位在前的方式分割成若干个字节进行存储。
  • Class文件中有两种数据类型:无符号数和表。
  • 包含的内容:
    1. 魔数
    2. Class文件版本
    3. 常量池
    4. 访问标志
    5. 类索引、父类索引、接口索引集合
    6. 字段表集合
    7. 方法表集合
    8. 属性表集合
类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count-1
u2access_flags1
u2this_class1
u2super_class1
u2interface_count1
u2interfacesinterface_count
u2fields_count1
field_infofieldsfields_count
u2method_count1
method_infomethodsmethod_count-1
u2attribute_count1
attribute_infoattributesattribute_count

魔数和Class文件版本

  • class文件的前四个字节就是魔数,用十六进制表示是CAFE BABE
  • 魔数后面的4个字节是版本号,主版本号在后面;JDK 1.8的主版本号是52;1.7的主版本号是51;

常量池(cp_info)

紧接版本号后是常量池的长度,类型是u2(也就是两个字节);后面紧跟常量池。

cp_info format
typedescriptorremark
u1tag
-info[]长度有u1的值所代表的tag类型所决定

访问标志

在这里插入图片描述

字段表集合

  • 字段表用于描述类或者接口中声明的变量
    在这里插入图片描述
    在这里插入图片描述

方法表集合

Method_info{
Access_flags u2
Name_index u2
Descriptor_index u2
Attributes_count u2
Attribute_info[ ]
}

属性表集合

  • 描述字段表和方法表的额外信息
    attribute_info {
    attribute_name_index u2
    attribute_length u2
    info[ ]
    }

字节码指令

简介

  • java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字,称之为操作码,以及跟随在其后的零至多个代表此操作所需参数的操作数而构成。
  • 操作码的长度为一个字节,因此最长只有256条。
  • 基于栈的指令集架构。

字节码与数据类型

  • 在虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。
  • 例如:iload中的i代表整数,那么可以类推出一个指令fload,f代表浮点数。
  • 也有不包含数据类型的指令,例如 goto这种与类型无关 还有 arraylength这种某种类型所特有的指令
  • 类型多,指令少的问题,采取了很多策略。例如:byte、short、char都用int来执行;指令转换。

加载和存储指令

  • 加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
  • 将一个数值从局部变量表加载到操作数栈:iload lload fload dload aload
  • 将一个数值从操作数栈存储到局部标量表:istore lstore fstore dstore astore
  • 将一个常量加载到操作数栈: bipush sipush ldc ldc_w ldc2_w aconst_null iconst_m1 iconst
  • 扩充局部变量表的访问索引的指令:wide

运算指令

  • 运算或者算术指令用于对两个操作数栈上的值进行某种特定的运算,并把结果存储在操作数栈顶。
  • 加法指令:add
  • 减法指令:sub
  • 乘法指令:mul
  • 出发指令:div
  • 取余指令:rem
  • 取反指令:neg

类型转换指令

  • 类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作以及用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
  • 有宽化类型处理和窄化类型处理
  • l2b i2c i2s l2i …

对象创建与访问指令

  • 创建类实例的指令: new
  • 创建数组的指令:newarray anewarray multianewarray
  • 访问类字段:getfield putfield getstatic putstatic
  • 把数组元素加载到操作数栈的指令:(b,c,s,l,f,d,a)aload
  • 将操作数栈的值存储到数组元素:(b,c,s,l,f,d,a)astore
  • 取数组长度的指令:arraylength
  • 检查实例类型的指令:instanceof checkcast

操作数栈指令

  • 操作数栈指令用于直接操作操作数栈
  • 将操作数栈的一个或者两个元素出栈 pop pop2
  • 复制栈顶一个或者两个数值并将复制或双份复制值重新压人栈顶 dup dup2 dup_x1 dup_x2
  • 将栈顶的两个数值互换:swap

控制转移指令

  • 控制转移指令可以让java虚拟机有条件或者无条件地从指定位置的指令而不是控制转移指令的下一条指令继续执行程序。可以认为控制转移指令就是在修改pc寄存器的值。
  • 条件分支:ifeq iflt ifle ifgt ifnull ifcmple…
  • 复合条件分支:tableswitch lookupswitch
  • 无条件分支:goto goto_w jsr jsr_w ret

方法调用指令和返回

方法调用指令

  • invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是java语言中最常见的方法分派方式。
  • invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用。
  • invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
  • invokestatic用于调用类方法(static方法)。
    方法返回指令
  • 方法的返回指令跟类型有关,包括ireturn(当返回值类型位boolean,byte,char,short和int时),lreturn,freturn,dreturn和areturn;另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。

异常处理指令

  • 在程序中显式抛出异常的操作会有athrow指令实现,除了这种情况,还有别的异常会在其他java虚拟机指令检测到异常状况时由虚拟机自动抛出。

同步指令

管程

  • 管程(Monitor,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或者一群变量。
  • 管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。
  • 系统中的各种硬件资源和软件资源,均可用数据结构抽象描述其资源特性,即用少量信息和对资源所执行的操作来表征该资源,而忽略了它们的内部结构和实现细节。
  • 利用共享数据结构抽象地表示系统中的共享资源,而把对该共享数据结构实施的操作定义为一组过程。

同步指令

  • java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
  • 方法级的同步是隐式的,及无需通过字节码指令来控制,他实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时调用指令将会检查方法ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将持有管程,然后再执行方法,最后在方法完成时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得一个管程。如果一个同步方法在执行的过程中抛出了异常,并且在方法内部无法处理该异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
  • 同步一段指令集序列通常是由java语言中的synchronized块来表示的,java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要编译器与java虚拟机两者协作支持。

类加载机制

概述

  • 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
  • 懒加载

类加载时机

  • 类的生命周期
    在这里插入图片描述
  • 加载的时机没有明确的规范,Hotspot使用的是懒加载。
  • 连接只要加载的内容足够开始连接就开始了,一定比加载结束的要晚。
  • 初始化
    一、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先出发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象的时候、读取或者设置一个类的静态字段的时候,以及调用另一个类的静态方法的时候(fianl修饰的放到了常量池,不会触发)。
    二、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
    三、当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先出发其父类的初始化。
    四、当虚拟机启动时,用户需要指定一个执行的主类,虚拟机会先初始化这个主类。
    五、JDK7的动态语言
  • 不被初始化的例子
    • 通过子类引用父类的静态字段,子类不会被初始化
    • 通过数组定义来引用类
    • 调用类的常量

加载

  • 通过一个类的全限定名来获取定义此类的二进制流
  • 将这个流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在堆中生成一个代表这个类的class对象,作为这个类的各种数据的访问入口
  • 类加载的最终产品是位于堆中的Class对象;Class对象封装了类在方法区内的数据结构,并且向java程序员提供了该数据结构的接口

验证(非必要步骤,可以通过虚拟机参数跳过)

  • 验证是连接的第一步,之一阶段的目的是为了确保Calss文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。包括:文件格式验证、元数据验证、字节码验证和符号引用验证。

准备

  • 准备阶段正式为类变量分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配。
  • 这里的初始值并非我们设置的值,而是其默认值;但是如果变量被final修饰,那么这个过程中,常量值会被一同指定。

解析

  • 解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。在进行解析之前需要对符号引用进行解析,不同虚拟机实现可以根据需要判断到底是在类加载器加载的时候对常量池的符号引用进行解析(也就是初始化之前),还是等到一个符号引用被使用之间进行解析(也就是在初始化之后)。到现在我们已经明白解析阶段的时机,那么还有一个问题是:如果一个符号引用进行多次解析请求,虚拟机中处理invokedynamic指令外,虚拟机可以对第一次解析的结果进行缓存(在运行时常量池中记录引用,并把常量标识为已解析状态),这样就避免了一个符号引用的多次解析。解析动作主要针对类或者接口、字段、方法类型、方法句柄和调用点限定符号引用。这里主要说明前四种的解析过程。
  • 类或接口的解析
    要把一个类或者接口的符号引用解析为直接引用,需要以下三个步骤:
    (1)如果该符号引用不是一个数组类型,那么虚拟机将会把该符号代表的全限定名称传递给类加载器去加载这个类。这个过程由于涉及验证过程所以可能会触发其他相关类的加载。
    (2)如果该符号引用是一个数组类型,并且该数组的元素类型是对象。我们知道符号引用是存在方法区的常量池中的,该符号引用的描述符会类似"[java/lang/Integer"的形式,将会按照上线的规则进行加载数组元素类型,如果描述符如前面假设的形式,需要加载的元素类型就是java.lang.Integer,接着由虚拟机生成一个代表此数组对象的直接引用。
  • 字段解析
    对字段解析需要首先对其所属的类进行解析,因为字段是属于类的,只有正确解析得到其类的正确的直接引用才能继续对字段进行解析。对字段的解析主要包括以下几个步骤:
    (1)如果该字段符号引用就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,解析结束。
    (2)否则, 如果在该符号的类实现了接口,将会按照继承关系从下往上递归搜索各个接口,如果在接口中包含了简单名称和字段描述符都与目标相匹配的字段,那么就直接返回这个字段的直接引用,解析结束。
    (3)否则,如果该符号所在的类不是Object类的话,将会按照继承关系从下往上递归搜索其父类。如果在父类中包含了简单名称和字段描述符都与目标匹配的字段,那么就直接返回这个字段的直接引用,解析结束。
    (4)否则,解析失败,抛出java.lang.NoSunchFieldError。
    如果最终返回了这个字段的直接引用,就进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError。
  • 类方法解析
    进行类方法的解析仍需要先解析此类方法的类,在正确解析之后需要进行如下的步骤:
    (1)类方法和接口方法的符号引用是分开的,所以如果在类方法表中发现class_index(类中方法的符号引用)的索引是一个接口,那么会抛出java.lang.IncompatibleClassChangeError
    (2)如果class_index的索引确实是一个类,那么该类中查找是否有简单名称和描述符都与目标匹配的方法,如果有的话就返回这个方法的直接引用,查找结束。
    (3)否则,在该类的父类中递归查找是否具有简单名和描述符都与目标匹配的方法,如果有的话就直接返回,查找结束。
    (4)否则,在这个类的接口以及它的父接口中递归查找,如果找到的话就说明这个方法是一个抽象方法,查找结束。返回java.lang.AbstractMethodError
    (5)否则,查找失败,抛出java.lang.NoSuchMethodError
    如果最终返回了直接引用,还需要对该符号引用进行权限验证,如果没有访问权限,就抛出java.lang.IllegalAccessError
  • 接口方法解析
    同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用,如果解析成功,就进行下面的解析工作:
    (1)如果在接口方法表中发现class_index的索引是一个类而不是一个接口,那么就会抛出java.lang.IncompatibleClassChangeError
    (2)否则,在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标相匹配的方法,如果有的话直接返回这个方法的直接引用
    (3)否则,在该接口的父接口查找,直到Object类,如果找到则直接返回这个方法的直接引用
    (4)否则,查找失败
    接口的所有方法都是public,所以不存在访问权限问题。

初始化

类初始化是类加载过程的最后一步,前面类加载的过程,除了加载阶段用户应用程序可以通过自定义类加载器参与外,其余动作完全有虚拟机主要与掌控。到了初始化阶段,才是真正执行类中定义的java程序代码。
在准备阶段,变量已经赋过一次系统要求的初始值。而在初始化阶段,则根据开发者通过程序控制制定的主观计划去初始化类变量和其他资源。
初始化阶段是执行类构造器< clinit>()方法的过程。
先来看一段代码:

public class Demo{
	static{
		i = 0;
		System.out.println(i);
	}
	static int i = 1;
}

上面这段代码变量赋值语句可以通过编译,而下面的输出却无法通过编译。这是为什么呢?
< clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的语句块中可以赋值,但是不能访问。

public class Parent {
	public static int A = 1;
	static {
		A = 2;
	}
	static class Sub extends Parent {
		public static int B = A;
	}
	
	public static void main(String [] args) {
		System.out.println(Sub.B);
	}
}

子类的< clinit>()在执行之前,虚拟机保证父类的先执行完毕,因此在赋值前父类的static已经执行,因此结果为2。
接口中也有变量要赋值,也会生成< clinit>(),但不需要先执行父接口的< clinit>()方法,只有父接口中定义的变量使用时才会初始化。
如果多个线程同时初始化一个类,只有一个线程会执行这个类的 < clinit>()方法,其他的线程等待执行完毕。如果执行的时间过长,那么就会造成多个线程阻塞。

卸载

  • 一个类何时结束生命周期,取决于代表它的class对象何时结束生命周期。
  • 由java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。由用户自定义的类加载所加载的类是可以被卸载的。

类加载器

  • 虚拟机的设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己解决如何去获取所需要的类。实现这个动作的代码模块称为类加载器。
  • 只有被同一个类加载器加载的类的比较才有意义。相同的代码被不同的类加载器所加载以后得到的类一定不相等。

类加载器分类

  • 启动类加载器:由c++实现,是虚拟机的一部分,用于加载javahome下的lib目录下的类
  • 扩展类加载器:加载javahome下lib/ext目录中的类
  • 应用程序类加载器:加载用户类路径上的所指定的类库
  • 自定义类加载器

自定义类加载器的优势

  • 类加载器是java语言的一项创新,也是java语言流行的重要原因之一,它最初的设计是为了满足java Applet的需求而开发出来的。
  • 高度的灵活性
  • 通过自定义类加载器可以实现热部署
  • 代码加密

双亲委派模型

从JDK1.2开始,java虚拟机规范推荐开发者使用双亲委派模式进行类加载,其加载过程如下:

  • 如果一个类加载器收到了类加载请求,它首先不会去自己尝试加载这个类,而是把类的加载请求委派给父加载器去做。
  • 每一层的类加载器都把类加载请求委派给父加载器,直到所有的类加载请求都传递给顶层的启动类加载器。
  • 如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行加载。
  • 定义类加载器:如果某个类加载器能够加载一个类,那么该类加载器就称为定义类加载器;定义类加载器及其所有子加载器都称作初始类加载器。
  • 加载器之间的父子关系实际上是加载器对象之间的包装关系,而不是类之间的继承关系。一对父子类加载器可能是同一个加载器类的两个实例。
  • 当生成一个自定义的类加载器的实例时,如果没有指定指定他的父加载器,那么系统类加载器将成为他的父加载器
  • 每个类加载器都有自己的命名空间,该命名空间由该类加载器和其父加载器所加载的类构成;在同一个命名空间中,不会出现类的完整名字相同的两个类;在不同的命名空间中,有可能会出现类的完整名字相同的两个类。
  • 由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看他们的包名是否相同,还要看定义类加载器是否相同。只有属于同一个运行时包的类才能访问包可见的类和类成员。这样的限制能够避免用户自定义的类冒充核心类库中的类,去访问核心类库的包可见成员。

双亲委派模式的类加载机制的有点是java类和它的类加载器一起具备了一种带优先层次的关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。
在这里插入图片描述

打破双亲委派

Thread.setContextClassLoader(…)为线程设置一个上下文加载器,这是一个角色(任何的类加载器都可以担当这个角色),用以解决顶层ClassLoader无法访问底层的ClassLoader的类的问题。基本思想是在顶层的ClassLoader中传入底层ClassLoader的实例。
双亲委派模式是默认的模式,但不是必须要这么做;
Tomcat的WebappClassLoader就会先加载自己的class,找不到的时候再委托给parent
OSGI的ClassLoader形成网状结构,根据需要自由加载class

字节码执行引擎

主要内容

运行时栈帧结构

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 方法返回地址
  • 附加信息

方法调用

  • 解析
  • 分派
  • 动态语言支持

局部变量表

  • 最基本单位是slot。
  • 当一个变量的pc寄存器的值大于Slot的作用域的时候,Slot是可以复用的。
  • 不同栈帧之间的局部变量表是可能重叠的,方便方法之间进行高性能的参数传递。

操作数栈

虚拟机在操作数栈中存储数据的方式和局部变量表一样:如int、long、float、double和reference的存储。对于byte、short以及char类型的值压入到操作数栈之前,也会被转换为int。

动态连接

  • 在栈帧中包含一个指向运行时常量池中该栈帧所属方法的引用。?

方法返回地址和附加信息

  • 方法返回地址:方法调用时通过一个指向方法的指针指向方法的地址,方法返回时将回归到调用出,那个地方就是方法的返回地址。
  • 附加信息:虚拟机规范中允许具体的虚拟机实现增加一些规范中没有描述的信息到栈帧中,这部分信息完全取决于虚拟机的实现。

方法调用-解析分派

  • 方法调用并不等同与方法的执行,方法调用阶段的唯一任务就是确定被调用的方法的版本。
  • 编译器在进行编译时就已经能够确定下来调用哪个方法的情况,就叫做解析调用。也就是没有重写和重载的方法。

方法代用-静态分派调用

  • 根据声明类型进行方法选择,在编译时就能确定
  • 有多个匹配的方法,寻找最匹配的那个
  • 静态分派针对于方法的重载

方法调用-动态分派调用

  • 动态分派针对于方法的重写
  • 找到操作数栈顶的第一个元素所指向的对象的实际类型,如果在实际类型中找到与常量中的描述符和简单名称都相符的方法,则进行访问授权校验,如果通过则直接返回这个方法的直接引用,查找过程结束,如果不通过,则抛出异常;按照继承关系从下往上依次对实际类型的各父类进行搜索与验证,如果始终没有找到,则抛出AbstractMethodError。

双分派

  • 用来打破静态分派
/**
 * 通过双分派实现动态绑定
 */
public class DoubleAssignForDynamicBound {
    public static void main(String[] args) {
        OutputName out = new OutputName() ;
        Person p = new Person() ;
        Person man = new Man() ;
        Person woman = new Woman() ;
        p.accept(out) ;
        man.accept(out) ;
        woman.accept(out) ;
    }
}


class Person{
    public void accept(OutputName out) {
        out.print(this) ;
    }
}

class Man extends Person{
    public void accept(OutputName out) {
        out.print(this) ;
    }
}
class Woman extends Person{
    public void accept(OutputName out) {
        out.print(this) ;
    }
}


class OutputName{
    void print(Person p){
        System.out.println("person");
    }
    void print(Man m){
        System.out.println("man");
    }
    void print(Woman w){
        System.out.println("woman");
    }
}

动态类型语言支持

  • 静态类型语言在非运行阶段,变量的类型是可以确定的,也就是说变量是有类型的。
  • 动态类型语言在非运行阶段,变量的类型是无法确定的,也就是变量是没有类型的,但是值是有类型的,也就是在运行期间可以确定变量的值的类型。
  • 在jvm中也就是一个invokedynamic指令的支持
  • 执行js
    在这里插入图片描述

java线程高级

内存模型

在这里插入图片描述

happens-before

  • happens-before 是用来指定两个操作之间的执行顺序。提供跨线程的内存可见性。
  • 在java内存模型中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必然满足happens-before关系。
  • happens-before规则如下:
    • 程序顺序规则:当个线程中的每个操作,总是前一个操作happens-before于该线程中的任意后续操作。
    • 监视器锁规则:对一个锁的解锁,总是happens-before于随后对这个锁的加锁。
    • volatile变量规则:对volatile变量的写总是happens-before对该volatile变量的读。
    • 传递性:A happens-before B,B happens-before C,则 A happens-before C。
    • start规则
    • join规则

重排序问题

  • 什么是重排序:编译器和处理器为了提高程序的运行性能,对指令进行重新排序。
  • 数据依赖性(无依赖性、写后读、写后写、读后写)(as-if-serial)
  • 指令重排序分类:编译器重排序、处理器重排序
  • 为什么要进行指令重排序:提高程序的运行性能
  • 指令重排序所带来的影响:对于单线程只是提高了性能,没有什么附加的影响;对多线程的影响:
    在这里插入图片描述
    在这里插入图片描述
  • 竞争与同步

锁的内存语言

  • 锁的释放与获取所建立的happens-before关系
  • 锁的释放和获取的内存语义:锁除了让临界区互斥执行以外,还可以让释放锁的线程向获取同一个锁的线程发送消息(个人理解,就是强刷主内存中的变量,让获取锁的线程中的缓存变量失效)。

volatile的内存语义

-volatile所建立的happens-before关系

  • volatile的内存语义

final的内存语义

  • 写final域的重排序规则:写final域从重排序规则禁止把final域的写重排序到构造方法之外。
  • 读final域的重排序规则:在一个线程中,初次读对象引用和初次读该对象所包含的final域,java内存模型禁止处理器重排序这两个操作。
  • final域为静态类型
  • final域为抽象类型:在构造方法内对一个final引用的对象成员域的写入,与随后在构造方法外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

附录

一、JVM参数

-XX:+…是开启;-XX:-…是关闭

  1. -XX:+HeapDumpOnOutOfMemoryError 在内存溢出时,保存堆快照(保存的快照可以通过 Eclipse Memory Analyzer来进行分析)
  2. -XX:HeapDumpPath=d:/a.dump 导出OOM的路径
  3. -Xms20m 最小堆内存
  4. -Xmx20m 最大堆内存
  5. -Xmn20m 指定新生代的内存
  6. -XX:newSize 也是指定新生代的内存(和 -Xmn 那个后设置,哪个生效)
  7. -XX:MaxnewSize 新生代最大内存
  8. -XX:InitialSurvivorRatio 新生代Eden/Survivor空间的初始比例
  9. -XX:Newratio Old区 和 Yong区 的内存比例(例如4,代表yong:old=1:4)
  10. -XX:SurvivorRatio 设置两个Survivor去和eden区的比例(例如8,代表survivor:eden=1:8;2*survivor:eden=2:8)
  11. -XX:MetaspaceSize=8m 1.8后的参数,元数据区大小
  12. -XX:MaxMetaspaceSize=50m 1.8后的参数,元数据区的最大大小
  13. -XX:MaxTenuringThreshold 设置分代年龄阈值
  14. -verbose:gc 打印垃圾回收日志
  15. -XX:+PrintGCDetails 打印的垃圾回收日志更加详细
  16. -XX:+UseSerialGC 启用Serial垃圾收集器,在新生代和老年代使用串行收集器
  17. -XX:+UseParNewGC 在新生代使用并行收集器
  18. -XX:+UseParallelGC 在新生代使用并行回收收集器
  19. -XX:+UseParallelOldGC 老年代使用并行回收收集器
  20. -XX:ParallelGCThreads 设置用于垃圾回收的线程数(parallel)
  21. -XX:MaxGCPauseMillis垃圾收集器最大停顿时间(parallel)
  22. -XX:GCTimeRatio 吞吐量大小,取值范围(0,99)(parallel)
  23. -XX:+UseConcMarkSweepGC:新生代使用(parnew),老年代使用CMS+串行收集器(处理碎片问题)
  24. -XX:+ParallelCMSThreads 设定CMS的线程数量
  25. -XX:CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间被使用多少后触发
  26. -XX:+UseCMSCompactAtFullCollection 设置CMS收集器在完成FullGC后是否进行一次碎片整理
  27. -XX:CMSFullGCsBeforeCompaction 设定进行多少次CMS垃圾回收后,进行一次内存压缩
  28. -XX:+CMSClassUnloadingEnabled 允许对类元数据进行回收
  29. -XX:CMSInitiatingPermOccupancyFraction 当永久区达到这一百分比时,启动CMS回收
  30. -XX:+UseCMSInitiatingOccupancyOnly 表示只在达到阈值的时候,才进行CMS回收
  31. -XX:PretenureSizeThreshold 大于该值的对象直接进入老年代
  32. -XX:MaxTenuringThreshold 指定多大的存活年龄的对象可以进入老年代(jdk7以后并不严格按照该标准来执行)
  33. -XX:+HandlePromotionFailure 开启内存空间担保
  34. -XX:+PrintGC 开启GC日志打印
  35. -XX:+DoEscapeAnalysis 开启逃逸分析
  36. -XX:+PrintGCTimeStamps 打印GC发生的时间戳
  37. -Xloggc:log/gc.log 指定gclog的位置,以文件输出
  38. -XX:+PrintHeapAtGC 每一次GC后都打印堆信息
  39. -XX:+TraceClassLoading 监控类的加载
  40. -XX:+PrintClassHistogram 按下Ctrl+break后,打印类的实例个数
  41. -XX:OnOutOfMemoryError 在OOM时执行一个脚本
  42. -Xss: 设置栈的大小
  43. -XX:+EliminateLocks 开启锁消除,需要和逃逸分析配合使用
  44. -XX:+UseBiasedLocking 开启偏向锁,默认开启;
  45. -XX:BiasedLockingStartupDelay=0 偏向锁的启动延时;
  46. -XX:CompileThreshold=1000 设置JIT编译阈值(JIT(just in time)编译;方法调用计数器:方法调用次数回边计数器:方法内循环次数两个)
  47. -XX:+PrintCompilation 打印编译的结果
  48. -Xint 解释执行
  49. -Xcomp 全部编译执行
  50. -Xmixed 默认,混合
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐