读书笔记-深入了解JAVA虚拟机

本篇博文将会记录次书中的片段和理解,仅作读书笔记使用。

第一部分:走进JAVA

    1.2 JAVA技术体系

    概念:①JDK(Java Development Kit):包括JAVA程序设计语言、各种硬件平台上的JAVA虚拟机、JAVA API类库。JDK是用于支持Java程序开发的最小环境。

              ②JRE(Java RunTime Enviorment):包括JAVA API 类库中JAVA SE的子集 类库 ,JAVA虚拟机。JRE是支持Java程序运行的标准环境。

 

    2.2运行时数据区域:Java虚拟机所管理的内存将包括以下几个运行时数据区域,方法区、虚拟机栈、本地方法栈、堆、程序计数器。

    程序计数器:一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中,字节码解释器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令。此类内存为“线程私有”的内存。(一个线程一个)

    虚拟机栈:线程私有的,生命周期与线程相同。每个方法被执行时会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。通俗的java内存划分方式为“堆”和栈,栈即指虚拟机栈,或者是虚拟机栈中的局部变量表部分。局部变量表所需的内存空间是在编译期间完成分配的。

    本地方法栈:与虚拟机栈的区别为,虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Naive方法服务。有的虚拟机(例如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

    Java堆(GC堆):被所有线程共享,在虚拟机启动时创建,唯一目的是存放对象实例。分为新生代和老年代。它可以处于物理不连续的内存空间,只要逻辑上连续即可。

    方法区:线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。别名Non-Heap(非堆),与Java堆区分。垃圾收集行为在该区域较少出现,该区域内存回收目标主要是针对常量池的回收和对类型的卸载。

    运行时常量池:方法区的一部分,存放在Class文件中,其用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池。一般来说,翻译出来的直接引用也会存储于此。它区别于Class文件常量池的重要特点是具有动态性,运行期间也可能将新的常量放入池中,例如String类的intern()方法。

    直接内存:并不是虚拟机运行时数据区的一部分,也不是Java虚拟机中规范定义的内存区域。

 

    2.3 对象访问

    Object obj = new Object();

    此句代码中,Object obj会反映到Java栈的本地变量表,作为一个reference类型数据出现。new Object()这部分的语句会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存。

    访问方式:主流的访问方式主要有两种,使用句柄和直接指针。

        使用句柄:Java堆中将会划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自具体信息。

        使用直接指针:Java堆对象的布局中就必须考虑如何访问类型数据的相关信息,reference中直接存储的就是对象地址。

        各自的好处:使用句柄访问的优势为reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改;使用直接指针访问的好处是速度更快,节省了指针定位的时间开销。

     

    2.4.1 Java堆异常

        分析步骤:1.判断为内存泄漏还是内存溢出(泄漏即上次用完的资源尚未归还,溢出为越界

                         2.如果是内存泄漏,可进一步通过泄漏对象到GC Roots的引用链,定位泄漏代码的地址;如果是溢出,则应当检查虚拟机的堆参数(-Xmx与-Xms),从代码上检查是否存在某些对象生命周期过长,持有状态时间过常常的情况,尝试减少程序运行期的内存消耗。

    2.4.2 虚拟机栈和本地方法栈溢出

        两种异常:StackOverflowError、OutOfMemoryError

        StackOverflowError:线程请求的栈深度大于虚拟机所允许的最大深度。

        OutOfMemoryError:虚拟机在扩展栈时无法申请到足够的内存空间。

    2.4.3 运行时常量池溢出 (OutOfMemoryError)

        向常量池中添加内容最简单方法是使用String.intern()这个Native方法。

    2.4.4 方法区溢出

        方法区用于存放Class的相关信息,方法区溢出是一种常见的内存溢出异常,在经常动态生成大量Class的应用中,需要特别注意类的回收状况。场景:使用GCLib字节码增强、大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的类加载器加载也会视为不同的类)。

    2.4.5 本机直接内存溢出

        虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。

 

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

    (此章节内容讨论在本博客博文"掘金阅读记录"也有记载)

    3.2.1 引用计数算法

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

        优缺点:优点有实现简单,判定效率高;缺点在于,很难解决对象之间的相互循环引用问题。

(未完待续)

    3.2.2 根搜索算法

        定义:通过一系列的名为“GC Roots”的对象作为起始点,从这些几点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(不可达)时,则证明此对象是不可用的。

        可作为GCRoots的对象:① 虚拟机栈(栈帧中的本地变量表)中的引用的对象。 ② 方法区中的常量引用的对象。③ 本地方法栈中JNI(即一般说的 Native 方法)的引用的对象。

    3.2.3 再谈引用

        定义:①JDK1.2以前:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。②JDK1.2以后:进行扩充,分为强、软、弱、虚四种引用。

        (这四种引用同样在掘金阅读记录中有讲到,所以就不详细说明区别了。)

    3.2.4 生存还是死亡?

        判别死亡:在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程。

    3.2.5 回收方法区

        永久代的垃圾回收主要内容:①废弃常量 ②无用的类

      判定一个类为“无用的类”:①该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例。②加载该类的ClassLoader已经被回收。③该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    3.3 垃圾收集算法(同样在掘金的阅读笔记有,这里不详写)

        算法有:①标记-清除算法 ②复制算法 ③标记-整理算法 ④分代收集算法

    3.4 垃圾收集器

        Serial收集器:单线程收集器,曾经是虚拟机新生代收集的唯一选择。优点:简单而高效(与其他收集器的单线程相比)

        ParNew收集器:Serial收集器的多线程版本,它是许多运行在Sever模式下的虚拟机中首选的新生代收集器。

        Parallel Scanvenge 收集器(吞吐优先收集器):新生代,使用复制算法,并行的多线程收集器。特点:它的关注点与其他收集器不同,它的目标是达到一个可控制的吞吐量。高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运行任务,主要适合在后台运算而不需要太多交互的任务。

        Serial Old 收集器:Serial收集器的老年代版本,单线程,使用“标记-整理”算法。主要意义是被Client模式下的虚拟机使用。

        Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。

        CMS 收集器:一种以获取最短回收停顿时间为目标的收集器,重视服务的响应速度,基于“标记-清除”算法实现。

        收集过程分为四步:①初始标记 ②并发标记 ③重新标记 ④并发清除 。其中①③需要“Stop the World”(停掉其他一切线程),初始标志仅仅标记一下GC Roots 能直接关联到的对象,速度很快;并发标记阶段是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。

        优点:并发收集、低停顿

        缺点:①对CPU资源非常敏感 ②无法处理浮动垃圾(Floating Garbage)③收集结束时产生大量空间碎片

        G1收集器:垃圾收集器理论进一步发展的产物,与前面的CMS相比有两个显著的改进。① G1收集器基于“标记-整理”算法实现的收集器,不会产生空间碎片。 ②它可以非常精确地控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒,这几乎是实时Java(RTSJ)的垃圾收集器的特征了。

        特点:G1将整个Java堆划分为多个大小固定的独立区域,并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先队列,每次根据允许的收集时间,优先回收垃圾最多的区域。(Garbage First)

    3.5 内存分配与回收策略

        3.5.1 对象优先在Eden分配

        注意:新生代GC(Minor GC):回收频繁,一般速度也比较快。 老年代GC(Major GC/ Full GC):出现了Major GC,进场会伴随至少一次的 Minor GC,一般它的速度必Minor GC 慢 10 倍以上。

        3.5.2 大对象直接进入老年代

        大对象:需要大连连续内存空间的 Java 对象。虚拟机提供了一个参数,令大于这个设置值的对象直接在老年代中分配。目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝。

        3.5.3  长期存活的对象将进入老年代

        为了确认对象要放在哪里,虚拟机给每个对象定义了一个对象年龄计数器。当它的年龄增加到一定程度(默认为15),就会到老年代去。

        3.5.4 动态对象年龄判断

        为了能更好的适应不同程序的内存情况,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到要求的年龄。

        3.5.5 空间分配担保

        当出现大量对象在Minor GC 后仍然存活的情况时,就需要老年代进行分配担保。担保的前提是老年代本身还有剩余空间,而在实际内存回收之前是无法明确需要多少空间的,所以取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值与老年代的剩余空间进行比较,来决定是否要Full GC 腾出空间。如果出现担保失败,只好在失败后重新发起一次Full GC。

第四章 虚拟机性能监控与故障处理工具

    4.2 JDK的命令行工具

    实现语言:Java。好处:当应用程序部署到生产环境后,无论是直接接触物理服务器还是远程Telnet到服务器上都可能受到限制。借助tools.jar类库(不属于Java的标准API)里面的接口,我们可以直接在应用程序中实现功能强大的监控分析功能。

    工具:jps,jstat,jinfo,jmap,jhat,jstack

    jps(JVM Process Status):虚拟机进程工具

    作用:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类对的名称,以及这些进程的本地虚拟机的唯一ID(LVMID)。对于本地虚拟机进程来说,LVMID与操作系统的进程ID(PID,Process Identifier)是一致的;如果同时启动多个虚拟机线程,就要使用jps命令来显示主类的功能才能区分。

    jstate(JVM Statistics Monitoring Tool):虚拟机统计信息监视工具(这个工具在掘金阅读记录中有写)

    作用:监视虚拟机各种运行状态信息。可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据在没有GUI图形界面的服务器上,它是运行期定位虚拟机性能问题的首选工具。

    jinfo(Configuration Info for Java):Java配置信息工具

    作用:实时地查看和调整虚拟机的各项参数。

    jmap(Memory Map for Java):内存映像工具

    作用:生成堆转储快照(一般称为heapdump或dump文件),还可以查询finalize执行队列,Java堆和永久代的详细信息,如空间使用率、当前使用的是哪种收集器等。(与jinfo一样,很多功能在windows平台下受限)

    jhat(JVM Heap Analysis Tool):虚拟机堆转储快照分析工具

    作用:分析jmap生成的堆转储快照,与jmap搭配使用。(实际一般不用它)

    jstack(Stack Trace for Java):Java 堆栈跟踪工具(该工具在掘金阅读记录中有描述)

    作用:生成虚拟机当前时刻的线程快照(一般称为threaddump或javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成它的主要目的是定位线程出现长时间停顿的原因。(如线程死锁、死循环等等。)

    4.3 JDK的可视化工具

    JConsole:Java监视与管理控制台(基于JMX)

    使用:1.启动:位于JDK/bin目录下的jconsole.exe

               2.内存监控:用于监视受收集器管理的虚拟机内存(Java堆 和永久代   )

的变化趋势。

                3.线程监控:监控线程的状态(是否发生死锁等)

    VisualVM:目前最强大的工具

 

第五章 调优案例分析与实战

    5.2 案例分析

    5.2.1 高性能硬件上的程序部署策略

    1.通过64位JDK来使用大内存 2.使用若干个32位虚拟机建立逻辑集群来利用硬件资源

    方法1的问题:内存回收导致的长时间停顿;现阶段的64位JDK的性能测试结果普遍低于32位;需保证程序足够稳定,因为如果产生溢出几乎无法产生快照;相同的程序在64位中消耗的内存一般比32位大。

    方法2的问题:尽量避免节点竞争全局的资源;很难高效率地利用某些资源池;各个节点仍然受到32位的内存限制;大量使用本地内存。

     5.2.2 集群间同步导致的内存溢出:

     节点间的网络交互频繁,导致重发数据在内存中不断堆积产生内存溢出。

     5.2.3 堆外内存导致的溢出错误:

     垃圾收集进行时,虚拟机虽然会对Direct Memory 进行回收,但是Direct Memory不饿能像新生代和老年代,发现空间不足就通知收集器进行垃圾回收, 它只能等待老年代满了后Full GC,然后“顺便”帮它清理掉内存的废弃对象。否则,它只能等到抛出内存溢出异常时在catch块中System.gc()。

     5.2.4 外部命令导致系统缓慢:

      改用Java的API调用即可

     5.2.5 服务器 JVM 进程崩溃:

     可以想象一下某个程序需要与学校教务系统对接,但是教务系统老是崩,导致堆积的线程和Socket越来越多,最终导致超过虚拟机的承受能力后使得虚拟机进程崩溃。

     5.3 实战:Eclipse运行速度调优

     1. 升级 JDK 版本

     2.编译时间和类加载时间的优化:

     编译时间:指虚拟机的JIT(Just In Time)编译器编译热点代码(被调用次数达到一定程度的代码)的耗时。

    1)通过参数-Xverify:none禁止掉类加载时的字节码验证过程。

    2)通过使用力度更强的编译器。(-client  C1编译器---> -server  C2编译器)

    5.3.4 调整内存设置控制垃圾收集频率(最重要)

    1)用 -Xmn 扩展新生代容量    2)把 -Xms和 -XX:PermSize参数值恩别设置为 -Xmx 和 XX:PermSizeMax参数值,强制虚拟机在启动的时候就把老年代和永久代的容量固定下来,避免运行时自动扩展。

    5.3.5 选择收集器降低延迟

    案例中选择了新生代用ParNew 和 老年代用CMS收集器

    

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

    第六章 类文件结构 

    6.3  Class 类文件的结构

    Class文件:一组以8位字节为基础单位的二进制流,中间不添加任何分隔符。

    文件格式:一种类似于C语言结构体的伪结构,其中只有两种数据类型:无符号数和表。

    无符号数:属于基本的数据类型,以u1,u2,u4,u8来分别表示1、2、4、8个字节的无符号数,可用于描述数字、索引引用、数量值或按照UTF-9编码构成字符串值。

    表:由多个无符号数或其他表作为数据项构成的复合数据类型。

    6.3.1 魔数与Class文件的版本

    魔数:每个Class文件的头4个字节,唯一作用是用于确定该文件是否为一个能被虚拟机接收的Class文件。 许多文件存储标准中都使用魔数来进行身份识别。(例如图片格式,gif,jpeg),Java的Class文件魔数是 0xCAFEBABE(咖啡宝贝)

    版本号:紧接着魔数的4个字节,第五第六个字节是次版本号,第7,8个字节是主版本号。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生改变。

    6.3.2 常量池

    定义:紧接着主次版本号之后的是常量池入口。常量池是Class文件结构中与其他项目关联最多的数据类型,占用空间最大的数据项之一,Class文件中第一个出现的表类型数据项目。

    入口:在入口处放置一项u2类型的数据,代表常量池计数值。(从1开始),0作为“不引用任何一个常量池项目”的意思。除常量池的计数外,其他的容量计数都是从0开始。

    内容:两大类常量,字面量和符号引用。字面量:文本字符串、声明为final的常量值;符号引用:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符。

    特点:共有11种结构的表结构数据,表开始的第一位是一个u1类型的标志位,代表该常量的常量类型。

    注意:CONSTANT_Utf8_info型的最大长度是Java方法中和字段名的最大长度,为65535,如果Java程序中定义了超过了64KB英文字符的变量或方法名,将会无法编译。

     6.3.3  访问标志

     定义:用于识别一些类或接口层次的访问信息,为紧接着常量池结束后的2个字节。共有32个标志位可使用,当前只定义了其中的8个。

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

     定义:类和父类列索引都是一个u2类型的数据,接口索引是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类的继承关系。

     6.3.5 字段表集合

     定义:用于描述接口或类中声明的变量。字段包括了类级和实例级变量,但不包括在方法内部声明的变量。

     三种特殊字段:简单名称、描述符、全限定名。

     全限定名:例如 org/fensx/clazz/TestClass,即把类的全名中的.换成/,结束时用;表示。

     简单名称:没有类型和参数修饰的方法或字段名称。例如 inc() 的简单名称为inc。

     描述符:用于描述字段的数据类型、方法的参数列表和返回值。

     注意:字段表中不会列出从超类或父接口中继承而来的字段,但可能会列出原本Java代码中不存在的字段。(譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类的实例的字段。);且对字节码来说,如果两个字段的描述符不一致,那么字段重名就合法的。

     6.3.6 方法表集合

     定义:对方法的描述与对字段的描述几乎采用了完全一致的方式。

     6.3.7 属性表集合 

     在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述这些场景专有的信息。

      描述:无严格顺序要求,任何人实现的编译器都可以向属性表中写入自己的定义的属性信息,只要不和已有属性重合。

      1.Code属性(并非所有方法表都有该属性)

      包含内容如下图 

    max_locals:代表局部变量表所需的存储空间。单位为Slot。byte、char等长度<32位的需要1个slot,double等64位的需要两个Slot。且局部变量表中的Slot可以重用。

    code_length:理论上可达到2的32次方-1,但是虚拟机规范中限制了一个方法中不允许超65535条字节码指令,超过的话会拒绝编译。在编译复杂的JSP文件时,可能会因为这个而编译失败。

    this访问的实现:通过Javac编译器在编译的时候把this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数。因此在实例方法的局部变量表至少有一个指向当前对象实例的局部变量。

    2.Exceptions属性

    作用:列出方法中可能抛出的受查异常。也就是方法描述时在throws关键字后面列举的异常

    3.LineNumberTable属性

    作用:用于描述Java源码行号和字节码行号之间的对应关系。最主要的影响是,若不生成,在抛异常时,堆栈中不会显示出从的行号,并且在调试的时候无法按照源码来设置断点。

    4.LocalVariableTable属性

    作用:描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。影响是,若不生成,当其他人引用该方法,所有的参数名称都将丢失,IDE可能会使用如arg0等的占位符代替原有的参数名,对代码编写带来极大不便。

    5.SourceFile属性

    作用:用于记录生成这个Class文件的源码文件名称。若不生成,当抛异常时,堆栈中将不会显示出错误代码所属的文件名。

    6.ConstantValue属性

    作用:通知虚拟机自动为静态变量赋值。

    7.InnerClasses属性

    作用:记录内部类与宿主类之间的关联。

    8.Deprecated 及 Synthetic属性

    作用:都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。Deprecated属性用于表示某个类、字段或方法,已经被程序作者定为不再推荐使用,可在代码中用@deprecated注释进行设置。Synthetic代表该字段或方法不是由Java源码自动产生的,而是由编译器自行添加的。

    

第七章 虚拟机类加载机制 

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

     7.2 类加载的时机

     生命周期:加载、验证、准备、解析、初始化、使用、卸载。其中验证,准备,解析三个部分成为连接。

      顺序:加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,解析阶段不定:可以在初始化后再开始,这是为了支持Java的运行时绑定(也成为动态或晚期绑定)。加载阶段的开始由虚拟机的具体实现来决定。初始化阶段,虚拟机规定了有且只有四种情况必须立即对类进行“初始化”。

      情况:1,遇到new,getstatic,putstatic,invokestatic这4条字节码指令,即new一个对象,读取或设置一个类的静态字段,调用一个类的静态方法;2,使用java.lang.reflect包的方法对类进行发射调用的时候;3.父类没有进行初始化则要先初始化父类;4,虚拟机启动时,用户需指定一个要执行的主类,此主类要先初始化。

      以上为主动引用,除以上四种情况的都为被动引用,均不会触发初始化。

      接口与类加载的区别:在上述的第三种情况,接口在初始化时,不要求父接口全部完成初始化,只有在真正用到父接口时

  (如引用接口中定义的常量)才会初始化。

       7.3 类加载的过程

       定义:加载、验证、准备、解析和初始化这五个阶段。

       7.3.1 加载

       过程:1,通过一个类的全限定名来获取此类的二进制字节流;2,将这个字节流所代表的静态存储结构转为方法区的运行时数据结构;3,在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数的访问入口。

       7.3.2 验证

       作用:连接阶段的第一步,保证Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。

       验证过程:文件格式、元数据、字节码(类的方法体进行验证)、符号引用验证。

       7.3.3 准备

       说明:该阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。且分配的仅包括类变量,这里的初始值”通常情况下“是数据类型的零值。例如,public static final int value = 123,在准备阶段过后的值为0而不是123.把123赋值过去的动作是在程序被编译后。

      7.3.4 解析

      说明:此阶段为虚拟机将常量池内的符号引用替换为直接引用的过程。

      符号引用:以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。

      直接引用:可以是直接指向目标的指针、相对偏移量等,与内存布局相关,引用的目标已经在内存中存在。

      解析的具体时间根据显示需要判断,在执行了规定的13个操作符引用前,就会先对它们的符号引用进行解析。

       解析对象:四种,1,类或接口的解析;2,字段解析;3,类方法解析;4,接口方法解析。

       7.3.5 初始化

       初始化阶段是执行类构造器<clinit>()方法的过程。

       clinit方法:1,由编译器自动收集类中的所有类变量的复制动作和静态语句块中的语句合并产生的,收集顺序按照语句在源文件中出现的顺序。2,该方法与类的构造方法不同,它不需要显式地调用父类构造器,它会保证在子类的该方法执行前父类的该方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object;3,父类的静态语句块要优先于子类的变量赋值操作;4,该方法不是必须的;5,接口一样会有该方法,但它只有当父接口中定义的变量被使用时,父接口才会被初始化;6,虚拟机会保证一个类的该方法在多线程环境中被正确地加锁和同步。

       7.4 类加载器

       7.4.1 类与类加载器

       类加载器:让应用程序自己决定获取所需要的类,用于实现类的加载动作。比较两个类是否相等,必须要在同一个类加载器加载,否则即使是来源于同一个Class文件,这个两个类也必不会相等。

       7.4.2 双亲委派模型

       加载器类型:1,启动类加载器:为虚拟机所识别的类库加载到虚拟机内存中,启动类加载器无法被Java程序直接引用。

                             2,扩展类加载器:开发者可直接使用,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。

                   3,应用程序类加载器(系统类加载器):一般此为程序中默认的类加载器,为ClassLoader中的getSystemClassLoader()方法的返回值,负责加载用户类路径上所制定的类库。

       类加载器的关系如图:

                          

 

     

       除顶层的启动类加载器外,其余加载器都应当有自己的父类。并且父子关系以组合形式来复用父加载器的代码。

       工作过程:一个类加载器收到了类加载请求,会先把这个请求委派给父类加载器完成,每一层都是如此,如果父类加载器反应自己无法完成请求,子加载器才会尝试自己去加载。

       优点:Java类随着它的类加载器一起具备了一种带优先级的层次关系。

       7.4.3 破坏双亲委派模型(三次)

      1,在双亲委派模型之前;2,由它的自身缺陷导致,增加了线程上下文类加载器,使得父类加载器请求子类加载器去完成类加载的动作,例子有JDI,JDBC,JCE,JAXB和JBI等;3,用户对程序动态性的追求而导致(没太看懂)

 

第8章 虚拟机字节码执行引擎

      执行引擎:输入字节码文件,字节码解析,输出的是执行结果。类型有2种,解释执行和编译执行。

      8.2 运行时栈帧结构

      栈帧:1.定义:用于支持虚拟机进行方法调用和执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈中的栈元素。其大小仅仅取决于具体的虚拟机实现。

                  2.内容:包括局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。

                  3.当前栈帧:活动线程中的栈顶的栈帧,也是当前有效的栈帧。其所关联的方法为当前方法,执行引擎所运行的所欲字节码指令都只针对当前栈帧进行操作。

                  栈帧示意图:

                             

      8.2.1 局部变量表

      定义:一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。以Slot为单位。

      注意:局部变量一定要赋初值。

      8.2.2 操作数栈

      注意:1.其中的元素的数据类型必须与字节码指令的序列严格匹配,例如在用于整形数加法的指令中,最接近栈顶的两个元素必须都为int型,不能出现其他类型。

                  2.在概念模型中,两个栈帧都是独立的,但实际虚拟机会做优化,两个栈帧会有一部分重叠。

                                                         

 

      虚拟机的解释执行引擎为”基于操作数栈的执行引擎“。

      8.2.3 动态连接

      静态解析:Class文件中的常量池的符号引用在类加载或者第一次使用的时候转化为直接引用

      动态连接:Class文件中的常量池的符号引用在每一次的运行期间直接转化为直接引用。

      8.2.4 方法返回地址

      方法退出方式:1,正常完成出口;2,异常完成出口:执行中遇到异常,且该异常没有在方法体内得到处理。

      返回地址:正常退出时为调用者的PC计数器;异常退出时通过异常处理器表来确定。

      一般来说,动态连接、方法返回地址、其他附加信息都归为栈帧信息。

      8.3 方法调用

      唯一任务:确定被调用方法的版本。

      8.3.1 解析

      定义:被调用方法在程序代码中写好、编译器进行编译时就必须确定下来,此类方法的调用即为解析。

      符合条件的方法有静态方法和私有方法,前者与类直接关联,后者外部不可访问。因此都适合在类加载阶段进行解析。

      8.3.2 分派

      1.静态分派

      定义:依赖静态类型来定位方法执行版本的分派动作。最典型应用是方法重载。

      发生时期:编译阶段

      2.动态分派

      定义:在运行期根据实际类型确定方法执行版本的分派过程。

      3.单分派与多分派

      单分派:根据一个总量对目标方法进行选择,动态分派属于单分派。

      多分派:根据多于一个的总量对目标方法进行选择,静态分派属于多分派。

      4.虚拟机动态分派的实现

      过程:动态分派的方法版本选择过程需要在运行时在类的方法元数据中搜索合适的目标方法,故虚拟机采用稳定优化手段,为类在方法区中建立一个虚方法表,使用虚方法表索引来代替元数据查找来提高性能。虚方法表中存放的是各个方法的实际入口地址。

      其他优化手段:内联缓存,基于”类型继承关系分析“技术的守护内联,这两种都为非稳定的”激进优化“手段。

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

      8.4.1 解释执行

      Javac编译器完成了程序代码经过词法、语法分析,抽象语法树,再遍历语法树生成线性的字节码指令流的过程。这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现。

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

      例子:分别使用两种指令集去计算1+1的结果

                                     基于栈                                                  基于指令集

                                                                       

                                  

     

      优点:可移植性,基于寄存器会受到硬件的约束。

      缺点:执行速度相对较慢;完成相同功能所需的指令数量一般会比寄存器架构多;频繁的栈访问意味着频繁的内存访问,内存是执行速度的瓶颈。

 

第九章 类加载及执行子系统的案例与实战

      9.2.1 Tomcat:正统的类加载器架构

      主流Web服务器需解决的问题:1,部署在同一个服务器的两个Web应用所使用的Java类库可以实现相互隔离或者共享;2,服务器需尽可能地保证自身的安全不受部署的Web应用程序影响;3,支持JSP应用的Web服务器,十有八九都需要支持HotSwap功能。

      Tomcat目录结构:四个,/common/*,/server/*,/shared/*,/WEB-INF/*,具体含义百度。

      Tomcat自定义类加载器:Common/Catalina/Shared/WebappClassLoader,具体关系如下图(父类加载器加载的类能被子类加载器使用)                                     

 

      9.2.2 OSGi:灵活的类加载器架构

          定义:为OSGi联盟制订的一个基于Java语言的动态模块化规范 。

          好处:基于OSGi的程序很可能可以实现模块级的热插拔功能。

          类加载结构:Bundle(模块)类加载器之间没有委派关系。不涉及具体的Package时,每个加载器都是平级的关系,只有具体使用Package和Class的时候,才会根据Package导入导出定义来构造Bundle之间的委派和依赖;一个Bundle类加载器为其他的Bundle提供服务时,会根据Export-Package列表严格控制访问范围。

          问题:引入额外的复杂度,线程死锁和内存泄漏的风险。

      9.2.3 字节码生成技术与动态代理的实现

      动态代理:动态的优势在于可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活地应用在不同的应用场景。

      生成字节码:在ge'nerateProxyClass()方法中根据Class文件的格式去拼装字节码,但在实际开发中以byte为单位直接拼装除字节码的应用场合很少见。

      9.2.4 Retrotranslator:跨越JDK版本

      作用:可以把JDK1.5编译出的Class文件转变为可以在JDK1.4或1.3以上部署的版本。   

      JDK升级的功能大致分类:1,在编译器层面做改进(自动装拆箱,泛型);2,对Java API的代码增强;3,需要在字节码中进行支持的改动;4,虚拟机内部的改进。

      实现:上述四类,Retrotranslator只能模拟前两类。第二类可以独立类库的方式实现;第一类其使用ASM框架直接对字节码处理。

    第十章 早期(编译器)优化

      10.1 概述

      编译期:可指三类,分别为前端编译器,后端运行期编译器(JIT),静态提前编译器

      前端编译器:把*.java 文件转变为 *.class文件,代表为Sun的Javac,EclipseJDT中的增量式编译器。

      JIT编译器:把字节码转为机器码,代表为HotspotVM的C1、C2编译器。

      AOT编译器:直接把*.java文件编译成本地机器代码的过程。代表为GNU Compiler for the Java,Excelsior JET。

      10.2 Javac编译器

      编译过程:1,解析与填充符号表过程;2,插入式注解处理器的注解处理过程;3,分析与字节码生成过程。

      1.解析与填充符号表

      词法解析:把源代码中的字符流转变为标记(Token)集合,标记为编译过程中的最小元素。

      语法解析:根据Token序列来构造抽象语法树,由com.sun.tools.javac.parser.Paser完成;抽象语法树是一种用来描述程序代码语法结构的树形表示方式,每一个节点都代表着程序代码中的一个语法结构。由com.sun.tools.javac.tree.JCTree表示。

       2.填充符号表

      符号表:由一组符号地址和符号信息构成的表格。

      填充过程由com.sun.tools.javac.comp.Enter类实现,此过程的出口是一个待处理列表,包含了每一个编译单元的抽象语法树的顶级节点,以及package-info.java(若存在)的顶级节点。

      3 注解处理器

      作用:可以把其看作一组编译器的插件,在插件里面可以读取、修改、添加抽象语法树中的任意元素。若这些插件在处理注解期间对语法树进行了修改,那么编译器会回到第一个过程重新处理,直到所有的插入式注解处理器都没有再对语法树进行修改位置,每一次循环称为一个Round,即上图的回环过程。

      4.语义分析与字节码生成

      语义分析作用:对结构上正确的源程序进行上下文有关性质的审查,如进行类型审查。      

 

 

 

      过程:1,标注检查;2,数据及控制流分析;(1,2为语义分析步骤)3,解语法糖;4,字节码生成

      1.标注检查

      内容:变量使用前是否已被声明、变量与赋值类型是否能够匹配等;常量折叠(譬如定义a=1+2,经过常量折叠,会折叠为字面量3),故在代码中定义”a=1+2“比起直接定义”a=3“,并不会增加程序运行期哪怕仅仅一个CPU指令的运算量。

      实现类:com.sun.tools.javac.comp.Attr和sun.tools.javac.comp.Check类。

      2.数据及控制流分析

      作用:是对程序上下文逻辑更进一步的验证,它可以检查出如程序局部变量在使用前是否有赋值、方法的每条路径是否有返回值,是否所有的受查异常都被正确处理了等问题。

      3.解语法糖

      语法糖定义:指在计算机中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便

  程序员使用,可 增加程序的可读性。

      Java中常用的语法糖:泛型、变长参数、自动装箱拆箱等。Java在现代编程语言中属于”低糖语言“。

      解语法糖:虚拟机运行时不支持这些语法,它们在编译阶段被还原成简单的基础语法结构,这个过程为解语法糖。由desugar()方法触发,在com.sun.tools.javac.comp.TransTypes和com.sun.tools.javac.comp.Lower类中完成。

      4.字节码生成

      工作:把前面各个步骤所生成的信息(语法树、符号表)转换成字节码写到磁盘,还进行少量的代码添加和转换。

      完成了对语法树的遍历和调整,会把信息交到com.sun.tools.javac.jvm.ClassWriter类上,有此类的writeClass()方法输出字节码,生成最终的Class文件,到此整个编译过程宣告结束。

      10.3 Java语法糖的味道

      10.3.1 泛型与类型擦出

      泛型本质:参数化类型的应用,即所操作的数据类型被指定为一个参数,这种参数类型可用在类、i二口、方法的创建中,分别为泛型类、泛型接口、泛型方法。

      真实泛型:在C#中,泛型在程序源码、编译后的中间语言、运行期的CLR都是真实存在的,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现成为类型膨胀,基于这种方法实现的泛型称为真实泛型。

      伪泛型:在Java中,泛型只在程序源码中存在,在字节码文件中就已经被替换为原来的原生类型(裸类型)了,并且在相应的地方中插入了强制转换类型。故对于运行期中的Arraylist<int>和ArrayList<String>是同一个类。此种泛型实现方法为类型擦除,基于此种方法实现的泛型为伪泛型。

PS:关于类型擦除及其后续并不是很懂,于是上网找了一篇blog来解答疑惑

连接:https://blog.csdn.net/love_register/article/details/52279471

以及书本293前后几页的内容可以重新回看多几次,对类型擦除的理解会好很多。

      10.3.2 自动装箱、拆箱与遍历循环(Java中使用得最多的语法糖)

      PS:包装类的 “==”在没有遇到算术运算的情况下不会自动拆箱

		Integer a = 1;
		Integer b = 2;
		Integer c = 3;
		Integer d = 3;
		Integer e = 321;
		Integer f = 321;
		Long g = 3L;
		System.out.println(c == d);// true -127-127 有一个内部类,指向的均为此类
		System.out.println(e == f); // false 超过了上述范围,故为两个不同的对象比较
		System.out.println(c == (a+b)); // true 同1
		System.out.println(c.equals(a+b));// true
		System.out.println(g == (a+b)); // true // 在比较时,a+b得到的Intger进行自动拆箱,数据类型自动升为Long,后续比较与1相同
		System.out.println(g == (a+c));
		System.out.println(g.equals(a+b)); // false 类型不同精度不同

   10.3.3 条件编译

      C、C++:使用预处理指示符(#ifdef)来完成条件编译。预处理器的最初任务是解决编译时的代码依赖关系

      Java:无预处理器。(它是将所有的编译单元的语法树顶级节点输入到待处理列表中再进行编译,因此各个文件能够互相提供符号信息)

      Java进行条件编译:只能使用条件为常量的if,即if(true)等。若使用常量与带有其他条件判断能力的语句搭配,则可能会在控制流分析中提示错误,被拒绝编译。

      其他语法糖:内部类、枚举类、断言、对枚举和字符串的switch支持、在try中定义和关闭资源等

第十一章 晚期(运行期)优化

      11.1 概述

      即时编译器:把热点代码编译成与本地平台相关的机器码,并进行各种层次的优化。

      11.2 HotSpot虚拟机内的即时编译器

      11.2.1 解释器与编译器

      解释器:当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行。

      编译器:当把越来越多的代码编译成本地代码后,可以获取更高的执行效率。

      交互模式如下图:

 

        三种模式:1,“混合模式”,指解释器与编译器搭配使用;2,“解释模式”,只使用解释器;3

  ,“编译模式”,这时候将优先采用编译方式,但是解释器仍然要在编译器无法进行的情况的情况下介入执行过程。

        HotSpot编译策略:分层编译

        第0层:程序解释执行,解释器不开性能监控功能,可触发第一层编译。

        第1层:也称为C1编译,将字节码编译为本地代码,进行简单可靠的优化,如有必要将加入性能监控的逻辑。

        第2层(或2层以上):也称为C2编译,即将字节码编译成本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

        使用分层编译后,C1和C2编译器将会同时工作,许多代码会被多次编译。

        11.2.2 编译对象与触发条件

        热点代码:1,被多次调用的方法;2,被多次执行的循环体。

        栈上替换(OSR):对于热点代码为被多次执行的循环体的情况,尽管编译动作是由循环体所触发的,但编译器仍然会以整个方法作为编译对象。这种编译方式因为编译发生在方法执行过程,故被称为栈上替换。

        热点探测:探测一段代码是不是热点代码,是否需要触发即时编译的行为。目前主要的方法有两种,1,基于采样的热点探测;2,基于计数器的热点探测。

        1.基于采样的热点探测方法:虚拟机会周期性地检查各个线程的栈顶,若发现某个(些)方法经常出现在栈的顶,那么此方法则为“热点方法”。

        2.基于计数器的热点探测方法:虚拟机会为每个方法建立计数器,统计方法的执行次数,若次数超过一定的阈值就认为它是热点方法。(HotSpot中使用的是第二种)

        HotSpot触发即时编译示意图:

                                                     

 

        上述的两个计数器分别为方法调用计数器和回边计数器,回边计数器用于统计一个方法中循环体代码执行的次数。回边计数器统计的目的是为了触发OSR编译。(栈上替换)

        热度衰减:当超过一定的时间,如果方法的调用次数仍然不足让它提交给JIT编译器编译,那么此方法的调用计数器就会呗减少一半,此过程为热度衰减,而此段时间则称为此方法统计的半衰周期。

        回边计数器触发OSR编译的过程:

 

        两种计数器的不同:回边计数器没有计数热度衰减的过程;当计数器溢出时,它会把方法计数器的值也调整到溢出状态,这样下次进入改方法时就会执行标准编译过程。

        PS:上述都仅仅是描述Client VM 的即时编译方式,Server VM 要复杂得多。

                                    

 

        11.2.3 编译过程

 

 

        1.Client 编译器过程:它是一个简单快速的三段式编译器,主要的关注点在于局部性的优化。

          

       2.Server 编译器过程:专门面向服务端的典型应用并未服务端的性能配置特别调整过的编译器,它会执行所有经典的优化动作:如无用代码消除等等。

       11.3 优化技术概览

       典型代表:1,语言无关的经典优化技术之一:公共子表达式消除;2,语言相关的:数组范围检查消除;3,最重要的:方法内联;4,最前沿的:逃逸分析。

       11.3.2 公共子表达式消除

       含义:如果一个表达式E已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为了公共子表达式。则对于此种表达式,直接用前面计算过的表达式结果代替E就可以了。

       局部公共子表达式消除:此种优化仅限于程序的基本块内

       全局公共子表达式消除:此种优化涵盖了多个基本块

       11.3.3 数组边界检查消除

       优化:例如,在运行期只对数组边界进行少数必要的检查,如数据下标为一个常量,如foo[3],只需要在编译器根据数据流分析来确定foo.length的值判断是否大于3即可;在数据访问发生在循环内,只需判断循环变量的取值范围是否永远在[0,foo.length)内即可。

       其他与语言相关的消除:自动装箱消除、安全点消除、消除反射等。

       11.3.4 方法内联(这里有点复杂,没看懂)P309

       含义:把目标方法的代码“复制”到发起调用的方法中,避免发生真实的方法调用。

       11.3.5 逃逸分析

       含义:并非直接优化代码的手段,而是为其他优化手段提供依据的分析技术。

        基本行为:分析对象动态作用域:当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种行为称为方法逃逸。甚至可能被外部线程访问到,这种行为称为线程逃逸。

       为了避免一个线程不会发生以上两种逃逸,即别的方法和线程不能通过任何途径访问到这个对象,则可以为此对象采取以下优化::

       1.栈上分配:让此对象在栈上分配内存,对象所占用的内存空间就可以岁栈帧出栈而销毁。

       2.同步消除:若逃逸分析此变量不会出现线程逃逸,则可以消除此变量的同步措施,提高时效。

       3.标量替换:不可再分解的量为标量,可再分解的为聚合量。Java中的对象为聚合量。若逃逸分析发现此对象不会 出现方法逃逸,并且此对象可拆分,则可以改为直接创建它的若干个被这个方法使用到的成员变量来代替。

        此项技术还不成熟,不能保证逃逸分析的性能必定高于它的消耗,故很少使用。

 

第五部分 高效并发

        第十二章 Java内存模型与线程

        12.2 硬件的效率与一致性 

        为了让运算快速进行,现代计算机系统都不得不加入一层高速缓存来作为内存与处理器之间的缓冲,将运算需要使用的数据复制到缓存中,当运算结束后再把缓存同步回内存。

        出现问题:缓存一致性

        解决方案:各个处理器访问缓存都要遵循一些协议,在读写时根据协议来操作。例如MESI,MOSI,Synapse,Firefly及Dragon Protocol等等。

        处理器、高速缓存、主存间的交互关系:

                      

        乱序执行优化(处理器的内部优化):为了让处理器内部的运算单元能尽量被利用,处理器会对输入代码进行乱序执行优化。它会在计算之后将乱序执行的结果宠卒,保证该结果与顺序执行的结果一致。(各个语句计算顺序不一定一致)与其类似的有Java虚拟机中的即时编译中的指令重排序优化。

        12.3 Java内存模型

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

        12.3.1 主内存与工作内存

        内存模型的目标:定义程序中各个变量的访问规则,即从虚拟机中将变量存储到内存和出从内存中取出变量这样的底层细节。但不包括局部变量和方法参数,因为它们是线程私有的,不存在竞争问题。

        主内存:所有变量的存储位置

        工作内存:每条线程都有自己的工作内存,工作内存里面保存了被该线程用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读取主内存中的变量。线程中变量的值的传递均需通过主内存来完成。三者关系如下图。                                                                       

        12.3.2 内存间交互操作

        共八种:

        用于主内存的变量:

        1,lock(锁定):把一个变量标识为一条线程独占的状态。

        2.unlock(解锁):把处于锁定状态的变量释放,释放后才可被其他变量锁定。

        3.read(读取):把一个变量的值从主内存传输到线程的工作内存中。(与load配合)

        8.write(写入):把store操作从工作内存中得到的变量的值传送到主内存中,以便以后的write操作使用。

        用于工作内存的变量:

        4.load(载入):把read操作中的变量值放如工作内存的副本。

        5.use(使用):把工作内存中的每个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用的变量的值的字节码指令时将会执行此操作

        6.assign(赋值):把一个从执行引擎接收到的值赋值给工作内存的变量。

        7.store(存储):把工作内存中的每一个变量传送到主存中。(与write配合)

        共有七条需遵守的执行规则。

        12.3.3 对于volatile型变量的特殊规则

        volatile关键字作用:1,保证此变量对所有线程的可见性,但不确保基于此类型变量的运算在并发下是安全的。

故在不符合以下两条规则的运算场景中,仍需通过加锁来保证原子性。

        1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程能够改变变量的值。

        2.变量不需要与其他的状态变量共同参与不变约束。

        作用2:禁止指令重排序优化。

        与synchronized的比较:volatile变量读操作的消耗性能与普通变量几乎无区别,但写操作会比较慢。因为会在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。但大多数场景下,volatile的总开销要比锁低。

        规则太多了,详见书本P348页。

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

        long和double的非原子性协定:java中允许虚拟机对没有被volatile修饰的64位数据的读写操作分为两次的32位的操作进行,即允许虚拟机可以不保证64位数据类型的load、store、read和write这四个操作的原子性。

        但实际开发中,所有商用虚拟机都把64位数据的读写操作作为原子操作对待,因此一般不需要将long和double变量专门声明为volatile。

        12.3.5 原子性、可见性与有序性

        此三类为Java内存模型的特征。

        原子性:由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write六个操作。大致可以认为基本类型的访问读写是具备原子性的。若需保证更大范围的原子性,可以使用lock和unlock。虚拟机并未直接开放上述两个操作给用户,但可以通过字节码指令monitorenter和monitorexit来隐式使用,这两个字节码指令反应到Java代码中就是synchronized关键字。

        可见性:即当前一个线程修改了共享变量的值,其他线程能立即得到这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。除volatile外,synchronized和final也能实现可见性。对final来说,被final修饰的字段在构造器一旦被初始化完成,并且没有this引用逃逸,那么其他线程中就能看见final字段的值。

        有序性:本线程内观察,所有操作均有序;在一个线程中观察另一个线程,所有操作均无序。前半句对应“线程内部表现为串行的语义”,后半句对应“指令重排序”和“工作内存与主内存同步延迟”现象。volatile和synchronized可保证线程之间操作的有序性。synchronized保证了持有同一个锁的两个同步快只能串行进入。

        12.3.6 先行发生原则

        定义:先行发生是Java内存模型中定义的两项操作之间的偏序关系,若操作A发生在操作B之前,则操作A产生的影响能被B观察到。

        Java中一些“天然的”先行发生关系:1,程序次序规则;2,管理锁定规则;3,volatile变量规则;4,线程启动规则;5,线程终止规则;6,线程中断规则;7,对象终结规则;8,传递性。

        PS:时间上的先后顺序与先行发生原则之间基本无太大关系,衡量并发安全问题时必须以先行发生准则为准。

        12.4 Java与线程

        12.4.1 线程的实现

        有三种方式,使用内核线程实现;使用用户线程实现;使用用户线程加轻量级进程混合实现。

        1.使用内核线程实现:

        内核线程即直接由操作系统内核支持的线程,每个内核线程都可以看作内核的一个分身,支持多线程的内核交多线程内核。程序一般不直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(即我们通常意义上所讲的线程)。轻量级线程与内核线程间1:1的关系称为一对一的线程模型,如图所示。

                                        

        缺陷:各种进程操作均需进行系统调用,系统调用的代价相对较高;需要消耗一定的内核资源。

        2.使用用户线程实现

        狭义的用户线程指完全建立在用户空间的县城库上,系统内核不能感知到的线程存在的实现。这种进程与用户线程之间1:N对的关系就称为一对多的线程模型,如图所示。

                           

        缺陷:所有线程操作都需要用户程序自己处理,诸如“”阻塞如何处理“、”多处理器系统如何将线程映射到其他处理器上“等问题解决异常困难,故用户线程使用得越来越少。

        3.混合实现

        即存在用户线程,也存在轻量级进程。此模式下,用户线程与轻量级进程的数量比为M:N的关系,为多对多的线程模型。许多Unix系列的操作系统,都提供了M:N的线程模型实现。

                                

        4.Java线程的实现

        JDK 1.2前:基于名为”绿色线程“的用户线程实现

        JDK1.2:基于操作系统原生线程模型实现

       在Windows和Linux版本:使用一对一的线程模型实现

        在Solaris平台:一对一及多对多

        12.4.2 Java线程调度

        定义:指系统为线程分配处理器使用权的过程,主要调度方式有两种,协同式和抢占式。

        协同式:线程把自己的工作执行完后主动通知系统切换到另外一个线程上。实现简单,但线程执行时间不可控。

        抢占式(Java采用):系统分配执行时间,线程切换由系统决定。

        12.4.3 状态转换

        进程状态:五种,分别为新建,运行,无限期等待,限期等待,阻塞,结束。

        新建(New):创建后尚未启动的线程处于此种状态

       运行(Runnable):包括了操作系统线程状态中的Running和Reday。

       无限期等待(Waiting):等待被其他线程显式唤醒

       限期等待(Timed Waiting):一定时间后会由系统自动唤醒

       阻塞(Blocked):在等待获取一个排他锁

       结束(Terminated):已终止线程的线程状态

上述五种状态的转换图如下:

                        、

      第十三章 线程安全与锁优化

      13.2 线程安全

      定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个线程就是线程安全的。

      13.2.1 Java语言中的线程安全

      按照”安全程度“将Java中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

      1.不可变

      如果是基本数据类型,则只要定义时使用final即可。若为一个对象,那么就需要保证对象的行为不会对其状态产生任何影响。(参照String)

      确保措施 :可以把对象内带有状态的变量都声明为final

      2.绝对线程安全

      其完全满足上述线程安全的定义。此外,在Java API中标注自己为线程安全的类,大多数都不是绝对的线程安全。

      (这里举了一个使用vector的例子,但是我不是很懂,P347)

      3.相对线程安全

      即通常意义上的线程安全,对于一些特定顺序的连续调用,就可能要采取额外的同步手段来保证调用的正确性。在Java中,大部分的线程安全都是此类型,例如Vector,HashTable,Collections的sychronizedCollection()方法包装的集合等。

      4.线程兼容

      指对象本身并非线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中安全地使用。Java API中大部分类都是线程兼容的,例如前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。

      5.线程独立

      指不管调用段是否采用了同步措施,都无法在多线程环境下并发使用的代码。由于Java语言天生就具备多线程特性,线程独立很少出现,且通常有害,应尽量避免。

      13.2.2 线程安全的实现方法

      1.互斥同步

      它是最常见的一种并发正确性保障手段,同步指多个线程并发访问共享数据,保证共享数据在同一时刻只被一条线程使用。互斥是实现同步的手段,临界区、互斥量和信号量都是主要的互斥实现方式。

      问题:进行线程阻塞和唤醒所带来的性能问题,故也称为阻塞同步,一种悲观的开发策略

      java中的互斥实现方式:

      1.synchronized:其为一个重量级操作,因为状态转换需要消耗很多的处理器时间。

      2.重入锁(java.util.concurrent包下的):与synchronized基本相似,都具备一样的线程重入特性,只不过一个表现为API从层面的互斥锁,一个表现为原生语法层面的互斥锁。且ReentrantLock比synchronized增加了一些高级功能,主要有等待可中断、可实现公平锁、以及锁可以绑定多个条件。

      等待可中断:指正在等待的线程可选择放弃等待。

      公平锁:多个线程在等待同一个锁,必须按照申请锁的时间顺序来依次获得锁。synchronized的锁是非公平的,ReeentrantLock默认下也是非公平的。

      锁绑定多个条件:指一个ReetrantLock对象可以同时绑定多个Condition对象,只要多次调用newCondition()方法即可。

      两者对比,如果synchronized可以实现需求,优先考虑synchronized。

      2.非阻塞同步

      一种基于冲突检测的乐观并发策略,需要”硬件指令集的发展“才能进行。常用指令有:测试并设置;获取并增加;交换;比较并交换(CAS);加载链接/条件储存。其中,后两条是新增加的.(后面在讲CAS,但是没看懂P354)

      3.无同步方案

      若方法中不涉及共享数据,那么这些代码就是天生安全的。以下介绍两类:

      1.可重入代码(Reentrant Code):也叫纯代码,可以在代码执行的任何时刻中断,转而去执行另外一段代码,控制权返回后,结果不会出现任何错误。

      特征:不依赖在堆上的数据和公共资源,用到的状态量都由参数传入、不调用非可重用方法。

      判断原则:若一个方法返回结果是可预测的,即满足可重入性的要求,即是线程安全的。

      2.线程本地存储:满足共享数据的代码保证在同一个线程中执行。

      例子:大部分使用消费队列的架构模式,应用实例入经典Web交互模型中的”一个请求对应一个服务器线程“。

      实现:在java中,可以通过java.lang.ThreadLocal类来实现线程本地存储功能。每个线程的Thread对象都有一个ThreadLocalMap对象,此对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对。

      13.3 锁优化

      13.3.1 自旋锁和自适应自旋

      自旋锁意义:共享数据的锁定状态通常很短,挂起和回复线程的操作给系统的并发性能带来很大压力。

      自旋锁定义:如果物理机器有一个以上的处理器,能让两个或两个以上的线程同时并行执行,就可让后面请求锁的线程执行一个忙循环(自旋)。

      优缺点:若锁被占用时间端,则自旋效果好;若占用时间长,则会白白消耗处理器资源,带来性能的浪费。

      自适应的自旋锁:自旋的时间由前一次在同一个锁上的自旋时间及锁的持有者的状态来决定。

      13.3.2 锁消除

      定义:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

      判定依据:逃逸分析的数据支持

      13.3.3 锁粗化

      定义:若虚拟机探测到整个一连串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部。例子:

                

它的锁会扩展到第一个append()操作前到最后一个append()操作后。(本来是每个append都会的,因为string不可变,所以sb对象就是锁)

      13.3.4 轻量级锁

      1.HotSpot虚拟机对象头Mark Word图示

              

      加锁过程:加锁前后堆栈与对象的状态图示(过程有点复杂,详看P359)

                                         

      若加锁失败,则轻量级锁膨胀为重量级锁,锁标志的状态值变为”10”,Mark Word中存储的就是指向重量级锁的指针,后面等待锁的进程也要进入阻塞状态。

      解锁过程:同样通过CAS操作,若对象的Mark Word仍然指向着线程的锁记录,就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word 替换,若替换失败,则说明有其他线程尝试过获取该锁,则在释放锁的同时,唤醒被挂起的线程。

      提升性能的依据:对于绝大部分的锁,在整个同步周期内都不存在竞争。在有竞争的情况下,轻量级锁会比重量级更慢。

      13.3.5 偏向锁

      定义:偏向锁会偏向于第一个获取它的线程,若在下面的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。

     加锁:若虚拟机启动了偏向锁,则当锁对象第一次被线程获取时,对象头的标志位会被设“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程ID记录到对象的Mark Word之中。若CAS成功,则持有偏向锁的线程以后进入这个锁相关的同步块时,将不用进行任何同步操作。

     解锁:若有另一个线程尝试获取该锁,偏向模式就结束。根据锁对象目前是否被锁定的状态,撤销偏向恢复到未锁定或轻量级锁定的状态。后续操作入上面介绍的轻量级锁那样执行。偏向锁、轻量级锁、对象Mark Word 转换关系如图。

             

   

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值