深入理解java虚拟机 读书笔记(全)

第二章 java内存区域&内存溢出异常

2.2运行时数据区域

深入理解java虚拟机 读书笔记(全)_第1张图片

程序计数器(线程私有)

各线程之间的计数器互不影响,生命周期与线程相同。

Java 虚拟机栈(线程私有)

 Java 方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧(Stack Frame),存储

  1. 局部变量表
  2. 操作栈
  3. 动态链接
  4. 方法出口

每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

这个区域有两种异常情况:

  1. StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
  2. OutOfMemoryError:虚拟机栈扩展到无法申请足够的内存时

本地方法栈(线程私有)

本地方法栈(Native Method Stacks)为虚拟机使用到的 Native(非java语言) 方法服务。

Java 堆(线程共享)

Java 堆(Java Heap)是 Java 虚拟机中内存最大的一块。Java 堆在虚拟机启动时创建,被所有线程共享。

作用:存放对象实例。垃圾收集器主要管理的就是 Java 堆。Java 堆在物理上可以不连续,只要逻辑上连续即可。

方法区(线程共享)

方法区(Method Area)被所有线程共享,用于存储已被虚拟机加载类信息、常量、静态变量、即时编译器编译后的代码等数据。

和 Java 堆一样,不需要连续的内存,可以选择固定的大小,更可以选择不实现垃圾收集。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。保存 Class 文件中的符号引用、翻译出来的直接引用。运行时常量池可以在运行期间将新的常量放入池中。(String)

直接内存

 

2.3 Hotspot虚拟机

 

 

第三章 垃圾收集器&内存分配策略

3.1Java 中对象访问是如何进行的?

Object obj =  new  Object();

对于上述最简单的访问,也会涉及到 Java 栈、Java 堆、方法区这三个最重要内存区域。

Object obj

如果出现在方法体中,则上述代码会反映到 Java 栈的本地变量表中,作为 reference 类型数据出现。

new  Object()

反映到 Java 堆中,形成一块存储了 Object 类型所有对象实例数据值的内存。Java堆中还包含对象类型数据的地址信息,这些类型数据存储在方法区中。

3.2对象已死吗

  1. 引用计数法
  2. 根搜索算法

引用计数法?

给对象添加一个引用计数器,每当有一个地方引用它,计数器就+1,;当引用失效时,计数器就-1;计数器都为0的对象就是不能再被使用的。

引用计数法的缺点?

很难解决对象之间的循环引用问题。

什么是根搜索算法?可达性分析算法

通过一系列的名为“GC Roots”的对象作为起始点,向下搜索,路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。

1.jpg-40.4kB

java语言中,GC Roots包括四种对象:

1 虚拟机栈(栈帧中的本地变量表)中引用的对象

2 本地方法栈中(native方法)引用的对象

3 方法区中类静态属性引用的对象

4 方法区中常量引用的对象

Java 的4种引用方式?(引用强度,可伸缩性回收)

在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为 强度依次减弱

  1. 强引用 Strong Reference  Object obj = new Object(); 不会回收
  2. 软引用 Soft Reference 内存溢出时回收
  3. 弱引用 Weak Reference  下一次垃圾回收前回收
  4. 虚引用 Phantom Reference类实现

生存还是死亡


假设在可达性分析算法中某个对象不可达,它也并非”非死不可”。如果这个对象覆盖了finalize()方法且这个方法没有被JVM调用过,则JVM会执行finalize()方法。这时你可以在这个方法中重新使某个引用指向该对象。当然,finalize()方法只能救它一次。

回收方法区 方法区的垃圾收集主要回收两部分内容:

废弃常量
回收废弃常量与回收Java堆中的对象非常相似。以常量池中的字面量的回收为例,例如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其它类、接口、方法、字段的符号引用也与此类似


无用的类

判定一个类是否是“无用的类”的条件苛刻许多。需要同时满足下面的三个条件

  1. 该类的所有实例已经被回收,也就是Java堆中不存在该类的任何实例
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的class对象已经被回收,无法在任何地方通过反射访问到该类的方法

3.3垃圾收集算法  6.27

  1. 标记-清除算法
  2. 复制算法 
  3. 标记-整理算法 
  4. 分代收集算法

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

分为标记清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。

有什么缺点?

1. 效率问题。标记和清除过程的效率都不高。 
2. 空间问题。标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致,程序分配较大对象时无法找到足够的连续内存,不得不提前出发另一次垃圾收集动作。

2.jpg-81.6kB

复制算法(Copying)- 新生代

将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。

优点?

复制算法使得每次都是针对其中的一块进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点?

将内存缩小为原来的一半。在对象存活率较高时,需要执行较多的复制操作,效率会变低。

3.jpg-95.7kB

应用?

商业的虚拟机都采用复制算法来回收新生代。因为新生代中的对象容易死亡,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的 Eden 空间两块较小的 Survivor 空间。每次使用 Eden 和其中的一块 Survivor。

当回收时,将 Eden 和 Survivor 中还存活的对象一次性拷贝到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。Hotspot 虚拟机默认 Eden 和 Survivor 的大小比例是8:1

标记-整理算法(Mark-Compact)-老年代

标记过程仍然与“标记-清除”算法一样,但不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存

分代收集算法

根据对象的存活周期,将内存划分为几块。一般是把 Java 堆分为新生代老年代,这样就可以根据各个年代的特点,采用最适当的收集算法。

  • 新生代:每次垃圾收集时会有大批对象死去,只有少量存活,所以选择复制算法,只需要少量存活对象的复制成本就可以完成收集。

  • 老年代:对象存活率高、没有额外空间对它进行分配担保,必须使用“标记-清理”或“标记-整理”算法进行回收。

3.4HotSpot算法实现

1.枚举根节点 

从可达性分析中从GC Roots节点找引用为例,可作为GC Roots的节点主要是全局性的引用与执行上下文中,如果要逐个检查引用,必然消耗时间。 
另外可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里的“一致性”的意思是指整个分析期间整个系统执行系统看起来就行被冻结在某个时间点,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性就无法得到保证。这点是导致GC进行时必须暂停所有Java执行线程的其中一个重要原因。 
由于目前主流的Java虚拟机都是准确式GC,做一档执行系统停顿下来之后,并不需要一个不漏的检查执行上下文和全局的引用位置,虚拟机应当有办法得知哪些地方存放的是对象的引用。在HotSpot的实现中,是使用一组OopMap的数据结构来达到这个目的的。 

2.安全点 

在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots的枚举,但可能导致引用关系变化的指令非常多,如果为每一条指令都生成OopMap,那将会需要大量的额外空间,这样GC的空间成本会变的很高。 
实际上,HotSpot也的确没有为每条指令生成OopMap,只是在特定的位置记录了这些信息,这些位置被称为安全点(SafePoint)。SafePoint的选定既不能太少,以致让GC等待时间太久,也不能设置的太频繁以至于增大运行时负荷。所以安全点的设置是以让程序“是否具有让程序长时间执行的特征”为标准选定的。“长时间执行”最明显的特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。 
对于SafePoint,另一个问题是如何在GC发生时让所有线程都跑到安全点在停顿下来。这里有两种方案:抢先式中断和主动式中断。抢先式中断不需要线程代码主动配合,当GC发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程来响应GC。 
而主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的另外再加上创建对象需要分配的内存的地方。 

3.安全区域 

使用安全点似乎已经完美解决了如何进入GC的问题,但实际情况却并不一定,安全点机制保证了程序执行时,在不太长的时间内就会进入到可进入的GC的安全点。但是程序如果不执行呢?所谓的程序不执行就是没有分配cpu时间,典型的例子就是线程处于sleep状态或者blocked状态,这时候线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm显然不太可能等待线程重新分配cpu时间,对于这种情况,我们使用安全区域来解决。 
安全区域是指在一段代码片段之中,你用关系不会发生变化。在这个区域的任何地方开始GC都是安全的,我们可以把安全区域看做是扩展了的安全点。 
当线程执行到安全区域中的代码时,首先标识自己已经进入了安全区,那样当在这段时间里,JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。当线程要离开安全区域时,他要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则他就必须等待,直到收到可以安全离开安全区域的信号为止。

 

3.5垃圾收集器

共七中垃圾收集器

年轻代:serial、parnew、parallel scavenge

年老代:cms、serial old、 parallel old、G1;g1用于年轻代和年老代。所有收集器都存在stop the world,不过在java发展中 一直在优化停顿时间。

比较

年轻代:

4.1.Serial:适用于年轻代垃圾回收,复制算法,jdk1.3之前 推荐用于客户端模式(Client)下的虚拟机,属于单线程,无对stop the world优化,属于最老的年轻代垃圾收集器产品;

4.2 ParNew:适用于年轻代垃圾回收,复制算法,jdk1.3发布,推荐用于服务端模式(Server)下的虚拟机,属于多线程,无对stop the world优化,其实就是Serial的多线程版本;是服务端开发的首先;

4.3 Parallel Scavenge:适用于年轻代垃圾回收,复制算法,jdk1.4发布,属于多线程,无对stop the world优化,它是一个可以控制吞吐量的收集器,拥有自适应调节策略;需要注意一点的是,gc停顿时间缩短是牺牲吞吐量和新生代空间来换取的。

stop The World:gc时 需要停止所有线程进行垃圾回收;

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提高最合适的停顿时间或最大吞吐量。

年老代

4.4 Serial old :适用于年老代垃圾回收,标记-整理算法,jdk1.5之前,主要用于client下的虚拟机,如果用在server模式下主要有俩个作用:一是jdk1.5以及之前用于与Parallel Scavenger搭配使用,二就是为cms提供后备预案,属于单线程,无stop the world优化。

4.5 Parallel Old:适用于年老代垃圾回收,标记-整理算法,jdk1.6发布,属于多线程,注重于吞吐量控制,是为了Parallel Scavenge定制的;

4.6 CMS: 适用于年老代垃圾回收,标记-清除算法,jdk1.5发布,推荐server模式下,属于多线程,对stop the world有优化,CMS是一种以获取最短回收停顿时间为目标的收集器,也就是优化服务器响应速度。CMS分为4步:其中 初始化标记,重新标记 stop the world

4.6.1 初始标记:仅仅只是标记GcRoots可以直接关联的对象;

4.6.2 并发标记:进行gcroots tracing(也就是 引用链搜索);优化成并发

4.6.3 重新标记:修正并发标记因用户程序继续运行而导致标记产生变动那一部分对象的标记记录。

4.6.4 并发清除:并发清理标记的对象内存。

4.6.5 CMS缺点:

4.6.5.1 CMS对cpu资源非常敏感,默认启动的回收线程数是(cpu数量+3)/4;垃圾回收线程数量不少于25%的cpu资源,当cpu越大,线程数/cpu总量 越小;但是当cpu小于4个时 显得就不怎么合适了;

4.6.5.2 无法处理浮动垃圾,需要等下一次gc才能处理;并且还需要预留一部分空间提供并发收集时的用户程序运作使用,即启动阈值(-XX:CMSInitiatingOccupancyFraction 阈值百分比);要是CMS运行期间预留的空间不满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这是虚拟机启动后备预案:临时启用Serial Old,这样一来停顿时间就很长了,所以-XX:CMSInitiatingOccupancyFraction 设置太高容易导致大量“Concurrent Mode Failure”失败,性能反而降低

4.6.5.3 由于用的是标记-清除算法,所以会出现内存碎片;CMS提供-XX:UseCMSCompactAtFullCollection开关参数(默认为开),用于CMS收集器要进行FullGC时进行内存碎片合并整理,-XX:CMSFullGCsBeforeCompaction 用来设置执行多少次不压缩Full GC后跟着来一次带压缩的(默认为0,表示每次进入Full GC都进行碎片压缩)。

年轻代+年老代

4.7 G1:jdk1.7发布,整体看是“标记-整理算法”,从局部(俩个Region之间)上来看是基金“复制”算法实现,也就是说没有内存碎片;

如果应用追求低停顿,那G1现在可以作为一个尝试的选择,如果应该追求吞吐量,G1并不会带来什么特别的好处!!!

 

3.6 内存分配与回收策略

1 对象优先在Eden分配

major gc时经常会伴随至少一次的minor gc,但并非绝对,比如说 Parallel Scavenge就是直接major gc没有伴随minor gc;

2 大对象直接进入老年代

-XX:PretenureSizeThreshold设置最大对象,单位B;PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效;Parallel Scavenge收集器不需要设置;如果遇到必须使用此参数的场景,可以考虑ParNew+CMS组合;

3 长期存活的对象放入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器;gc一次Age+1,当Age大于一定程度(默认为15岁)就会被晋升到老年代;-XX:MaxTenuringThreshold:最大年龄;

4 动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于改年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄;

5空间分配担保

jdk6 update 24后,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行major gc,否则Full GC;

 

第四章 监控工具

 

第五章 调优案例 

 

第六章 类文件结构

6.2 无关性的基石

深入理解java虚拟机 读书笔记(全)_第2张图片

6.3Class类文件的结构

  1. 任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。
  2. Class文件是一组8位字节为基础单位的二进制流,类似结构体的伪结构,它只包含两种类型:无符号数和表
  3. 无符号数u1,u2,u3,u4表示1,2,3,4个字节,可描述数字、索引引用、数量值或者字符串值。
  4. 表:描述有层级关系的复合数据

魔数与Class文件的版本

魔数:每个Class文件的头4个字节,确定该Class文件是否能够被虚拟机接受

接下来是Class文件的版本号:第5,6字节是次版本号(Minor Version),第7,8字节是主版本号(Major Version)。

前四个字节为魔数,次版本号是0x0000,主版本号是0x0033,说明本文件是可以被1.7及以上版本的虚拟机执行的文件

常量池

常量池是Class文件之中的资源仓库,与其他项目关联最多的数据类型;最大的数据项目之一;Class文件中第一个出现的表类型数据项目

开头:两个字节u2是常量池数量(00 16 :16项),1为开始,0表示不需要引用。主要存放两个类常量:字面量(文本字符串、声明为final的常量值等)和符号引用。符号引用则属于编译方面的概念,包括:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符(07 00 02 :类型标识+看类型的结构定义

常量池的每一项常量都是一个表,常量之间可以互相引用,也就是常量表之间可以关联。

每个表开始的第一位是一个u1类型的标志位,表示是14张常量表中的哪一个

深入理解java虚拟机 读书笔记(全)_第3张图片

访问标志 7.1

在常量池之后,紧接着的两个字节代表访问标志,识别类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否为abstract类型;是否为final等

4个字节魔数->4个字节版本号->连续出现的常量表->类访问标志

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

确定继承关系。类索引和父类索引都是一个u2类型的数据,而接口是一组u2类型的数据的集合                                          

4个字节魔数->4个字节版本号->连续出现的常量表->类访问标志->类索引->父类索引->接口索引

字段表集合

描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量

字段表由access_flags、name_index、descriptor_index、attributes_count、attributes组成

字段表不会列出从超类或者父接口中继承而来的字段,编译器可能会自定添加字段,比如在内部类中为了保持对外部类的访问性,最自动添加指向外部类的实例的字段

4个字节魔数->4个字节版本号->连续出现的常量表->类访问标志->类索引->父类索引->接口索引->字段表集合

 

6.4-6.6 暂略

 

第七章 虚拟机类加载机制-运行期加载

7.2 类加载的时机

类的生命周期:加载-{(连接)验证-准备-解析}-初始化-使用-卸载

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:

虚拟机规范规定有且只有5种情况必须立即对类进行初始化

1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时;最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候

2.使用java.lang.reflect包的方法对类进行反射调用;

3.如果父类没初始化,则需要先触发其父类的初始化

4.虚拟机启动时候,指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
上面的5种成为对类的一个主动引用

除此之外,所有引用类的方式都不会触发初始化,也称为被动引用: 

  1. 通过子类引用父类的静态字段,不会导致子类初始化
  2. 通过数组定义来引用类,不会触发此类的初始化,数组在虚拟机中其实是虚拟机自己创造的一个类
  3. 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

7.3类加载的过程

加载

加载需要完成三件事:

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

对于数组类,由java虚拟机直接创建,它的加载过程:

  1. 如果数组的组件类型是引用类型,那就递归采用上面提到的加载过程加载这个组件类型
  2. 如果不是引用类型,会把数组标记为与引导类加载器关联
  3. 数组类的可见性与它的组件类型的可见性一致

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式存储在方法区中,方法区中的数据存储格式虚拟机自行定义,然后在内存中实例化一个Class类的对象

验证

验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求
验证包括:文件格式验证、元数据验证、字节码验证、符号引用验证

1.文件格式验证

第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这个阶段的验证时基于二进制字节流进行的

1.是否以魔数oxCAFEBABE开头

2.主、次版本号是否在当前虚拟机处理范围之内

3.常量池的常量中是否有不被支持的常量类型(检查常量tag标志)

4.指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量

5.CONSTANT_Itf8_info 型的常量中是否有不符合UTF8编码的数据

6.Class文件中各个部分及文件本身是否有被删除的或附加的其他信息

2.元数据验证。对类元数据信息进行语义校验

1.这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)

2.这个类的父类是否继承了不允许被继承的类(被final修饰的类)

3.如果这个类不是抽象类,是否实现类其父类或接口之中要求实现的所有方法4.类中的字段、方法是否与父类产生矛盾(列如覆盖类父类的final字段,或者出现不符合规则的方法重载,列如方法参数都一致,但返回值类型却不同等)

3.字节码验证

第三阶段是整个验证过程中最复杂的一个阶段,主要目的似乎通过数据流和控制流分析,确定程序语言是合法的、符合逻辑

1.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,列如,列如在操作数栈放置类一个int类型的数据,使用时却按long类型来加载入本地变量表中

2.保证跳转指令不会跳转到方法体以外的字节码指令上

3.保证方法体中的类型转换时有效的,列如可以把一个子类对象赋值给父类数据类型,这个是安全的,但是吧父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的

4.符号引用验证

发生在虚拟机将符号引用转化为直接引用的时候

1.符号引用中通过字符串描述的全限定名是否能找到相对应的类

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

3.符号引用中的类、字段、方法的访问性是否可被当前类访问

 如果全部代码都已经被反复使用和验证过,那么在实施阶段就可以考虑使用Xverify:none参数来关闭大部分的类验证措施

准备

正式为类变量分配内存并设置变量初始值,内存都将在方法区中进行分配,分配的是类变量不是实例变量,是包括类变量(static修饰)。实例变量会分配在java堆中,另外这里所说的初始值是指对应类型的零值

如果类字段的字段属性表中存在ConstantValue属性(如final),那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值

解析

  1. 解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程
  2. 符号引用是以一组符号来描述所引用的目标,直接引用则是直接指向目标的指针
  3. 解析包括:类或接口的解析,字段 解析,类方法解析,接口方法解析

初始化

  1. 初始化是加载过程的最后一步,初始化是执行clinit方法的过程,ciinit方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。静态语句块中只能访问到定义在静态语句块之前的变量
  2. 虚拟机保证在子类的clinit方法执行之前,父类的clinit方法已经执行完毕,因此父类中定义的静态语句块要优先于子类的变量赋值操作
  3. clinit方法不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不生成该方法
  4. 虚拟机会保证一个类的clinit方法在多线程环境中被正确的加锁、同步

类加载器

“通过一个类的全限定名称来获取描述此类的二进制字节流”

类与类加载器

一个类是由它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性。两个相同限定名的类,经过不同的类加载器加载也是代表两个不同的类,而且Class的equals,isAssignableFrom,isInstance方法返回的结果也会不一致

双亲委派模型

从虚拟机角度有两种不同的类加载器:一种是启动类加载器(Bootstrap Classloader),是C++实现;另一个部分就是所有其他的类加载器,是由Java实现的,且用户可以自定义,继承ClassLoader

从开发人员的角度可以分为三种类加载器:

深入理解java虚拟机 读书笔记(全)_第4张图片

  1. 启动类加载器:负责加载JAVA_HOME/lib目录中的被虚拟机识别的类,无法被Java直接引用,用户在编写自定义的类加载器的时候,如果需要把类加载请求委托给引导类加载器,直接给加载器赋值为null就行
  2. 扩展列加载器:负责加载JAVA_HOME/lib/ext目录中的类,可以直接使用
  3. 应用程序加载器:由AppClassLoader实现,一般称为系统类加载器,负载加用户的ClassPath上说指定的类,开发者可以直接使用这个类加载器,也是默认使用的类加载器

优先级:启动类加载器->扩展类加载器->应用程序类加载器->自定义类加载器

双亲委派模型:要求除了顶层启动类加载器外,其余的类加载器都应当有自己的父类加载器

过程: 如果一个类加载器收到了类加载的请求,它首先不会尝试加载这个类,而是把请求往上传递,只有当父加载器反馈无法加载的时候,子加载器才会尝试加载

破坏双亲委派模型:

第一次: 由于JDK1.2之后才引入的双亲委派模式,因此为了前向兼容,允许用户自定义loadClass的代码,从而可以使用自定的加载类加载代码。JDK1.2之后,建议通过findClass来定义自己的类加载器

第二次:JNDI,JDBC等需要调用独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI)的代码,因此需要委托子加载器加载代码,可以通过Thread类的setContextClassLoader()来设置加载器,默认是应用程序类加载器

第三次:像OSGi的热代码替换技术重新构建了自己的类加载逻辑,没有采用双亲委派模式,而是引入了Bundle的概念,Bundle类似于模块的概念,当更换一个Bundle的时候,就把Bundle连通类加载器一起更换

第12章 Java内存模型与线程

Java内存模型

Java试图定义一种内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各个平台 下都能达到一致的内存访问效果

主内存和工作内存

Java内存模型规定了所有的变量(线程共享的变量)都存在主内存中,每个线程还有自己的工作内存。线程的工作内存中保存了该线程使用到的变量的主内存的拷贝,线程对变量的所有操作都是在工作内存中进行的,不同的线程之间不会互访工作内存。工作内存和主内存通过save和load指令交互
主内存实际对应着java堆中的数据,而工作内存实际对应着虚拟机栈中的部分区域
内存交互操作
Java内存模型定义了8中操作,虚拟机必须保证每一种操作都是原子的:

lock:作用于主内存

unlock:作用于主内存

read:从主内存读

load:把read的数据放入工作内存

use:作用于工作内存,用于向执行引擎传递数据

assign:作用于工作内存,从执行引擎接收数据赋值给变量

store:作用于工作内存,把变量的值传送到主内存中

write:作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存变量中

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

不允许read和load、store和write操作之一单独出现

不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。

不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。

一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现

如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值

如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。

对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

Java针对这8中操作定义了一些基本的规则,这样基本规则实际等效于happen-before原则

volatile的变量的特殊规则

volatile保证了变量对所有线程的可见性
但是保证可见性并不代表是线程安全的,在不符合下面两条规则的场景仍需要加锁: 

运算结果并不依赖变量的当前值,或者能够确保只有单一线程修改变量的值
变量不需要与其他的状态变量共同参与不变约束

volatile变量的第二个语义是禁止指令重排序优化,它是通过以下方式实现的: 

会在操作指令中加入一条空操作,这条空操作带有lock指令,使其后面的指令不能重排到前面去,lock会使CPU的cache写入内存,因此会使其他的cache无效化,通过这样一个空操作,可让前面的volatile变量的修改对其他CPU立即可见

该操作把修改同步到主内存,意味着所有之前的操作都已经执行完毕,这样便形成了”指令重排序无法越过内存屏障“的效果
double和long的操作可以是非原子的,尽管目前大部分虚拟机都是实现为原子的

Java内存模型中对volatile变量定义的特殊规则。假定T表示一个线程,V和W分别表示两个volatile变量,那么在进行read、load、use、assign、store、write操作时需要满足如下的规则:

1.只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load操作。线程T对变量V的use操作可以认为是与线程T对变量V的load和read操作相关联的,必须一起连续出现。这条规则要求在工作内存中,每次使用变量V之前都必须先从主内存刷新最新值,用于保证能看到其它线程对变量V所作的修改后的值。

2.只有当线程T对变量V执行的前一个动是assign的时候,线程T才能对变量V执行store操作;并且,只有当线程T对变量V执行的后一个动作是store操作的时候,线程T才能对变量V执行assign操作。线程T对变量V的assign操作可以认为是与线程T对变量V的store和write操作相关联的,必须一起连续出现。这一条规则要求在工作内存中,每次修改V后都必须立即同步回主内存中,用于保证其它线程可以看到自己对变量V的修改。

3.假定操作A是线程T对变量V实施的use或assign动作,假定操作F是操作A相关联的load或store操作,假定操作P是与操作F相应的对变量V的read或write操作;类型地,假定动作B是线程T对变量W实施的use或assign动作,假定操作G是操作B相关联的load或store操作,假定操作Q是与操作G相应的对变量V的read或write操作。如果A先于B,那么P先于Q。这条规则要求valitile修改的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。

 对于long和double型变量的特殊规则

  1. double和long的操作可以是非原子的,尽管目前大部分虚拟机都是实现为原子的

原子性、可见性、有序性

Java内存模型是围绕着并发过程中如何处理原子性、可见性和有序性的3个特征来建立的

原子性:底层靠的是lock和unlock指令,更高层次发展为monitorenter和monitorexit,然后是synchronized关键字

可见性:volatile关键字,synchronized和final关键字也支持

有序性:如果本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有的操作都是无序的

线性并发原则

  1. 也就是happens-before原则,它是判断数据是否竞争、线程是否安全的主要依据。它定义了Java内存模型中定义的两项操作之间的偏序关系,这是无需任何同步手段就能成立的
  2. 先行发生和时间上的先后没有关系

Java与线程

线程的实现

实现线程主要有3中方式:使用内核实现、使用有用户线程实现和使用用户线程加轻量级机进程混合实现

使用内核:有内核来完成线程切换和线程调度,程序一般不会直接去使用内核线程,而是去使用内核线程的高级接口——轻量级进程(即LWP 线程)

使用用户线程:效率很低

混合实现

Java线程调度

系统调度主要有两种方式:协同式线程调度(工作执行完切换)和抢占式线程调度(固定时间切换),Java采用后者

状态转换

Java定义了5中线程状态

阻塞和等待区别:阻塞是等待锁释放;等待是等待时间到

1.新建

2.运行:可能正在执行。可能正在等待CPU为它分配执行时间

3.无限期等待:不会被分配CUP执行时间,它们要等待被其他线程显式唤醒

4.限期等待:不会被分配CUP执行时间,它们无须等待被其他线程显式唤醒,一定时间会由系统自动唤醒

5.阻塞:阻塞状态在等待这获取到一个排他锁,这个时间将在另一个线程放弃这个锁的时候发生;等待状态就是在等待一段时间,或者唤醒动作的发生

6.结束:已终止线程的线程状态,线程已经结束执行

第13章 线程安全与锁优化

线程安全

Java语言中的线程安全

volatile不具备原子性

操作共享数据分为5类:

不可变:一定是线程安全的。数据不可变(final String Long Double 数值包装类型)
绝对线程安全:不需要任何同步就能达到线程安全
相对线程安全:需要额外的保障(Vector 方法被synchronized修饰。但还是需要同步修饰。 HashTable Collections)
线程兼容:不安全但使用得当也没问题
线程对立:肯定会死锁,无法同步(Thread的suspend和resume方法)


线程安全的实现方法:

  • 互斥同步(阻塞同步,有阻塞唤醒问题 悲观,总想加锁):加锁

Synchronized,ReentrantLock增加了一些高级功能

1.等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助

2.公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;非公平锁则不能保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。Synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁

3.锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition方法即可

  • 非阻塞同步:CAS

不用同步关键字,使用Atimic里面的包装类。记录了旧值和新值。但可能产生ABA问题,这时互斥同步更好。

  • 无同步方案:1、可重入的代码都是线程安全的,即执行时可中断去执行其他事情。判断:输入相同,结果就相同。2、线程本地存储:促进共享数据在同一线程执行。

锁优化

自旋锁

互斥同步有阻塞问题,但一般锁只会持续很短时间。数据被锁是,让线程自旋(循环)等待锁释放,对于锁时间短的数据可用。可设置自旋次数。

自适应锁

自旋锁自适应自旋次数。

锁消除

虚拟机检测无数据共享问题,进行锁消除

锁粗化

对同一个对象反复加锁解锁,虚拟机会扩大同步的代码块范围

轻量级锁

在无竞争的情况,用CAS消除同步使用的互斥量。可转为重量级(一般的锁)

偏向锁

在无竞争的情况,把整个同步都消除掉,偏向锁线程将将永远不需同步。可转化

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Java,Java,编程,虚拟机)