java的长处:

  • 解脱了硬件平台解放,实现了一次编写,到处运行的现实
  • 提供了一个绝对平安的内存治理和拜访机制,防止了绝大多数的内存透露和指针越界
  • 实现了热点代码检测和运行时编译及优化,使得java利用能随着运行工夫的减少而取得更高的性能
  • 有一套欠缺的利用程序接口,还有有数商业机构和开源社区的第三方类库

第一局部

Java技术体系

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

JDK : java程序设计语言,java虚拟机和java api。 是反对java程序开发的最小环境。
JRE : 把java api类库重的 java SE api子集和java虚拟机这两局部统称为jre。 Jre 是反对java程序经营的规范环境

第二局部 主动内存治理

2 运行时数据区域

2.1 程序计数器

能够看作是以后线程所执行的字节码行号指示器

如果线程正在执行的是一个java办法,这个计数器记录的是正在执行的虚拟机字节码的指令地址;如果正在执行的是Native办法,这个计数器值为空(Undefined)。此内存区域是惟一一个在java虚拟机标准中没有规定任何OOM状况的区域

2.2虚拟机栈

线程公有的,生命周期与线程雷同。
虚拟机栈形容的是java办法执行的内存模型:每个办法在执行时都会创立一个栈帧。用于存储局部变量表、操作数栈、动静链接、办法进口等信息。

局部变量表寄存了编译器可知的各种根本类型(boolean、byte、char、short、int、float、long、double)对象援用(refrence类型,他不等同于对象自身,可能是一个执行对象起始地址的援用指针,也可能是只想一个代表对象的句柄或其余与此对象相干的地位)和returnAddress类型(指向了一条字节码指令的地址)

局部变量表所须要的内存空间在编译期间实现调配,当进入一个办法时,这个办法须要在帧中调配多大的局部变量空间是齐全确定的。

2.3本地办法栈

本地办法栈与虚拟机栈施展的作用相似,不过虚拟机栈为虚拟机执行java办法(也就是字节码)服务,本地办法栈则为虚拟机应用到的Native办法服务。
在虚拟机标准中办法应用的语言、应用的形式与数据结构并没有强制规定,因而具体的虚拟机能够自在实现。
本地办法栈与虚拟机一样也会跑出StackOverflowError 和 OutOfMemoryError

2.3java 堆

java堆是被虽有线程共享的一块内存区域。
此区域的惟一目标就是寄存对象实例。简直所有的对象实例都在这里分配内存。

  • 所有的对象实力以及数组都要在堆上调配,然而随着JIT编译器的倒退和陶艺剖析技术逐步成熟,栈上调配、标亮替换优化技术将会导致一些奥妙的变动,所有的对象都调配在堆上也慢慢变得不那么相对了。

因为当初的收集器根本都采纳粉黛手机算法,所以Java堆还能够细分为:新生代和老年代。
再粗疏一点:

  • Eden
  • From Survivor
  • To Survivor
  • Old
  • Permanent(对于HotSpot)

如果在堆中没有内存实现实例调配,并且堆也无奈再扩大时,将会抛出OutOfMemoryError异样

2.5 办法区

(Method Area) 与堆一样,是各个线程共享的内存区域。用于存储曾经被虚拟机记录的类信息、常量、动态变量、即时编译器编译后的代码等数据。尽管Java虚拟机标准把办法区形容为堆的一个逻辑,然而它却有一个别名叫做Non-heap,目标应该是与Java堆辨别开

仅对于Hotspot虚拟机来说,存在永恒代这个概念。仅仅是因为HotSpot虚拟机设计团队抉择把GC分代手机扩大至办法去,或者应用永恒代实现办法区而已。对于其余虚拟机则不存在永恒代。

对这一区域的内存回收次要是针对常量池的回收和对类型的卸载。

当办法去无奈满足内存调配需要时将抛出OOM异样

2.6 运行时常量池

(Runtime Constant Pool)是办法区的一部分。Class文件中除了有类的范本、字段、办法、接口信息等、还有一项是常量常量池(Constant Pool Table),用于寄存编译期生成的各种字面量和符号援用。

运行时常量池绝对于Class文件常量池的另外一个重要特色是具备动静个性,Java语言并不要求常量肯定只有编译期能力缠身,也就是并非预置入Class文件中常量池的内容能力进入办法区是进入常量池。运行期间可能将新的常量放入池中。

当常量池无奈再申请到内存是会抛出OutOfMemoryError

2.7 间接内存

间接内存(Direct memory) 并不是虚拟机运行时数据区的一部分,也不是Java虚拟机标准中定义的区域。然而这部分内存被间接被频繁应用,也可能导致OutOfMemoryError

在JDK1.4中新退出了NIO类,引入了一种基于通道(Channel)与缓冲区(BUffer)的I/O形式,它能够应用Natice函数间接调配堆外内存,而后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的援用进行操作。这样能在一些场景中进步性能,因为防止了Java堆和Native对中的来回复制数据。

3 HotSpot虚拟机探秘

3.1 对象的创立

虚拟机遇到一条new指令时

  • 查看这个指令的参数是否能在常量池中定位一个类的符号援用,并且查看这个符号援用代表的类是否曾经被加载、解析和初始化过。如果没有,则必须执行相应的类加载过程
  • 虚拟机将为新生对象分配内存。对象所需的大小在类加载实现后便能够确定。
对于频繁创建对象的状况,并发时,并不是线程平安的。可能呈现正在给对象A分配内存,还没来得及批改,对象B又同时应用了原来的指针来分配内存。解决这个问题有两种计划1、对于分配内存空间的动作同步解决——实际上,虚拟机采纳CAS配上失败重试保障更新操作的原子性2、另一种是把内存调配的动作依照线程划分在不同的空间之中进行,没喝线程在Java堆中事后调配一小块内存,成为本地线程调配缓冲(Thread Local Allocation Buffer, TLAB)能够通过 -XX:+/-UseTLAB参数来设定
  • 内存调配完后,虚拟机须要将调配到的内存空间都初始化为零值(不包含对象头)
  • 虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何能力找到类的元数据信息、对象的哈希码、对象在GC分代年龄等信息。这些信息寄存在对象头中。依据虚拟机以后的运行状态不同,如是否应用偏差锁等,对象头会有不同的设置形式
  • 下面的工作都实现后,从虚拟机的视角来看,一个新的对象曾经缠身了, 然而从Java程序的角度来看,对象的创立才刚刚开始——<Init>办法还没有执行,所有的字段都还是零。所以,一般来说(由字节码中是否追随有invokespecial指令所决定),执行new指令之后会接着执行<init>办法,把对象进行初始化。这样才真正可用的对象才齐全产生进去。

3.2 对象的内存布局

hotspot虚拟机中,对象在内存中存储的布局能够分为三块区域:

  • 对象头(header)
  • 实例数据(Instance data)
  • 对齐填充(Padding)
对象头

hotspot虚拟机的对象头包含两局部

第一局部用于存储对象本身的运行时数据,如Hash码,GC年龄分代,锁状态标记、线程持有的锁、偏差线程ID、偏差工夫戳等这部分在32位和64位虚拟机中别离占32bit和64bit(Mark word)

对象头信息是与对象本身定义的数据无关的额定存储老本。
Mark word 被设计成一个非固定的数据结构以便在极小的空间存储尽量多的信息,它会依据对象的状态复用本人的存储空间。

存储内容标识为状态
对象哈希码、对象年龄分代01未锁定
指向锁记录的指针00轻量级锁定
指向重量级锁的指针10收缩(重量级锁定
空,不须要记录信息11GC标记
偏差线程ID,偏差工夫戳、对象分代年龄01可偏差
对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

并不是多有的虚拟机实现都必须在对象数据上保留类型指针。也就是说,查找对象元数据信息不肯定要通过对象自身。对于Java数组,对象头中还有一块用于记录数组长度的数据,因为虚拟机能够通过一般java对象的元数据信息确定java对象大小,然而从数组的元数据中缺无奈确定数组大小。

实例数据局部

也就是程序代码中所定义的各类型的字段内容
这部分的存储程序会受到虚拟机调配策略参数和字段值java中定义程序的影响

对齐填充

不是必然存在的,仅仅起着占位符的作用

3.3 对象的拜访定位

java须要通过栈上的reference数据来操作堆上的具体对象。因为reference类型在java虚拟机标准中只规定了一个指向对象的援用,并没有定义这个援用应该通过何种形式定位、拜访堆中的对象的具体位置,所以对象拜访形式也是取决于虚拟机实现而定的。
目前支流形式是应用句柄间接指针

  • 如果应用句柄,java会划出一块内存作为句柄池,
  • 如果应用间接指针拜访,那么java堆对象的布局就必须思考如何搁置拜访类型数据的相干信息,而reference中存储的间接就是对象地址
这两种对象拜访形式各有劣势,应用句柄来拜访的最大益处就是reference中存储的是稳固的句柄地址,在对象被挪动时,只会扭转句柄中的实例数据指针。应用间接指针拜访形式的最大益处就是速度更快,它节俭了一次指针定位的工夫开销。

3 垃圾收集器与内存调配策略

对于GC须要实现的三件事

  • 哪些内存须要回收
  • 什么时候回收
  • 如何回收

3.2 对象已死

3.2.1援用计数法

循环援用问题

3.2.2 可达性剖析

Reachability Analysis
通过一系列成为GC Roots的对象作为起始点,从这些节点开始向下搜寻。
搜寻走过的门路成为援用链。当一个对象的GC roots 没有任何援用链相连接(用图论的话来说,就是GC roots到这个对象不可达),则证实此对象是不可用的。

GC roots包含

  • 虚拟机栈、栈帧中的本地变量表中援用的对象
  • 办法区中类动态属性援用的对象
  • 办法区中常量援用的对象
  • 本地办法站JNI援用的对象
3.2.3在谈援用

jdk1.2以前,java中的援用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表的是一个援用。
对于形容一类对象:当内存空间还够时,可能保留在内存之中,如果内存空间进行垃圾收集后还是十分缓和,则能够抱起这些对象。很多零碎的缓存性能都合乎这样的利用场景。

java对援用的概念进行了裁减

  • 强援用 就是指代码中普遍存在的,这类援用,只有抢援用还在,垃圾收集器永远不会回收掉被援用的对象
  • 软援用 用来形容一些还有用然而非必须的对象。对于软援用关联着的对象,在零碎将要产生内存溢出前,将会把这些对象进行二次回收。
  • 弱援用 用来形容非必须的对象,他的强度比软援用更弱。被软援用关联的对象只能生存到下一次垃圾收集产生前。无论内存是否足够,都会被回收掉。
  • 虚援用 是最弱的一种援用关系。一个对象是否有虚援用存在,齐全不会影响其生存工夫。也无奈通过虚援用来获得一个对象的实例。一个对象设置虚援用的惟一目标就是能在这个对象被零碎收集时收到一个零碎告诉。
3.2.4生存还是死亡

即便在可达性剖析中不可达的对象,也并非是非死不可的。 他们会处于一个缓刑的状态。真正宣告一个对象的死亡,要经验两次标记过程:

  • 如果对象在可达性剖析时发现没有GCroot与其向链,那么将会被第一次标记,并且会进行筛选筛选的条件是次对象有没有必要执行finalize()办法。
  • 如果这个对象被断定为有必要执行finalize()办法,那么这个对象会被放在一个叫做F-Queue的队列之中。finanlize()是一个对象逃脱死亡命运的最初一次机会。稍后GC将会对F-Queue中的对象进行第二次小规模的标记,如果对象要在finanlize()中援救本人,只须要从新与援用连伤的任何一个对象建设关联即可。

任何一个对象的finalize()都只会被零碎主动调用一次。如果零碎面临下一次回收,那么finanlize()将不会被执行。

3.2.5回收办法区

永恒代的垃圾回收次要回收两局部内容, 废除常量和无用的类

无用的类

  • 该类所有的实例都曾经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的classLoader曾经被回收
  • 该类对应的java.lang.Class对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法

3.3 垃圾收集算法

3.3.1 标记革除算法(Mark-Sweep)

最根底的收集算法

  1. 首先标记处所有须要回收的对象,在标记实现后对立回收所有被标记的对象。
  2. 之所以说是最根底的算法,是因为后续的收集算法都是基于这种思路并对其有余进行了改良而失去的。

有余

1.效率问题,标记和革除的两个过程效率都不高2.革除之后会产生大量不间断的内存碎片,空间碎片太多会导致当前调配较大对象的时候无奈找到最狗的间断内存而不得不提前登程垃圾收集动作。

3.3.2 复制算法

代价是将内存放大为了原来的一半
3.3.3 标记整顿算法

依据老年代的特点,提出了标记整顿算法(Mark-Compact)算法

3.3.4分代收集算法

Generational Collection
常识依据对象存活周期的不同将内存划分为几块。
个别把堆分为新生代和老年代,这样就能够依据各年代的特点采纳最适宜的收集算法。

  • 新生代个别采纳复制算法
  • 老年代采纳标记清理或者标记整顿算法

3.4 Hotspot 的算法实现

3.4.1 枚举根结点

作为GC root的节点次要在全局性的援用(例如常量或类动态属性)与执行上下文(例如栈帧的本地变量表)中,当初很多利用仅仅办法区就有数百兆,如果要一一查看这外面的援用,那么必然会耗费很多工夫。

另外可达性剖析对执行工夫的敏感还体现在GC进展上

HotSpot应用一组成为OopMap的数据结构来达到这个目标。

3.4.2 平安点

程序执行时并非在所有的中央都能停顿下来开始GC,只有达到平安点能力暂停。
平安点的选定不能太少,以至于让GC等待时间过长,也不能过于频繁导致过分增大运行时的负荷。
所以,平安点的选定基本上是以程序“是否具备让程序长时间执行的特色”为规范。

对于平安点另一个须要思考的问题是,如何在GC产生时,让所有线程都跑到平安点在停顿下来。

  • 抢占式中断
不须要线程的执行代码被动去配合,在GC产生时,首先把所有线程全副中断, 如果发现有线程终端的中央不再平安点上,就复原线程,让他跑到平安点上
  • 主动式中断
在GC须要中断线程时,不间接对线程操作,仅仅简略的设置一个标记,各个线程执行时,被动去轮询这个标记,发现中断中断标记为真时就终端本人挂起。
3.4.3 平安区域

平安区域是指在一段代码中,援用关系不会发生变化。在这个区域中的任意地位开始GC都是平安的。咱们能够把safe region看作是被扩大了的Safepoint
在线程执行到Safe region时,首先标示本人曾经进入了Safe region,那样,当在这段时间里jvm要发动GC时,就不必管标示本人为Safe Region状态的线程了。在线程要来到Safe Region时,它要查看零碎是否曾经实现了根结点枚举(或者是整个GC过程),如果实现了,那线程就继续执行,否则他就必须期待直到收到能够来到Safe Region的信号为止。

3.5 垃圾收集器

3.5.1 serial
3.5.2 ParNew收集器
3.5.3 Parallel Scavenge收集器
3.5.4 Serial Old
3.5.5 Parallel Old
3.5.6 CMS收集器
3.5.7 G1收集器

3.6 内存调配与回收策略

依据本人应用的收集器写一写程序去验证

3.6.1 对象优先在Eden调配

对象在Eden中调配,如果Eden中没有足够空间时,虚拟机发动MinorGc

-XX:SurvivorRation=8 决定Eden与一个Survivor的空间比例
  • 新生代GC (MinorGC)十分频繁,回收速度较快
  • 老点奶GC(MajorGC)
3.6.2 大对象间接进入老年代
-XX:PremenureSizeThreshold大于这个值的对象间接调配到老年代中

这样做的目标是为了防止Eden区以及两个Survivor区之间产生大量内存复制

PretenureSizeThreshold 支队Serial和ParNew两款收集器无效。
3.6.3 长期存活的对象将进入老年代

虚拟机给每个对象定义了对象年龄(Age)计数器。
如果对象在Eden出世,并通过第一次Minor GC后依然存活,并且能被Survivor包容的话,将被挪动到Survivor空间,并且对象年龄设为1。
对象年龄减少到肯定水平就会被降职到老年代

-XX:MaxTenuringThreshold能够设置年龄阈值
3.6.4 动静对象年龄断定

为了能更好的适应不同应用程序的内存情况,虚拟机并不永远要求对象的年龄必须达到MaxTenuringThreshold中要求的年龄
如果Survivor空间中雷同年龄所有对象的大小综合大于Survivor空间的个别,年龄大于或等于该年龄的对象就能够间接进入老年代

3.6.5 空间调配担保

在产生MinorGC之前 虚构机会先查看老年代最大可用的间断空间是否大于新生代所有对象总空间。 如果这个条件成立,那么MinorGC能够确保是平安的。
如果不成立,则虚构机会查看HandlePromotionFailure设置值是否容许担保失败。
如果容许,那么会持续查看老年代最大可用的间断空间是否大于历次降职到老年代对象的均匀大小。
如鬼大雨平寻大小,则会尝试进行一次MinorGC
如果小于,或者HandlePromotionFailure设置不容许冒险,那么这是要改为进行一次Full GC

4 虚拟机性能监控与故常解决

定位系统问题的时候,常识、教训是要害根底,数据是根据。这里说的数据包含:运行日志、异样堆栈、GC日志、线程块照、堆转储块照等

5 调优案例剖析与实战

高性能硬件上部署程序

  • 通过64位JDK来应用大内存
  • 应用若干个32位虚拟机建设逻辑集群来利用硬件资源
应用若干个32位虚拟机建设集群来利用硬件资源,具体做法是在一台物理机器上启动多个应用服务过程,,诶个服务器过程调配不同端口,而后在前端搭建一个负载均衡器,以反向代理的形式来调配拜访申请。

应用逻辑集群的形式部署程序,可能会遇到一下问题

  • 尽量避免节点竞争全局资源,最典型的就是磁盘竞争
  • 很难最高效率的利用某些资源池,譬如链接池
  • 各节点仍不可避免的收到32位内存的限度。
  • 大量应用本地缓存的利用,在逻辑集群中会造成较大的内存节约

第三局部 虚拟机执行子系统

6 类文件构造

代码编译后果就是从本地机器码转变为字节码

各种不同平台的虚拟机与所有平台都对立应用的程序存储格局--字节码(ByteCode)形成了平台无关性的基石

In the future, we will consider bounded extensions to the Java virtual machine to provide better support for other languages

实现语言无关性的根底依然是虚拟机和字节码存储格局
java虚拟机不和包含java在内的任何语言班丁,它治愈Class文件这种特定的二进制文件格式所关联。Class文件中伴晗Java虚拟机指令集和符号表以及若干其余辅助信息

6.3 Class类文件构造

《Java虚拟机标准》
任何一个Class文件都对应着惟一一个类或接口的定义信息。然而类或接口并不一定都得定义在文件里(譬如类或接口能够通过类加载器间接生成)

Class文件是一组以8位字节为根底单位的二进制流。
当遇到占用8位字符以上空间的数据项时,会依照高位在前的形式宰割成若干个8位字节进行存储

Class文件格式采纳一种相似C语言构造题的伪构造来存储数据,这种伪构造只有良种数据类型:无符号数和表

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant__pool_count-1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofields1
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attribute_infoattributesattribute_count
6.3.1 魔数与class文件版本

每个class文件的头4个字节成为魔数,惟一的作用是确定这个文件是否能被虚拟机承受的Class文件。

0xCAFEBABE

魔数前面的第5、第6个字节是次版本号, 第7和第8个字节是主版本号

6.3.2 常量池

紧接着主版本号是常量池入口,常量池能够了解为是Class文件的资源仓库

常量池的索引从1开始。设计者将第0项空进去,是为了满足前面某些只想常量池的索引值的数据在特定状况下须要表白“不援用任何一个常量池我的项目”的含意

Class文件中只有常量池的容量计数是从1开始的

长岭池中次要寄存两类常量:字面量符号援用
字面量靠近于Java语言层面常量概念,文本字符串、申明为final的常量值等
符号饮用属于编译原理方面的概念

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 办法的名称和形容福

当虚拟机运行时,须要从常量池取得对应符号饮用,再在类创立时或运行时解析、翻译到具体的内存地址中。
常量池中每一项常量都是一个表

6.3.3 拜访标记

(Access flags)两个字节代表拜访标记
用于辨认一些类或者接口档次的访问信息

  • class 是类还是接口
  • 是否定义为public
  • 是否是abstract
  • 是否被申明为final
6.3.4 类索引、父类索引与接口索引汇合
  • 类索引用于确定这个类的全限定名
  • 父类索引用于确定这个类父类的全限定名
6.3.5 字段表汇合

(field_info)用于形容接口或者类中申明的变量

  • 作用域 (public private protected)
  • 实例变量还是类变量
  • 可变性
  • 字段数据类型
  • 字段名称

字段表汇合中不会列出从超类或者父接口中继承而来的字段,但有可能列出本来Java代码之中不存在的字段,譬如在内部类中味了放弃对外部类的拜访性,会主动增加只想外部类实例的字段。

6.3.6 办法表汇合
  • 拜访标记
  • 名称索引
  • 描述符索引
  • 属性表汇合

7 虚拟机类加载机制

类加载机制:虚拟机把形容类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机间接应用的java类型

与那些在编译时须要进行连贯工作的语言不通,java语言,类型的加载、连贯和初始化都是在程序经营期间实现的。这种策略尽管减少一些性能开销,然而为Java应用程序提供了高度的灵便
Java里天生能够动静扩大的语言个性就是以来经营期动静加载和动静链接的特点实现的。
用户可通过Java与定义的和自定义的类加载器,让一个本地的应用程序能够在运行时从网络或者其余中央家在一个二进制流作为程序代码的一部分。

7.2 类加载的机会

类从加载到虚拟机开始到卸载出内存整个生命周期包含:

  • 加载
  • 验证
  • 筹备
  • 解析
  • 初始化
  • 应用
  • 卸载

虚拟机规定有且只有5种状况下必须立刻对类执行初始化

  • 遇到 new, getstatic, putstatic或invokestatic这四个字节码指令时。别离对应实例化一个对象、读取或者设置一个类的动态字段(被final润饰、已在编译器把后果放入常量池的动态字段除外)、调用一个类的静态方法
  • 应用java.lang.reflect包的办法对类进行反射调用的时候
  • 当初始化一个类的时候,如果发现其父类还没初始化,则先初始化其父类
  • 当虚拟机启动时,用户须要制订一个要执行的主类,虚构机会先初始化这个主类
  • 应用JDK 1.7的动静语言反对时,如果一个java.lang.invoke.MethodHandle实例最初的解析后果REF_getStatic,REF_invokeStatic,REF_invokeStatic的办法句柄,并且这个办法句柄所对应的类没有进行初始化,则须要先登程初始化。

这5种行为被称为被动援用
除此之外,所有的援用类的办法都不会登程初始化,被称为被动援用。

接口与类的初始化的区别在于上述第三条:当一个类在初始化时,要求其把父类全副都曾经初始化过了,然而一个接口在初始化时,并不要求其父接口全副都曾经实现了初始化,只有在真正应用到父接口时才会初始化。

7.3 类加载过程

加载 验证 筹备 解析和初始化

7.3.1 加载
  1. 通过类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的精彩存储构造转化为办法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为办法区这个类的各种数据的拜访入口

对于类加载的其余阶段,一个非数组类的加载阶段(精确的说是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的
对于数组类而言,数组类自身不通过类加载器创立,而是有java虚拟机间接创立的。然而数组类与类加载器仍有亲密的关系,因为数组类的元素类型最终要靠类加载器去创立
一个数组类创立过程遵循一下准则

  • 如果数组的组件类型是援用类型,那就递归采纳本节中定义的加载过程去加载组件类型。数组c将在加载该组件类型的类加载器的类名称空间上被标识
  • 如果数组的组件类型不是援用类型(如int[]),java虚构机会将数组c标记为与疏导类加载器关联
  • 数组类的可见性与它的组件类型是统一的,如果组件类型不是援用类型,那数组类的可见性将默认为public

Class对象比拟非凡,它尽管是对象,然而寄存在办法区外面,这个对象将作为程序拜访办法区中这些类型数据的内部接口

7.3.2 验证

为了确保Class文件的字节流中蕴含的信息负荷以后虚拟机的要求

  • 文件格式验证。基于二进制字节流进行的

基于办法区的存储构造进行的

  • 元数据验证 语义校验
  • 字节码验证 通过数据流和控制流剖析,确定程序语义是否合乎逻辑,是否非法
  • 符号援用验证。虚拟机将符号援用转化为间接援用
7.3.3 筹备

筹备阶段是正式为类变量分配内存并设置类变量初始值的阶段 这些变量所应用的内存都将在办法区进行调配
内存调配的仅包含类变量(static润饰的变量),而不是实例变量,实例变量将会在对象实例化时随着对象一起调配在java堆中

static初始化时为0值,对static变量赋值的putstatic指令时程序被编译后,寄存于类结构器<cinit>()办法之中。所以static赋值的动作在初始化才执行

数据类型零值
int0
long0L
short(short)0
char'u0000'
byte(byte)0
booleanfalse
float0.0f
double0.0d
referencenull

ConstantValue属性所指的值,在筹备阶段就会被初始化
static final

7.3.4 解析

解析是虚拟机将常量池的符号援用替换为间接援用的过程

  • 符号援用(Symbolic Reference) 符号援用以一组符号来形容所援用的指标,符号能够是任何模式的字面量,只有应用时能无歧义地定位到指标即可

符号援用与虚拟机内存布局无关,援用的指标不肯定曾经加载到内存中。符号援用的字面量模式明确定义在Java虚拟机标准中

  • 间接援用(Direct References) 间接援用能够是间接指向指标的指针、绝对便宜粱或者一个可能间接定位到指标的句柄。

间接援用是和虚拟机实现的内存布局相干的。

解析动作次要针对类或接口、字段、类办法、接口办法、办法类型、办法句柄和调用点限定符7类符号援用进行

7.3.5 初始化

初始化阶段是执行类结构器<cinit>()办法的过程

  • <cinit>()是由编译器主动收集所有的类变量的赋值动作和动态语句快(static{})合并而产生的,编译器收集的程序是由语句在源文件中呈现的程序决定的。

动态语句块只能拜访到定义在动态语句块之前的变量。定义在之后的变量,后面的动态语句块能够赋值,然而不能拜访。

public class Test{        static{            i = 0;  //能够赋值            System.out.print(i);  //不可援用        }        static int i = 1;}
  • <cinit>()与类构造函数不通,他不须要显示调用父类结构器,虚拟机保障子类<cinit>()执行前,父类的<cinit>()执行结束
  • 父类的<cinit>()限制性,就意味着父类的定义的动态语句快优先执行
  • <cinit>()对于类或接口并不是必须的,如果一个类中没有动态语句块,也没有对变量的复制操作,那么编译器能够不为这个类生成<cinit>()办法
  • 接口中不能应用动态语句块,但仍有变量初始化的赋值操作,因而接口与类一样都会生成<cinit>()办法,但接口与类不同的是,执行接口的<cinit>()不须要先执行父接口的<cinit>(),只有当父接口中定义的变量应用时,才会初始化父接口
  • 虚构机会保障一个类的<cinit>()办法在多线程环境中被正确的加锁、同步

7.4 类加载器

通过一个类的全限定名来获取形容此类的二进制字节流

7。4.1 类与类加载器
  • 对于任意一个类,都须要由加载它的类加载器和这个类自身一起确定其在java虚拟机中的唯一性
  • 每一个类加载器都领有一个独立的类名称空间

比拟两个类是否相等,只有来两个类是由同一个类加载器加载的前提下才有意义。

7.4.2 双亲委派

Java虚拟机存在两种不同的类加载器:

  • 启动类加载器(BootStrap ClassLoader) 这个类加载器应用C++实现,是虚拟机本身的一部分
  • 其余类加载器,这些都是Java语言实现的,独立于虚拟机内部,全副继承java.lang.ClassLoader
  • 启动类加载器,负责将寄存在<JAVA_HOME>lib目录中的,或者被-Xbootclasspath参数所指定的门路中的,并且是被虚拟机是别的类库加载到虚拟机内存中。

启动类加载器无奈被java程序间接援用,用户在编写自定义类加载器时,如果须要把加载申请委派给疏导类加载器,那间接应用null代替即可

  • 扩大类加载器(Extension ClassLoader) 这个加载器负责加载<JAVA_HOME>libext 目录中的或者被java.ext.dirs零碎变量所指定的门路中的所有类库
  • 应用程序类加载器(Application ClassLoader) 这个类加载器时ClassLoader的getSystemClassLoader()办法的返回值,所以个别也称他为零碎类加载器。它负责加载用户类门路上所指定的类库

类加载器之间,依照双亲委派模型(Parents Delegation Model) 双亲委派模型要求除了顶层启动类加载器外,其余的类加载器该当都有本人的父类加载器
双亲委派模型的工作过程是:如果一个类加载器收到类加载申请,它首先会委派赴类加载器去实现,每一个档次的类加载器都是如此,因而所有的加载申请最终都会传送到顶层的启动类加载器。只有当父加载器无奈实现这个加载申请时,子加载器才会尝试本人去加载

  • 一个不言而喻的益处就是Java类随着他的类加载器一起,具备了一种带有优先级的档次关系。
7.4.3 毁坏双亲委派模型

双亲委派模型并不是一个强制性的束缚模型

8 虚拟机字节码执行引擎

  • 物理机时建设在处理器、硬件、指令集和操作系统层面上的
  • 虚拟机的执行引擎则是本人实现的,因而能够自行制订指令集与执行引擎的构造体系

在不同的虚拟机实现外面,在执行引擎执行java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即便编译器产生本地代码)

8.2 运行时栈帧构造

栈帧存储了办法的局部变量表,操作数栈、动静链接和办法返回地址等信息
在编译程序代码的时候,栈帧须要多大的局部变量表,多深的操作数栈都曾经齐全确定了,并且写入到办法表的Code属性之中,因而一个栈帧须要调配多少内存不会收到程序运行期间变量的数据的影响,而仅仅取决于具体的虚拟机实现。

对于互动线程,只有位于栈顶的栈帧才是无效的,称为以后栈帧(Current Stack Frame),与这个栈帧相关联的办法,称为以后办法(Curren Method)

8.2.1 局部变量表

是一组变量值的存储空间,用于寄存办法参数和办法外部定义的局部变量

对于64位的数据类型,虚构机会以高位对其的形式为其调配两个间断的slot空间
局部变量位于县城的堆栈上,无论slot操作是否为院子,都不会引起数据安全问题

办法执行时,虚拟机事应用局部变量表实现参数值到参数变量列表的传递过程。 如果执行的事实例办法,那局部变量表中第0位索引的slot默认事用于传递办法所属对象实例的援用,即 this

slot事能够重用的。

对象是否被回收的根本原因是局部变量表的slot中是否还有对于对象的援用。
不应用的对象应手动赋值为null

局部变量不像类变量存在于筹备阶段, 如果一个局部变量定义了,没有赋初始值 是不能应用的。

8.2.2 操作数栈

Operand Stack (LIFO)

概念模型中, 两个栈帧作为虚拟机栈的元素,是齐全互相独立的。然而在大多数虚拟机里都会做一些优化解决,令两个栈帧呈现一部分重叠,让下边的栈帧的局部操作数栈与上边的栈帧局部中和,实现数据共用

8.2.3 动静连贯

每个栈帧都蕴含一个只想运行时常量池中该栈帧所属办法的援用,

8.2.4 办法返回地址

办法对出的过程机会就等同于把以后栈帧出栈,因而退出时可能执行的操作有,复原下层办法的局部变量表和操作数栈。把返回值压入调用者栈帧的操作数栈中

8.2.5 附加信息

8.3 办法调用

办法调用阶段的文艺工作就是确定被调用办法的版本

Class文件的编译过程不蕴含传统编译过程的连贯步骤,所有办法调用在Class文件外面都是只是符号援用,而不是办法在理论运行时内存布局中的入口地址,这给了Java带来了更弱小的动静扩大能力,也是的Java办法调用更简单,须要在类加载器见,甚至到运行时能力确定指标办法的间接援用

8.3.1 解析

顶用指标在程序代码写好、编译器进行编译时就必须确定下来。 这类办法的调用称为解析(Resolution)

在java语言中合乎“编译器可知,运行期不可变”的办法 包含静态方法和公有办法两大类

只有能被invokestatic和invokespecial指令调用的办法,都能够在解析阶段中确定惟一的调用版本,合乎这个条件的有静态方法、公有办法、实例结构器、父类办法4类,他们在类加载的时候就会把符号援用解析为该办法的间接援用

解析是一个动态过程,在编译期就能够齐全确定

8.3.2 分派

Dispatch

  1. 动态分派
public class StaticDispatch{    static abstract class Human{    }    static class Man extends Human{    }    static class Woman extends Human{    }}

Human human = new Man()
Human被称为动态类型(Static Type)
Man 被称为理论类型

虚拟机在重载时时通过参数的动态类型而不是理论类型作为判据的。

所有依赖动态类型来定位办法执行版本的分派动作称为动态分派

动态分派典型的利用时办法重载。 动态分派产生在编译阶段。

变长参数重载优先级是最低的。

  1. 动静分派

重写的体现

  • invokevirtual 指令的多态查找
  • invokeVirtual指令 找到操作栈顶第一个元素所指向的对象的理论类型
  1. 单分派和多分派

办法的接受者与办法的参数统称为办法的宗量

  • 单分派是依据一个宗量对指标办法进行抉择
  • 多分派是依据多于一个宗量对指标办法进行抉择

目前java还是一个动态多分派动静单分派的语言

  1. 虚拟机动静分派的实现

建设虚办法表 应用虚办法表索引来代替元数据查找以进步性能

8.3 动静语言与动态语言

动态类型语言在编译器确定类型,最显著的益处是编译器能够提供谨严的类型查看,这样与类型相干的问题能在编码的时候就可能发现,有利于稳定性及代码达到更大规模。而动静类型语言在运行期确定类型,这能够为开发人员提供更大的领过行

8.4 基于栈字节码解释执行引擎

基于栈的指令集与基于寄存器的指令集

基于栈的指令集有点事能够治,寄存器由硬件间接提供,程序间接以来这些硬件寄存器,则不可避免的受到硬件的束缚。
栈架构指令集还有一些长处 如代码更加紧凑,编译器实现更加简略等

栈架构指令集的次要毛病是执行速度相对来说会慢,

尽管栈架构指令集的代码十分紧凑, 然而实现雷同性能须要的指令数量会比寄存器架构多。 另外栈架构在内存之中,频繁的栈拜访也就意味着频繁的内存拜访,绝对于处理器来说内存始终是执行速度的瓶颈。

第四局部程序编译与代码优化

10 晚期(编译器)优化

10.3 语法糖
10.3.1 泛型与类型擦除

泛型技术再C#和Java之中的应用形式看似雷同,然而实现却又基本的一致

  • C#的泛型无论再程序源码中、编译后的Il(Intermediate Language)或是运行期的CLR中都是切实存在的, 不同的类型再零碎运行期生成,有本人的虚办法表和类型数据。 这种实现称为类型收缩,基于这种办法实现的检查叫做实在泛型
  • Java中的泛型规定:它只在源码中存在,编译后的字节码文件中就曾经替换为原来的原升类型,并在相应的中央插入了强制类型转换。 这种办法称为伪泛型

办法重载要求办法具备不同的特色签名,返回值并不蕴含在办法的特色签名中,所以返回值不参加重载。
然而在Class文件中,只有形容不是完全一致的办法就能够共存。也就是说,两个办法如果有雷同的名称、特色签名然而返回值不同,他们也能够非法的共存于一个Class文件中

Java代码层面的办法特色签名,最重要的工作就是作为举世无双的不可反复的ID,在Java代码层面只包含了办法名称、参数程序和参数类型在字节码中的特色签名还包含办法返回值、受查异样表               ```

所谓擦除仅仅是对办法Code属性中的字节码进行擦除,实际上元数据中还保留了泛型信息,这也是咱们能通过反射伎俩获得参数化的基本根据

10.3.2 主动装箱、拆箱循环遍历
10.3.3 条件编译

Java语言的编译形式: 并非一个个的编译Java文件,而是将所有编译但愿的语法树顶级节点输出到待处理列表后进行编译,因而哥哥文件之间可能相互提供符号信息

11 早期(运行期)优化

11.2.1 解释器与编译器
  • 解释器:省区编译的额工夫,立即执行
  • 编译器: 代码翻译老本地代码,进步执行效率。

当程序运行环境中内存资源限度较大时,应用解释执行,节约内存

-Xint 强制虚拟机运行于解释模式-Xcomp 运行编译模式
11.2.2 编译对象与触发条件

运行过程中被即时编译器编译的热点代码有两类

  • 屡次被调用的办法
  • 被屡次执行的循环体

对于第一种状况, 因为是办法调用登程的编译,因而编译器会天经地义的以整个办法作为编译对象,这种编译也是虚拟机中规范的JIT编译形式
对于第二种,因为是循环体所触发的编译动作。编译器仍会以整个办法作为编译对象,这种编译形式因为编译产生在办法执行的过程之中,因而形象的称之为栈上替换(On stack Replacement)OSR,即办法栈帧还在栈上就替换了

判断热点代码的根据——热点探测

  • 基于采样的热点探测(Sample based hot spot detection)
  • 基于计数器的热点探测(Counter based hot spot detection)

Hotspot虚拟机中应用第二种,为此筹备了两类计数器:办法调用计数器、回边计数器

11.2.3 编译过程

Client Compiler 是一个简略疾速的三段式编译器, 次要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化

  • 第一个阶段,一个平台独立的前端将字节码结构成一种高级两头态代码示意(High Level Intermediate Representation, HIR),应用动态但调配(Static single Assignment),在此之前,编译器会在字节码上实现一部分根底优化,如办法内联,常量流传等
  • 第二阶段,一个平台相干的后端从HIR中产生第几中间代码示意,再次之前,会在HIR上实现一些优化,如管制查看打消,范畴查看等
  • 最初,是在平台相干的后端应用线性扫描算法,在LIR上调配寄存器……

Server Compiler是专门面向服务端的典型利用,并为服务端的性能配置特地调整过的编译器。

11.4 Java与C/C++的编译器比照

Java与C/C++的编译器实际上代表了最经典的即时编译器与动态编译器的比照

Java虚拟机的即时编译器与C/C++的动态优化编译器相比可能在下列起因导致输入的本地代码有一些劣势

  • 即时编译器运行占用用户程序的运行工夫
  • Java语言是动静的类型平安语言,这就意味着须要由虚拟机来确保程序不会违反语言语义或拜访非结构化内存。从实现层面上,虚拟机必须频繁的进行动静查看
  • java语言尽管没有virtual关键字,然而应用虚办法的频率却远大于C/C++,意味着运行时对办法接受者进行多台抉择的频率要远远大于C/C++,意味着即时编译器进行一些优化(如内联)时的难度要远远大于C/C++
  • Java时动静扩大语言,运行时加载新的类可能改变程序类型的继承关系,使得很多全局优化无奈进行。
  • java语言的对象都是堆上调配的,只有办法的局部变量能力在栈上调配。 C/C++有多种调配形式。如果能够在栈上调配线程公有对象,将加重内存回收的压力。另外C/C++因为本人进行内存治理,相比垃圾收集机制,运行效率要高。

Java的语言个性时就义了局部性能,换去了开发的效率。

第五局部 高效并发

12 Java内存模型与线程

12.2

内存模型:能够了解为在特定的操作协定下,对特定的内存或高速缓存进行读写访问的形象过程

不同架构的物理机器能够领有不一样的内存模型。

除了减少高速缓存之外,为了是的处理器外部的运算单元尽可能的被利用,处理器会对输出代码进行乱序执行优化。
与处理器的乱序执行优化蕾丝,Java虚拟机的即时编译器也有相似的指令重排序

12.3 Java内存模型

12.3.1 主存与工作内存

Java内存模型的次要指标是定义程序中各个变量的拜访规定。即虚拟机中将变量存储到内存和从内存取出变量这样的底层细节

变量包含了:实例字段、动态字段和形成数组对象的元素

所有的变量都存在主内存中。 每条线程还有本人的工作内存。线程的工作内存中保留了呗该线程应用到的变量的主内存正本拷贝。线程对变量的所有操作都必须在工作内存中进行,不能间接读写主存变量。

12.3.2 内存间交互操作

定义了如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节

  • lock,锁定:作用于主内存的变量,把一个变量表识为一条线程独占的状态
  • unlock,解锁:作用于主内存变量,把一个处于锁定状态的变量释放出来
  • read,读取:作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存
  • load,载入:作用于工作内存内的变量,把read操作从主内存失去的变量值放入工作内存的变量正本中
  • use,应用:作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎
  • assign,赋值:作用于工作内存的变量,它把一个从执行引擎接管到的值赋给工作内存的变量
  • store,存储,作用于工作内存变量,它把工作内存中的变脸该值传送到主内存中
  • write,写入:作用于主内存的变量,它把store操作从工作内存中失去的变量值放入主内存的变量中

此外,Java内存模型还规定了在执行上述8种基本操作时必须满足

  • 不容许read和load、store和write操作之一独自呈现
  • 不容许一个线程抛弃它最近assign的早错
  • 不容许一个线程无起因的把数据从工作内存同步到主内存
  • 一个新的变量只能在主内存中诞生
  • 一个变量在同一时刻只容许一条线程对其lock
  • 如果对一个变量执行lock,那会清空工作内存中此变量的值,在执行引擎应用这个变量前,须要从新执行load或者assign
  • 如果一个变量没有当时呗lock,那就不逊于执行unlock
  • 对一个变量unlock前,必须先把此变量同步回主内存
12.3.3 volatile
  • 保障此变量对所有线程时可见的
  • volatile禁止指令重排序优化
12.3.4 long/double 64位数据类型
12.3.5 原子性、可见性和有序性
  • 原子性, 内存模型间接来保障原子性变量、根本类型的拜访读写就是具备原子性的,(另外就是 long、double的非原子性协定)

java内存模型提供了lock和unlock来满足更大范畴的原子性保障,只管没有间接给用户应用,却提供了高层次的monitorenter和monitorexit来隐式应用这两个操作。这两个字节码反映到Java代码中就是synchronized关键字

  • 可见性, 指当一个线程批改了共享变量的值,其余线程可能立即得悉这个批改。

除了 volatile之外还有sunchronized和final可能保障可见性

  • 有序性,
12.3.6 Happens before

Java内存模型下一些人造的后行产生关系,这些后行产生关系无需任何同步帮助就曾经存在,如果两个操作无奈从这些关系中推导进去,他们就没有程序性保障,虚拟机就能够对他们进行任意重排序

  • 程序秩序规定(Program Order Rule):在一个线程内,依照程序代码的。(控制流程序而不是代码程序)
  • 管程锁定规定(Monitor Lock Rule):一个unlock操作后行产生于,前面对同一个所的lock操作
  • volatile变量规定:一个对volatile的鞋操作咸鱼前面对着变量的读操作
  • 线程启动规定
  • 线程终止规定
  • 线程终端规定
  • 对象中介规定
  • 传递性

12.4 Java与线程

12.4.1 线程的实现
  • 应用内核线程实现(KLT。 kernel level thread)

程序个别不会间接应用内核线程,而是去应用内核线程的一种高级接口——轻量级过程(LWP,Light weight process),轻量级进城就是咱们通常意义上所讲的线程。 轻量级过程与内核线程的比例1:1

因为是基于内核线程实现的,所以各种线程操作,如创立、析构、同步都须要进行零碎调用。而零碎调用的代价绝对较高,须要在用户态和内核台来回切换,其次,每个轻量级过程都须要一个内核线程的反对,因而轻量级过程要耗费肯定的内核资源
  • 应用用户线程实现

    狭义上的 一个线程只有不是内核线程 就能够认为是用户线程。
    广义上,使之齐全建设在用户空间上的线程。零碎内核不感知线程存在。用户线程的建设、同步、销毁和调度齐全再用户态中实现,不须要再内核的帮忙。

    因为不须要切换到内核态,因而操作能够是十分快,耗费低。然而因为没有内核意愿,所有的解决都须要用户程序本人解决。

    • 应用用户线程加轻量级线程
    • Java线程的实现

操作系统反对怎么的线程模型,在很大水平上决定了Java虚拟机的线程是怎么映射的,这点再不同的平台上没有方法达成统一。 线程模型只是对线程的并发规模和操作老本产生影响,对Java程序的编码和运行过程来说,这些差别都是通明的。

12.4.2 Java线程调度
  • 协同式线程调度
  • 抢占式线程调度
12.4.3 状态转换
  • 新建
  • 运行
  • 无限期期待
  • 限期期待
  • 阻塞
  • 完结

13 线程平安与锁优化

线程平安:当多个线程拜访一个对象时,如果不必思考这些线程再运行时环境下的调度和交替执行,也不须要进行额定的同步,或者在调用方进行任何其余协同的操作,调用这个对象的行为都能够取得正确的后果,那这个对象时线程平安的。

13.2.1 Java语言中的线程平安

1.不可变

2.相对线程平安

3.绝对线程平安

4.线程兼容

  1. 线程对抗
13.2.2线程平安的实现办法

1.互斥同步

  • synchronized/reentrantlock
相比synchronized,ReentrantLock新增的性能包含,期待可中断,可实现偏心锁,以及锁能够绑定多个条件

2.非阻塞同步
基于冲突检测的乐观并发策略

  • 乐观并发策略须要硬件指令集的倒退才能够进行
  • CAS: compareAndSwap
cas指令须要有三个操作数,别离是内存地位,旧的预期值,新值

对于CAS的ABA问题
J.U.C为了解决这个问题提供了一个带有标记的援用类,它能够通过管制变量值的版本来保障CAS的正确性。不过大部分状况下ABS问题不会影响程序并发的正确性。

3.无同步计划

可重入代码:无状态依赖
线程本地存储

13.3 锁优化

13.3.1 自旋锁与自适应自旋锁

为了让线程期待,只须要让线程执行一个忙循环,这项技术即所谓的自旋锁
如果被占用的十佳很短,自旋锁的成果就会十分好,反之,如果锁被占用的工夫很长,那么自旋的线程只会白白耗费处理器资源,而不会做任何有用的工作,反而会带来性能上的节约。

13.3.2 锁打消

如果判断一段代码,堆上的所有数据都不会逃逸进来从而被其余线程拜访到,那就能够把他们当作栈上的数据看待,认为他们是线程公有的,同步加锁能够去掉

13.3.3 锁粗化

如果探测到有一串系统的操作都对同一个对象桎梏,将会把桎梏同步的范畴扩大到整个操作序列的内部,

13.3.4 轻量级锁

轻量级锁并不是用来代替重量级锁,它的本意是在没有多线程竞争的前提下缩小传统的重量级锁应用操作系统互斥量产生的性能耗费

了解轻量级锁,必须从HotSpot虚拟机对象头的内存布局介绍。
HotSpot虚拟机的对象头分为两局部,第一局部用于存储对象本身的运行时数据,如哈希码,GC分代年龄等。另一部分用于存储只想办法区的对象类型数据指针。

13.3.5 偏差锁

偏差锁的目标是打消数据在无竞争状况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的状况下应用CAS操锁区打消同步应用的互斥量。偏差锁就是在无竞争的情况下,把整个同步都打消掉。