一、走进Java
1.几个内存区域,程序计数器/java方法栈/本地方法栈都是线程相关的,编译时就能确定要分配多少内存和在何时收集(方法结束时)。主要考虑的是Java堆和方法区的内存回收。(多个实现类需要的内存不同,方法的多个分支用的内存也不同,运行时才知道创建多少对象,这部分分配和回收都是动态的)
2.回收对象查找算法。
(1)引用计数算法。看对象上的计数,缺点:很难解决循环引用问题。
(2)根搜索算法。通过一系列名为GC Roots的对象做为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链(一个对象对gc root没有任何引用链就可以回收)
gcroot:虚拟机栈中引用的对象,方法区中静态属性和常量引用的对象;本地方法栈JNI(一般的native方法)引用的对象。
(3)再谈引用。强引用(obj=new Object()) ; 软引用(在内存溢出之前会回收)--SoftReference;弱引用WeakReference(只生存到下次垃圾收集之前);虚引用PhantomReference(对回收没影响,只是回收时会收到系统通知)。
(4)何时死亡。两次标记过程。找到没有引用链的对象进行一次标记,有finalize方法实现且没有被Jvm调用过Finalize的对象放入F-Queue队列。(finalize方法只有一次复活机会,在第二次标记之前复活)。没有复活第二次标记,会马上死亡。F-Queue队列(在一个低优先级线程中执行,防止Finalize执行很慢卡住)
(5)回收方法区。(hotspot成为永久代)--收集效率低(堆一次收集会收回70-95%空间)。(jvm规范不要求在这里实现垃圾回收)。主要回收两部分:废弃的常量和无用的类。
废弃常量判断类似于堆(如"abc"---如果没有引用他的变量或常量,会认为是常量池中废弃的常量)。其他常量池中的类(接口)/方法/字段的符号引用法也这样。
判定无用类的法则,同时满足:该类的所有实例已经回收(已经不存在该类实例);加载该类的classloader已经回收;该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射获取该类代码。对类hostspot虚拟机用-Xnoclassgc控制,还可以用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading察看类的加载和卸载信息。(-verbose:class以及-XX:+TraceClassLoading可以在product版虚拟机用,-XX:+TraceClassLoading参数需要fastdebug版虚拟机支持)。
大量用发射/动态代理/cglib等bytecode框架的场景,以及动态生成大量jsp和osgi这类频繁自定义classloader场景需要具备类卸载功能,保证永久代不溢出。
3.垃圾收集算法:(涉及程序细节且操作内存方式不同,不讨论细节)----内存回收方法论。
(1)标记-清除算法:容易产生碎片。
(2)复制算法:易产生浪费,新生代的主要算法。
(3)标记-整理算法:速度较慢。
(4)分代收集算法:分新生代和老年代。
4.垃圾收集器。
(1)Serial收集器。 UseSerialGC. 单线程,阻塞。clent模式下默认的模式。
(2)ParNew收集器. UseParNewGC. 多线程,阻塞。-XX:SurvivorRatio -XX:PretenureSizeThreshold(多大的对象直接放入永久代) -XX:HandlePromotionFailue(决定FullGc的频率--打开选项会减少fullGC). Server模式运行首选的收集器(只有它能与cms一起工作,性能).
用XX:ParallelGCThreads 限制线程数。
(3)Parallel Scavenge收集器.UseParallelGC。(考虑的是吞吐量)与g1都没有用传统的gc收集器代码框架,另外独立实现。其他的都共用部分代码框架。
关注点最不同:吞吐量。可以限制两个参数。-XX:GCTimeRatio 与-XX:MaxGCPauseMills.--毫秒数 (gctimeratio=0-100的整数。gc/total 吞吐量的倒数。---1/(1+值)=gc时间比。
开关参数:-XX:+UseAdaptiveSizePolicy 开关参数。(有了它不需要指定新生代大小xmn,SuivivorRatio,今生老年代年龄PretenureSizeThreshold等参数了)只需指定上面两个参数中的一个,以及xmx。----gc ergonomics(gc自适应的调节策略).
(4)Serial Old收集器. UseParallelOldGC。 标记整理。用途---jdk5以前与parallel scavenge搭配使用;cms收集器的后备预案(发生concurrnt Mode Failure)时用。
(5)Parallel Old收集器. UseParallelOldGC 标记-整理。jdk6中有了才能和parallel scanvage提供吞吐量优先的收集器。
(6)CMS收集器(concurrent Mark Sweep)。useConcMarkSweepGC.强交互应用划时代的收集器--真正意义并发。
四步:初始标记cms initial mark(扫描与根直接关联的对象)、cms concurrent mark(初始标记的基础上向下追溯标记)、cms remark(扫描cms堆中的对象,重新标记,并并处理对象关联)、cms concurrent sweep. (两个并发操作可以与用户线程一起用)。 除此外还有concurrent precleaning(超找在并发标记阶段新进入老年代的对象,减少remark阶段压力). cms remark(cms堆数据结构重置)。
缺点:cpu资源敏感;无法处理浮动垃圾,并行过程中产生的垃圾,不能等满了清除。-XX:CMSInitiatingOccupancyFraction提高百分比,比例高引发concurrent mode failure.
;碎片(标记清理) -XX:+UseCMSCompactAtFullCollection清理完碎片整理.,-XX:CMSFullGCsBeforeCompaction.
(7)G1收集器.
5.内存分配回收策略。-XX:PrintGCDetails
(1)在堆上分配,优先在Eden分配;多线程开通了缓存,优先在TLAB上分配. Minor GC/Full GC.
(2)大对象直接进入老年代. -XX:PretenureSizeThreshold.
(3)长期存活对象直接进入老年代;-XX:MaxTenuringThreshold
(4)动态对象年龄判定;如果一个年代的存活的对象大小超过survior对象的一半,年龄大于或等于改年龄对象都进入老年代。
(5)空间分配担保。-XX:HandlePromotionFailue。
(三)虚拟机性能监控与故障处理工具
1.命令行工具。使用的是tools.jar里面的类和接口,可以用tools.jar中的东西自己写监控。
(1)jps: jsp显示java执行主类。-l(全名) -q(只查进程号) -v(启动时参数) -m(输出main的参数)
(2)jstat:监视性能(显示虚拟机进程中类装载、内存、运行时编译信息)。
参数-class -gc -gccapacity -gcutil -gccause -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -compiler -printcompilation.
jstat option vmid [interval [s|ms]] [count]
(3)jinfo:java配置信息的工具。 java参数的默认值显示方法(java -XX:+PrintFlagsFinal ; jinfo -sysprops显示系统属性;jinfo -flag 查询参数。
jinfo [option] pid -flag arg=value.
(4)jmap:java内存影像工具。(不用它可以用. -XX:+HeapDumpOnOutOfMemoryError ; -XX:+HeapDumpOnCtrlBreak参数)
jmap [option] vmid
option : -dump , -finalizerinfo ,-heap,-histo,-permstat ,-F.
(5)jstack:java堆栈跟踪工具。成为java threaddump或者javacore文件。每条线程正在执行方法堆栈集合(照线程卡顿原因)。
jstack [option] vmid . -F强制输出 -l还有锁信息 -m显示本地方法c/c++堆栈。 java.lang.Thread添加了getAllStackTraces()方法用于显示所有线程的StackTraceElement对象。
(6)jhat.
jhat dmp文件 会启动一个服务,可以在网页中看。包为单位分组展示;heap histogram与jmap-histo功能类似,ool页签。
2.jdk可视化工具。
(1)JConsole.默认概述/内存/线程/类/vm摘要/MBean.六个页签。
针对JMX MBean进行管理。内存-jstat ,线程jstack.
(2)VisualVM.
兼容范围和插件安装(npm以及自动安装)。s生成和浏览转储快照;分析程序性能profiling.
Btrace 可以给正在运行的程序增加代码输出日志调试;还可以进行性能监视、定位连接泄露、内存泄露、解决多线程竞争问题。
(3)商用. IBM---Support Assistant/Heap Analyzer/Javacore Analyzer/Garbage Collector Analyzer.
HP---HPjmeter、HPjtune.
Eclipse--Memory Analyzer Toll.
BEA--JRockit Mission Control.
(四)调优案例分析与实战。(前两章是知识/上一章是工具,本章是经验)--故障处理和调优经验。
1.案例分析
(1)高性能硬件上的程序部署策略。部署策略:通过64位Jdk使用大内存;使用若干个32位虚拟机建立逻辑集群使用硬件资源。
分配大堆需要把FULL GC频率控制的足够低--(绝大部分对象是否符合朝生夕灭的原则;不能产生大量的、长生存时间的大对象)
64位jdk考虑的问题:内存回收导致的长时间停顿;64位Jdk性能低于32位;需要保证程序足够稳定,大内存几乎不能产生内存快照;相同程序在64位消耗大--指针膨胀/类型补白等)
32位集群方式:启动多个应用程序线程分配不同端口,前段搭建负载,以反向代理的方式分配访问请求。无session复制的亲和式集群符合要求。缺点:避免竞争全局资源(如磁盘)同时访问一个文件会有io异常;难高效率用某些资源池(连接池,自己节点建立自己的连接池;公共连接池用jndi会牺牲一部分性能);各个节点受到32位内存的限制(只能2GB);大量使用本地缓存(hashmap作为k/v存储),可以考虑用集中缓存。 最后用cms手机起进行内存回收提高老年代的回收速度。
(2)进群间同步导致的内存溢出。2台服务器,每台3个实例的亲和式集群。未考虑非堆内存的使用导致系统内存溢出。用JBossCache全局缓存,带着-XX:+HeapDumpOutOfMemoryError参数运行了一段时间,发现了大量的org.jgroups.protocols.pbcast.NAKACK对象。JBossCache用Jgroups进行集群间数据通讯,用协议栈的方式实现特性*组合,数据包接受和发送经过up/down方法(保证有效顺序以及重发).信息重发的可能性,发送的信息要在内存中保留,而且此服务实现了全局Filter,更新访问时间到其他节点,一个用户的请求会造成几次甚至几十次请求,网络交互频繁,堆积的数据内存溢出。
jbossCache不适合频繁写。
(3)堆外内存导致的溢出错误。
-XX:+HeapDumpOutOfMemoryError也不会产生文件,只在系统日至里发现了内存溢出。 2GB给了堆1.6G.大量NIO导致的,direct 内存不能引发full gc,只能cache大喊system.gc();虚拟机不听(禁用显式gc)报内存溢出.
除了堆内存和永久代外还有:Direct Memory/线程堆栈/Socket缓存区/JNI代码/虚拟机和gc.
(4)外部命令导致系统很慢. 用户的请求处理会请求一个shell获取系统信息(Runtime.getRuntime().exec())调用,很消耗资源。执行外部命令:克隆一个跟当前虚拟机一样环境变量的进程,再用这个线程执行命令,最后推出这个进程,频繁执行,消耗很大。(cpu/内存)---fork.
(5)服务器JVM进程崩溃. hs_err_pid###.log. mis系统与oa集成。(远端连接异常)。
mis工作流要返回给oa的待办事项。soapUI测试了Oa待办的几个web服务很慢。
为了不被拖慢,采取了异步服务,由于两边速度不对等,时间多了积累了很多没完成的信息;(等待的线程和socket连接多导致虚拟机承受不了崩溃)
解决:通知解决慢的web服务;使用生产者消费者模式消息队列。
2.实战:Eclipse运行速度调优。(gc和内存、类加载、编译时间、版本)
(1)调优前运行程序的状态。启动不是很快。
(2)升级JDK1.6的性能变化及兼容问题。添加-xx:maxPermSize制定永久代容量。
(3)编译时间和类加载时间的优化。字节码验证时间久。-Xverify:none.
编译时间---转化为机器码即时编译器。-client/-server.
(4)调整内存设置控制垃圾收集频率.减少minor gc/尤其是full gc次数。
因为逐渐增加到最大内存导致的。一次调到。-xms和-XX:PermSize设置。
后面的full gc由于系统的显式gc. 禁掉。(-XX:+DisableExplicitGC.) (经常用jstat -gccause查询最近gc原因。
(5)选择收集器降低延迟。(编译代码时老年代时间久)。
-XX:+UseConcMarkSweepGC和-XX:+UseParNewGC. (提高cms收集门槛默认68%. -XX:CMSInitiatingOccupancyFraction=85.
服务器的调忧还会有数据库、资源池、磁盘io.
三、虚拟机执行子系统
(一)类文件结构(机器指令集无关、平台中立的格式)
1.无关性的基石。平台无关+语言无关。实现无关的基石是字节码和虚拟机。(字节码命令提供的语义描述能力比Java语言本身更强大,java不支持的特性字节码本身支持)。
2.Class文件结构。class是以8位字节为基础单位的二进制流,各项严格紧凑的存在CLass文件中。(8位以上的数据按照高位在前方式分割成8位字节)。
用类C的结构体存储,class中只有无符号整数与table表数据类型类型。无符号数:u1/u2/u4/u8(描述数字,索引引用,数量值,utf-8串值)。
表类型习惯以_info结尾。
(1)魔数与class文件的版本。头4个字节--(0x)CAFEBABE. 紧接着4个字节是版本。5-6是Minor version. 7-8是大版本。(jdk1是45,每个版本+1)。
(2)常量池。与其他项目关联最多的类型,第一个出现的表类型(cp_info 代表constant_pool)。constant_pool_count(u2) +cp_info. 容量计数从1开始,不是从0.(0省出来做空引用,其他都是从1引用)。
常量池包含字面量和符号引用。 字面量--java语言层面的常量;符号引用-编译原理方面概念(类和接口全限定名;字段的名称和描述符;方法的名称和描述符)。
java动态连接,class不存方法字段的内存布局信息,这些符号引用不经过转换没法用;运行时从常量池获得符号引用,类创建时/运行时解析翻译到具体内存地址中--创建和动态连接)。 每一个常量都是一个表。11中结构不同的数据(1-12,缺2).
-----CONSTANT_Class_info型常量结构:u1-tag;u2-name_index(指向utf-8常量部分)。
CONSTANT_utf8_info: u1/u2/u1(长度Length). 分别代表标志/长度/实际的utf8串。
可以用javap -verbose帮我们翻译。
(3)访问标志。2个字节访问标志(class的)。
(4)类索引-u2/父类索引-u2/接口索引集合。
接口索引集合:interface_count接口数量。----再后面跟着接口的index集合。
(5)字段表集合(field_info). 包括类级和实例级,不含局部变量。(字段信息:作用域;实例/类;可变性;并发可见性;序列化;数据类型;名称。
结构:
u2 access_flag
u2 name_index (字段简单名称)
u2 descriptor_index (类型索引) 描述符含义--基本类型取第一个字母大写,对象类型取L+全限定名。 数组前面加[. (方法描述符,先参数列表后返回值)
u2 attributes_count
attribute_info。(如固定好的值ConstantValue---只有public static int有)。
java字段不能重名,但对字节码只有描述符不一致就可以。
(6)方法表集合
与字段类似。 签名机制不包含返回值,实际上class文件时可以认识返回值的。
(7)属性表集合。(前面长度/内容/顺序都要一样,这里不要求顺序。)
Code/ConstantValue/Deprecated/Exceptions/InnerClasses/LineNumberTable/LocalVariableTable/SourceFile/Synthetic.
符合规则的属性表。 u2/u2/u1分别表示attribute_name_index/attribute_length/info等信息。
-----Code属性。 attribute_name_index(u2)/attribute_length(u4)/max_stack(u2-操作数栈深度最大值)/max_locals(u2-局部变量表需要存储空间)/code_length(u4)/code(实际指令)/exception_table_length(u2)/exception_info(异常表)/attribute_count数量/attribute_info(code的属性数目和信息)。 整个class文件只有这里描述代码,其他都是元数据。
属性表(异常表结构):start_pc(u2)/end_pc(u2)/handler_pc(u2)/catch_type(u2)。
----Exceptions属性,这里记录throws出来的异常。结构:attribute_name_index(u2)/attribute_length/number_of_excepitons(u2)/exception_index_table.指向常量池
---LineNumberTable属性。java源码行号和字节码行号对比。 (可以在-g:none或者-g:lines取消或要求生成)。
attribute_name_index/length(u4)/line_num_table_length(u2)/line_num_info(start_pc和Line_number两个u2----前字节码行号后面是java行号)。
-----LocalVariableTable(栈帧中局部变量与源码定义变量的关系)(-g:none/-g:vars取消或要求生成)。与上类似。 info中存储的是name_index和descripter_index指向常量池。(start_pc/length/name_index/descriptor_index/index)
1.5泛型后引入了LocalVariableTypeTable属性。----把Descriptor_index换成了字段特征签名。非泛型---描述和特征签名相同,引入泛型后描述符中的参数化泛型的参数化类型被擦除掉了,描述符不能准确描述了就出了这个属性。
----SourceFile : (attribute_name_indx(u2)/attribute_length(u4)/sourcefile_index(u2)帮助找到源码文件文件名。
-----ConstantValue属性。自动为静态变量赋值(只有被static修饰的才用)。非static在<init>中进行;对应类变量有两种方式(<clinit>和ConstantValue).
如果是基本类型或者String且用final和static修饰会放在ConstantValue属性,否则放在<clinit>中。
结构:attribute_name_index/length/contantvalue_index. 这是个定长属性。
----InnerClass属性。记录内部类和宿主类的关系。(宿主和内部类都要有这个属性) attribute_name_index(u2)/attribute_index(u4)/number_of_classes(u2)/inner_class_info.
inner_class_info(inner_class_info_index(常量池中内部类符号引用)/out_class_info_index(常量池中外类符号引用)/inner_name_index(匿名内部类是0)/inner_class_access_flags)
---Deprecated及Synthetic属性。属于布尔。 attribute_name_index(u2)/attribute_length(u4). Length值都必须上设置成0.
3.class文件结构的发展。
改进集中在访问标志和属性表上。访问标志加入了ACC_SYNTHETIC/ACC_ANNOTATION/ACC_ENUM/ACC_BRIDGE/ACC_VARARGS五个标志。
属性表stackmaptable/enclosingmethod/signatue/sourcedebugextension/....Annotation和Runtime添加了一些属性。
(二)虚拟机类加载机制
1. (class文件加载到内存后进行校验、转换解析、初始化)。加载和连接是运行时完成,java可以动态扩展的语言天性就是以来运行期动态加载和动态连接这个特点实现的。
可以写针对接口的程序,等到运行时再指定其实现。本章说的class文件是二进制流。
2.类加载的时机
(1)加载(loading)-连接(验证verifying-准备preparation-解析resolution)-初始化(initialization)-使用(using)-卸载(unloading)
顺序不变的阶段:加载-验证-准备-初始化-卸载顺序不会变,解析阶段不一定。 解析可以在初始化后开始,支持Java的运行时绑定(动态绑定或晚期绑定)。
(2)加载---交给虚拟机实现(没有定义时机,可以定义自己的classloader).
(3)初始化-四种情况必须立即初始化:
a)遇到new/getstatic/putstatic/invokestatic这4条字节码指令时
b)用java.lang.reflect包的方法对类进行反射调用的时候(如果类还没初始化)
c)初始化一个类时候,发现父类没初始化,先初始化父类。
d)虚拟机启动时,用户指定的主类,先初始化这个类。
这四类叫主动调用,除此之外都是被动引用。 (如通过子类调用父类的静态字段,不会触发子类初始化,是否触发子类的加载和验证,虚拟机规范未规定hotspot会加载子类)--通过-XX:+TraceClassLoading。 被动引用的例子二:new 数组. 不会触发加载。 会触发一个[L..类初始化(是虚拟机自动生成,继承与Java.lang.Object的子类,有newarray动作触发,数组应有的方法和字段都有了)。
被动例子三:非主动使用字段。(对已经转化到常量池的Public static final字段访问)---不触发类初始化。(因为已经与类没什么关系了)
接口的初始化不同的一点:不会初始化父接口。
3.类加载过程。(字节码加载;验证;准备;解析;初始化)
(1)加载。任务:通过一个类全限定名获取二进制字节流;将这个字节流代表的静态存储转化为方法区的运行时数据结构;在java堆中生成一个代表Java.lang.Class的对象作为方法区这些结构的入口。
获取字节流的方式和位置都没有说,可以自己实现classloader.(zip/网络applet/运行时计算生成proxy/其他文件jsp/数据库中读取。
这是开发期可控性最强的阶段。
方法区存储格式由各虚拟机自己定义;加载阶段和连接阶段是交替进行的。(加载未完成,连接已经开始)
(2)验证。不正确会抛出VerifyError异常。
文件格式验证。(魔数/版本/常量池tag/指向常量的索引/utf8是否有不符合Utf8的/class个部分是否有被删除或者新添加的。)目的保证字节流正确的解析并且放入方法区内。(格式上符合Java类型信息要求)
元数据验证。(是否有父类;父类是否允许继承;是否抽象类,不是,是否都实现方法;类的字段方法是否与父冲突...
字节码验证.(数据流和控制流验证)-最复杂. 类型是否与字节码指令配合;跳转指令不会跳到方法体外;类型转化是否有效等等。由于复杂,设计了StackMapTable属性(描述了方法体所有的基本块-按控制流拆分)开始时变量本地表和操作栈应有的状态,将字节码验证的类型推倒转变为类型检查从而节省一些时间。-XX:-UseSplitVerifier来关闭这项优化或者用-XX:+FailOverToOldVerifier退回类型推倒。1.7不许退回。
符号引用验证. 校验发生在jvm将符号引用转化为直接引用的时候(这个转化动作在解析中发生)。 对类自身及常量池的信息进行匹配性校验:符号是否能找到推应类;类中是否存在符合方法字段描述符及简单名称描述的方法和字段;类方法/字段的访问性是否本类可访问。 【目的:确保解析动作能正常进行】--通过不了java.lang.InCompatibleClassChangeError的子类。 -Xverify:none参数关闭大部分类验证措施。
(3)准备:正式为变量分配内存并设置类变量初始值的阶段,内存会在方法区分配,仅包括类变量。会把Public final static int i这种类型的变量,进行赋值(constantValue).
static的变量需要在clint初始化后才赋值最终值,这里赋值0值。
(4)解析:虚拟机内的符号引用替换为直接引用的过程。Constant_Class_info等。
----两种引用区别:符号引用以一组符号来描述所引用的目标(引用与布局无关,不一定加载入内存)。直接引用可以是直接指向目标的指针或间接定位到目标的句柄(与内存布局相关)。
--何时:未明确规定。在执行anewarray/checkcast/getfield/getstatic/instanceof/invokeinterface/invokespecial/invokestatic/invokevirtual/multianewarray/new/putfield/putstatic 这13个操作指令之前,先对符号引用解析。【加载符号常量就解析还是使用时才解析,虚拟机自己实现】。一个符号多次解析很常见,虚拟机会对解析结果进行缓存(运行时常量池记录直接引用,标记为已解析)。四类解析如下。
a)类与接口的解析。(constant_class_info). 步骤:C不是数组类型,虚拟机就把要加载类的全名给本类加载器加载这个类(还有验证-其他类加载等),这个类加载失败,解析过程就失败;C是数组,且数组类型是对象,类似[L..形式,按照上面加载数组元素类型,然后由Jvm生成一个代表此数组维度和元素的数组对象;上面两步后C已经是有效的类或借口,解析完成前进行符号引用验证,确定C是否对D的访问权限。((C是加载类,D是本类)。
b)字段解析。Constant_Fieldref_info常量内容。对class_index进行解析,将这个字段或接口用C表示。C本身包含简单名称和字段描述匹配的字段转为直接引用;C中实现了接口,按照继承从上往下递归搜索各个接口和父接口,如果接口包含名称和描述匹配,返回直接引用;C不是Object,会按照继承关系从上往下递归搜索父类,负累包含,返回直接引用;否则查找失败。返回引用后会对权限验证。(父类和接口都存在这个字段拒绝编译)
c)类方法解析。类方法和接口方法是分开的,如果发现是接口直接报错;C中找;C的父类中递归查找;C的实现接口中找/抽象--AbstractMehodError;否则NoSuchMethodError.
d)接口方法解析。如果class_index不是接口就报错;看接口C中是否有,有就返回;父接口和Object找;否则NoSuchMethodError.
(5)初始化.开始真正执行java代码。执行<clint>()。-----收集类中赋值动作,静态块,顺序遵循原文件顺序,静态块只能访问之前定义的变量;<clinit>不需要显式调用负累构造器,虚拟机会保证子类clint前父类已经执行;父类的静态块优先与子类的;clint对类和接口不是必须的;接口中不用静态语句块,可以有变量初始化,不会先执行父接口的clinit;会保证多线程下的加锁同步。
4.类加载器。
(1)类与类加载器。二者共同确定唯一性,相等(equals/isAssignableFrom/isInstance等发给你法)。可以实现loadclass方法。(通过字节流都如用return defineClass(name,b,0,b.length)创建)。
(2)双亲委派模型。
默认BootStrap ClassLoader(负责JAVA_HOME/lib下和-Xbootclasspath指定路径下的rt.jar)/Extension ClassLoader(JAVA_HOME/lib/ext或者被java.ext.dirs指定的)/Application ClassLoader(是ClassLoader的getSystemClassLoader方法返回值-系统加载器,负责加载用户路径下的类classpath). 再有自定义的加载器一般会在应用加载器后。双亲委派模型使用组合实现。
每添加一个加载器---之需要实现findClass即可。(收到请求,先不自己尝试加载,交给父类加载器)
好处:随着类加载器有了带有优先级的层次关系,Object肯定是有BootStrap加载。(实现逻辑在loadclass中)
(3)破坏双亲委派模型。
第一次----Jdk1.2发布之前(这时候提出的,从这以后只实现findclass)。
第二次(这个模型自身缺陷导致)---基础类回调用户代码。如JNDI服务(由bootstrap加载,不认识用户代码),引入不太优雅设计线程上下文类加载器(java.lang.Thread,没设置从父线程继承,都没有用application classloader)-jndi用这个线程加载器加载需要的spi代码(父类加载器请求子类加载器)。SPI服务都采用了这样设计(JNDI/JDBC/JCE/JAXB/JBI)。
第三次:对程序动态性追求引起的。(代码热替换/模块部署)--OSGI。需要bundle会把classloader一起换掉。(java.*给父类;委派名单内的类给父类;import列表给Export的Bundle;当前Bundle的classloader;Fragment bundle classloader;dynamic import bundle的classloader;否则失败)
(三)虚拟机字节码执行引擎。自己制定的指令与执行引擎。(解释执行和编译执行)
1.运行时栈桢结构(支持方法调用和执行的数据结构)
每个方法调用到执行完都是一个栈桢的入栈和出栈。(需要内存提前已经确立好了,不会受运行时影响),活动线程只有栈顶的栈桢有效(当前栈桢--当前方法) 。 线程还有一个指令计数器。
(1)局部变量表。方法参数和内部定义变量值。变量槽slot.(一般每个变量一个槽,double/long两个)。----return address类型为jsr/jsr_w/ret服务(指向字节码指令地址)。
非static方法--0slot指的是this;其余参数按顺序排列;slot可重用;(slot复用会影响gc行为);没有初始化阶段。
(2)操作数栈。也叫操作栈(max_stacks). 指令向里面写入或者提取内容(类型要与指令严格匹配)。 解释执行引擎---基于栈的执行引擎。
(3)动态连接。栈桢包含一个指向运行时常量池该栈桢所属方法的引用,持有它支持调用过程的动态连接。(每一次运行时转为直接连接的叫动态连接。 类加载阶段/第一次运行转化--静态解析)
(4)方法返回地址。(正常和抛异常)。
都要返回到方法调用位置,返回时在栈桢保存一些信息,恢复上层方法的执行状态。(正常退出,pc计数器值可以作为返回地址,栈桢会保存这个计数器值;异常推出,返回地址需要一查功能处理器表确定,栈桢中不保存这部分信息)。推出时--恢复上层方法局部变量表和操作数栈,把返回值压入调用者栈桢的操作数栈中,调整pc计数器指向调用指令后面一条指令。
(5)附加信息。
2.方法调用
(1)解析:编译器可知,运行时不变(静态方法和私有方法)。invokestatic/invokespecial(init方法,私有方法,父类方法)/invokeinterface. 符合解析的只有:invokestatic/invokespecial。final的方法是通过invokevirtual调用,但是建议分到解析中。会直接分配直接引用,不用运行时决定。
(2)分派(多态-重载)
静态分派:按照传入参数的类型选择用哪个方法。(对重载理解)。 静态类型和实际类型。重载时用的是静态类型。 重载时按照类型选择最合适的方法版本。单个参数中自动转型,在变长参数是不能实现的。(解析和分派是不同层次筛选和确定目标方法的过程)。
动态分派(多态):override.过程:找到指向的实际类型C;在C中找到与常量中操作符和简单名称都相符的方法,权限校验(不通过报异常);对父类查找..;异常。
单多分派:接收者和方法参数--成为方法宗量。宗量多少--单多分派;静态分派是--多分派,动态分派是单分派。
(3)虚拟机分派实现。虚方法表vtable(虚方法表索引代替查找元数据);子类会把父类的方法(除了object)也放在表中。 vtable在类加载时进行初始化。
其他优化:内连缓存/CHA基于类继承关系的分析的守护内敛。
3.基于栈的字节码解释引擎(方法执行)
(1)解释执行.(gcj直接本地码)。源码--词法分析-单词流-语法分析-抽象语法树。后面两个分支
解释:指令流-解释器-解释执行
编译:优化器(可选)-中间代码(可选)-生成起-目标代码。 编译原理----源码转化为抽象语法树。(完整-gc++) (不完整javac,半独立实现).
(2)基于栈的指令集和基于寄存器的指令集。ISA-基于栈的指令集架构。(优势:移植性-代码紧凑/实现简单;缺点--速度慢-jvm通过栈顶缓存手段优化)
(3)基于栈的解释器执行过程。编译器有各种优化方法。
(四)类加载及执行子系统的案例与实战 (能通过程序操作的主要类加载器和字节码生成功能)。
1.例子:
(1)Tomcat--正统类加载(隔离/共享类/jsp_hotswap/自身不受影响。)
/common /share /server /webapp/webinf/
CommonClassLoader (CatalinaClassLoader独立) SharedClassLoader WebappClassLoader. 剩下三个都是父子关系。JsperClassLoader.
(2)osgi. 交叉加载可能会死锁(加载的时候--需要锁定)
(3)字节码生成和动态代理实现。(javasist/asm/cglib)javac是字节生成的祖宗。用到字节码生成(aop/jsp/proxy).
java.lang.reflect.Proxy或者实现过java.lang.reflect.InvocationHandler接口。spring做bean管理。 动态代理最大好处---在原始类和接口未知时确立了代理行为。
Proxy.newProxyInstance(ClassLoader,接口列表,InvocationHandler);
保存生成的类----System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
结果看到实现接口的方法中都调用了Invocationhandler的invoke方法。
(4)Retrotranslator:跨越jdk版本。只能翻译到之前的版本。
改进:编译器层面改进;对Java api增强;要在字节码中进行支持的改动;虚拟机内部改进。 这个工具只支持前两种。Enum当成普通类...
2.动手实现远程功能。
实现classloader--接收字节流。
实现System.out(新实现System----避免跟系统的混了)
实现classmodifier(修改java.lang.System变为实现的类)
实现工具类ByteUtils
实现执行类。
写Jsp。
四、程序编译与代码优化
(一)早期编译优化(javac)
1.javac(前端编译器) JIT /AOT
(1)javac源码与调试
javac源码在$JDK_SRC_HOME/langtools/src/share/classes/com/sun/tools/javac中。除了jdk代码就引用了JDK_SRC_HOME/langtools/src/share/class/com/sun/*里面的源码。建起来方便基本不用处理依赖关系。建立一个java工程把里面的代码考到里面。 AnnotationProxyMaker会提示access retriction.(由于eclipse jre system libarary默认包含了一系列范文规则,通过添加规则解决问题)点击包---edit.
---jvm定义了class文件格式,对java源码如何转变没有定义,与具体jdk实现相关。 编译的三个过程:
解析与填充符号表过程
插入式注解处理器的注解处理过程
分析与字节码生成过程
---三部分依存,基本前面依赖后面,注解处理的过程还会涉及解析与填充符号表过程。
javac动作入口com.sun.tools.javac.main.JavaCompiler类。(三个过程的代码在compile()和compile2()方法里)。最关键的处理由8个方法完成。
initProcessAnnotations(processors); 准备过程---初始化注解处理器。
delegateCompiler=processAnnotations( //执行注解过程。
enterTrees(stopIfError(CompilerState.PARSE, //输入到符号表
parseFiles(sourceFileObjects) , //词法语法分析
classnames
)
)
)
delegateCompiler.compile2();
case BY_TODO;
while(!todo.isEmpty())
generate(desugar(flow(attribute(todo.remove()))); 标注-数据流分析-解语法糖-生成字节码。
break;
(2)解析与填充符号表
----词法语法分析
parseFiles(sourceFileObjects)
词法分析:将源代码中的字符流转变为标记(token)集合,单个字符是编译过程最小元素 int a=b+2有6个标记。由com.sun.tools.javac.parser.Scanner类实现。
语法分析:token序列来构造抽象语法树(AST. abstract syntax tree)的过程,ast是描述程序代码语法结构的树形表示方式,语法树的每个节点都代表一个语法结构(Construct)--如包/类/修饰符/运算符/注释。Eclipse AST View插件描述代码抽象树。com.sun.tools.javac.parser.Scanner.Parser实现。产出的语法树由com.sun.tools.javac.tree.JCTree类表示。(生成它后编译器基本上不对文件操作了,以后的操作都在AST上)
---填充符号表(Symbol Table)
enterTrees()方法。符号表是由符号地址和符号信息构成的表格(可以想象成K-V形式,但是实现可以使有序符号表/树符号表/栈结构符号表)。符号表登记的信息在编译的不同阶段都要用到。语义分析中,符号表等级的内容用于语义检查(检查名字使用是否与说明一致)和产生中间代码。在目标代码生成阶段,对符号名地址分配时,是地址分配依据。com.sun.tools.javac.comp.Enter类实现。 过程的出口是一个待处理的列表(to do list),包含了每一个编译单元AST的*节点和package-info.java的*节点。【根据上面的AST,把符号的地址--符号名称用K-v表示,后面语法分析来检查】
(3)注解处理器
annotation与普通java一样在运行时发挥作用,jdk1.6 -jsr-269规范提供了插入式注解处理器api在编译期间对注解进行处理,可以看做一组编译器插件,可以读取、修改、添加AST中的任意元素【如果改了AST元素,编译器将回到解析及填充符号表的过程重新处理,知道没有注解处理器继续处理,一次循环成为一个Round】。
有了注解处理器可以做一些干涉编译器的行为,(语法树的任意元素在插件中都可以访问到),可以给插件功能上很大发挥空间。创意--可以用它完成只能在编码中完成事情。
initProcessAnnotations()方法,执行在processAnnotations()完成--判断是否有新的注解处理器,有的话通过com.sun.tools.javac.processing.JavacProcessingEnvironment类的doProcessing方法生成一个javaCompile对象对后续步骤处理。
(4)语义分析与字节码生成(获得了AST后,无法保证源程序符合逻辑)
语法分析:对结构上正确的源程序进行上下文性质的审查,int a =1; boolean b=false;char c=2; 只有int d=a+c; 正确,而int d=b+c;错误。
前两个步骤是语法分析。分别由attribute和flow方法完成。
---标注检查。检查:变量使用前是否被声明、变量与赋值间数据类型是否能匹配等。(有个重要的动作叫常量折叠---int a=1+2;变为int a=3;)-----ast中infix expression插入表达式。 实现由com.sun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check类完成。
--数据流及控制流分析。对上下文逻辑进一步验证,检查程序局部变量使用前是否赋值、方法的每条路径是否有返回值、是否有受检查的异常没有处理等问题。
编译时的数据流及控制流分析 与 类加载时数据流及控制流分析目的一致,但校验范围有区别,有些校验项只在编译或运行时期才进行。(如final修饰符分析,因为局部变量在字节码中没有修饰符,需要在编译时处理好,有无final的字节码结果是一致的。 局部变量的不变性仅提供编译时保障)具体实现com.sun.tools.javac.comp.Flow类完成。
---解语法糖(syntactic sugar).
语法糖对功能没有影响,只是方便程序员使用,减少出错机会。java属于低糖语言,最常用的语法糖是泛型、变长参数、自动拆箱装箱等。jvm运行不支持这些语法,编译阶段被还原为简单语法结构(解语法糖)com.sun.tools.javac.TransType类和com.sun.tools.javac.comp.Lower类完成。
--字节码生成。com.sun.tools.javac.jvm.gen完成。字节码生成不仅把前面步骤得到的信息(语法树和符号表)转化为字节码写到磁盘,编译器还进行了少量代码添加和转换工作。如<init>()方法。<clint>()方法===这些并不是构造函数,如果没有构造函数会添加空构造函数在填充符号表时完成。。init和clinit是代码收敛的过程,编译器会把static语句块、变量初始化、调用父类的实例构造器(不含clint,jvm会保证父类的clint先运行)等操作收敛到这里面。Gen.normalizeDefs()方法来实现。
除了这些还有代码替换优化实现,如字符串想加自动变为StringBuffer或者StringBuilder.
完成了对语法树的遍历和调整会包所有所需信息的符号表交到com.sun.tools.javac.jvm.ClassWriter类手上,由它的writeClass输出字节码,生成最终字节码文件。
2.java语法糖
(1)泛型与类型擦除。(泛型类、接口、方法),原来java只能通过Object和强制类型转换来实现。(可能会有ClassCastException). c#的泛型是真泛型。
java的泛型只在源代码中存在,编译后的字节码文件被替换为原生类型(raw type),相应地方加入了强制类型转换。(java中的泛型实现叫类型擦除,这种泛型是伪泛型).
public staitc void method(List<String> list){};
public staitc void method(List<Integer> list){}; 编译后都被替换为原生的List<E>,两个签名一样,无法重载。(但签名一样只是一部分原因)
public staitc String method(List<String> list){};
public staitc int method(List<Integer> list){}; 这就可以运行了,重载不是根据返回值确定,能编译执行成功的原因-----因为加入了不同返回值共存于一个class文件中。
泛型引入---(虚拟机解析/反射)等场景都对原有的基础产生影响和新的需求,如从泛型类中如何获取传入的参数化类型等。JCP对jvm规范做了修改,引入Signature和LocalVariableTypeTable新属性解决伴随泛型来的参数类型识别问题(signature是重要属性,作用存储一个方法在字节码层面的特征签名--含返回类型)。signature保存的参数类型不是原生类型、而是包含了参数化类型信息(49.0以上jvm都能识别)。
public staitc String method(List<String> list){};
public staitc int method(List<Integer> list){}; 只能通过不同返回值解决(不优雅和美感)结论:擦除法所谓的擦除只是对方法的Code属性中的字节码擦除,时间上元数据还保留了泛型信息,反射获取它的依据)
(2)自动装箱、拆箱、遍历循环(会被解析为Interator方式)
Integer.valueOf()/integer.intValue()放阿飞。 变长参数----调用时候变成了数组类型的参数。
==没有算术运算不会自动拆箱,equals也不会处理数据转型。(避免这样用装箱拆箱)
(3)条件编译。#ifdef(C/c++). java不用预处理器,因为编译器不是一个个的编译java文件,而是将编译单元的语法树*节点输入到待处理列表后进行编译,各个文件能互相提供符号信息)
if(true) syso() else{} ..编译后就只有syso(). 只有if后面的值是常量才有这个效果。 while(false){...}提示不能到达的错误(控制流分析,拒绝编译)
com.sun.tools.javac.comp.Lower类完成。
(4)其他语法糖。内部类/枚举类/断言语句/对枚举和字符串的switch支持(java 1.7),try中定义和关闭资源(jdk1.7).
3.注解式处理器
插入式注解处理器用处大。
(1)实战目标。CheckStyle/FindBug/Klocwork等工具,对java源码校验。
例子的目标检查书写规范。 (变量/类/接口/方法/字段)陀式命名法。
(2)代码实现。注解处理器实现一编译器插件。了解API知识。 处理器代码需要继承抽象类javax.annotation.processing.AbstractProcessor,这个类有一个必须覆盖的abstract方法process.(javac执行注解处理器代码弟阿勇过程)
process(annotations,roundEnv). 第一个参数---获取此注解处理器要处理的注解集合;第二个参数--roundenv中访问当前这个Round的语法树节点,每个节点为一个Element.(javax.lang.model.Element) Element包括了常用元素:package/ENUM/CLASS/annotation_type/interface/enum_constant/field/parameter/local_variable/exception_parameter/method/constructor/static_init/instance_init/type_parameter/other.
除了传入的参数还有一个protected变量processingEnv,有注解处理器初始化的时候创建继承了AbstrctProcessor代码可以访问它。(代表注解处理器的上下文环境/要创建的新代码/想编译器输出信息、获取其他工具类都要它)
注解处理器除了这些外还有两个配合的annotation . @SupportedAnnotationTypes和@SupportedSourceVersion. (前者代表对哪些注解处理器感兴趣*表示所有,后者表示哪些版本java代码) 每个注解处理器运行时都是单例的,如果不想要改变或生产语法树内容,process可以返回false,通知编译器这个Round没有变化,无需新的javaCompiler.
NameChcker代码长,通过继承javax.lang.model.util.ElementScanner6的NameCheckScannner类以visitor模式完成遍历。(分别visitType/visitVariable/visitExecutable完成)
(3)运行测试。 运行时添加注解处理器通过-processor参数指定。 -XprintRounds 和-XprintProcessorInfo参数查看注解处理器运作信息。
(4)其他实例。基于注解处理器的项目有用于校验hibernate标签使用正确性的Hibernate Validator Annotation Processor. 自动为字段生成getter和setter的Project Lombok.
(二)晚期编译优化.
1.Hotspot jvm内的即时编译器。
(1)解释器与编译器。何时解释?何时编译?
为什么并存?编译执行提高效率,解释执行省内存;解释执行也是编译器集锦优化的逃生门。
为什么两个即时编译器?client/server(c1和c2).可以自动选择,也可以手动指定(-server/-client). 可以手动指定解释执行-Xint. 可以用-Xcomp强制编译执行。
即时编译需要占用程序运行时间(编译出优化程度跟高代码,花费时间更长;编译优化高代码,解释器还要收集监控信息,影响速度)为了响应速度和运行效率最佳平衡,hotspot采用分层编译策略(1.7默认启用) 第0层-解释,第1层-c1,第2层-c2. 分层后c1/c2同时工作。
(2)编译对象与触发条件
哪些程序会编译成本地代码(如何编译)。(被多次调用的方法,被多次执行的循环体)---都是以整个方法编译整体,后一种情况叫栈上替换(on stack replacement. osr).
多次是多少次。(热点探测)-----两种方法:基于采样的热点探测(周期性检查栈顶,哪个方法出现的多就是热点方法,优点-简单高效,缺点-易受阻塞和外界影响);基于计数器的热点探测(为每个方法建立计数器,统计执行次数,方法严谨准确。缺点--实现麻烦,不能直接获取方法调用)。hotspot用的第二种(为每个方法维护了两个计数器--方法调用计数器和回边计数器)。
调用计数器(client-默认1500,server默认10000;可以通过-XX:CompileThreshold人工设定)。 编译器不同步等待编译器请求完成。(后台自动编译,完成后就可以用了)。方法调用计数器--是一段时间内调用的次数,超过一定时间还未提交会有一个热度衰减(时间--半衰周期)。-XX:-UseCounterDecay关闭热度衰减。
-XX:CounterHalfLifeTime设置半衰周期-单位秒。
回边计数器---(向后跳转指令就是回边)---目的为了出发OSR编译。 -XX:BackEdgeThreshold.(当前jvm未用),需要用-XX:OnStackReplacePercentage来简介调整。计算公式如下:
client: compileThreshold * osr比率/100. 默认的osr比率为933.都取默认值时回边技术器13995.
server compileThreshold * (osr比率-解释器监控比率)/100。 解释器监控比率-XX:InterpreterProfilePercentage(默认33).osr默认140,server回边计算的默认是10700.
回边计数器没有衰减过程。 当它溢出时会把方法计数器也调整到溢出,执行标准编译过程。
(3)编译过程
编译动作在后台执行,如果禁止-XX:-BackgroundCompilation禁止后台编译。(禁止后,编译时都等待)。
client-compiler--简单的三段式编译器。 平*立的前端将字节码构造成一种高级的中间代码(HIR). HIR用静态分配形式代表代码值--之中和之后的优化易于实现(方法内敛/常量传播)。
第二阶段:平台相关的后端从HIR中生成LIR。(另外优化-控制检查消除,范围检查消除)
第三阶段:平台相关后端用线性扫描算法,在LIR上分配寄存器,在LIR上做窥孔优化,产生机器码。
server-compiler.(执行所有经典优化):无用代码消除/循环展开/循环外提/常量传播/基本块重排序等。(不稳定优化-守护内敛/分支频率预测等)。
寄存器分配器是全局图着色分配器,充分利用处理器上的大寄存器集合。
(4)查看与分析即时编译结果。参数需要Debug版本和FastDebug版本的支持。-XX:printCompilation(打印编译的方法名称)
-XX:+PrintInlining 输出方法内连。
输出机器码(反汇编查看)。hsdis-amd64. -XX:printOptoAssembly 或者-XX:+print LIR(client)
-XX:+PrintCFGToFile (client) -XX:+PrintIdealGraphFile(server). 然后用java hostspot client compiler visualizer或者ideal graph visulizer查看。
2.编译优化技术。(优化不是在java代码上,为了展示方便用java语法表示)。
内敛好处:去除调用成本;为其他优化建立基础。 冗余访问消除/公共表达式消除。(分析不会变的代码直接z=y;公共的计算去掉)
复写传播,y=z,可以直接用y代替z;
无用代码消除:永远无法执行的代码,如y=y.
(1)公共子表达式消除(语言无关经典优化). 前面计算过,后面没有变化,还要计算时,直接用前值。(程序基本快内成为局部公共子表达式清除)。范围涉及多个基本快--全局公共字表达式消除。
(2)属组范围检查消除(语言相关优化)。即时编译的意向经典技术。
java访问元素会自动检查,每次判定是性能负担。是不是一次不漏检查可商量,分析上下文,将不需要的上下界检查去掉。
语言相关的其他消除:自动装箱消除/安全点消除/消除反射等。
(3)方法内敛。实现没那么简单,如果不是java做了努力,按照经典原理大多都不能内敛优化。方法解析和分派调用,只用结构invokespecial和invokestatic才是编译期间解析的,除了这些方法外可能有多台选择和多个版本的方法接受者。b.get不知道b的类型。java鼓励虚方法,与内敛锚段。
---优化方法:首先引入了类型继承关系分析(CHA确定目前加载的类中某方法是否有多于一种实现,是否存在子类,子类是否为抽象类信息)。
碰到虚方法----CHA查询,是否有多版本选择,只有一个版本,就用内敛(不过有点激进)---守护内敛。(准备好逃生门)
CHA有多个版本方法选择,用内敛缓存完成内敛,建立在方法之前的缓存。(原理:未发生调用,内敛缓存为空,一次调用后,缓存记录版本信息,每次调用都比较接受者版本,以后进来的方法还和缓存的一样,就一直用下去,不一致状态才取消内敛)
(4)逃逸分析(最前沿).分析对象动态作用域。(定义后外部方法引用---逃逸;或者外线程使用,线程逃逸)证明对象不会逃逸到方法或线程外就可以进行更进一步优化:栈上分配(不在堆,在栈上分配,不用回收,性能提高;同步消除;标量替换(聚合量拆散,按标量访问。)。
技术还不成熟。 确认有必要可以手动开启(费性能):-XX:+DoEscapeAnalysis.-XX:+PrintEscapeAnalysis.查看分析结果。-XX:+EliminateAllocations开启标量替换。.
-XX:+EliminateLocks开启同步消除。-XX:+PrintEliminateAllocations查看标量替换情况。
3.java与c/c++编译器对比。 即时编译器和静态编译器的对比。
性能劣势---jit占用户程序运行时间;类型安全语言,jvm确保程序不会违反语言语义或访问非结构化内存(频繁动态检查);java使用虚方法多;java可以动态扩展,可能改变继承关系;垃圾回收机制和内存分配。
五、高效并发
TPS(transaction per second)-多少是衡量服务器性能指标。硬件缓存优化,难保持缓存一致性;乱序执行。
(一).java内存模型与线程
1.java内存模型JMM。 JMM屏蔽掉Os/硬件访问内存差异,在此前(c/c++)直接用硬件的(os的)。
JMM目标:定义各个变量的访问规则,定义虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。变量:实例字段、静态字段、构成数组对象的元素【不包括局部变量与方法参数,线程私有】。
(1)主内存与工作内存:没有限制寄存器和缓存使用,也没有限制编译器调整代码执行顺序。
JMM规定所有变量都存于主内存中(类似于物理内存),每个线程有自己的工作内存(需要与主内存同步),工作内存是线程用到的变量的主内存拷贝,对变量的所有操作都在工作内存中执行,线程间不能互相访问工作内存。
(2)内存间的交互操作
lock--主内存变量
unlock--主内存变量
read-----主内存变量,读取主内存变量到线程工作内存,供Load用。
load--工作内存变量,read读到的放入工作内存的变量副本中。
use---工作内存变量,传递给执行引擎。
assign--工作内存变量,执行引擎值赋值给工作内存变量。遇到变量赋值的字节码执行的操作。
store---工作内存变量,传送工作工作内存变量到主内存。
write--作用主内存变量,接受store,写入主内存变量。
规则
----有序性:read-load, store-write.(必须按顺序执行,不需要连续),且不允许这4个指令单独出现
---不允许线程丢弃assign操作。
---不允许无原因的把线程数据同步到主内存。(如没有assign)
---新变量只能在主内存诞生。对主变量用use-store,必须执行过assign-load操作。
--只允许一个线程Lock,多次lock,只有同样多的unlock解锁。
--lock后,会清空工作线程的这个变量,重新load-assign.
---必须先Lock,才能unlock.
--unlock必须先同步变量到主内存。
(3)对于volatile型变量的特殊规则(轻量级的同步)
特性:保证可见性;抑制重排序。 可见性--一个线程修改了值,新值对其他线程立即得知,其他类型的变量必须主内存中转。
volatile线程安全不正确,虽不存在不一致问题(使用前都会先刷新),由于运算非原子,也不能保证安全。字节码分析不严谨(可以用-print Assembly分析)
volatile线程安全的情况只有:运算结果不依赖变量当前值或者能够确保只有单一的线程修改变量的值;变量不需要与其他状态变量共同参与不变约束。
禁止重排序优化:Java原本只能保证线程内串行语义,volatile避免线程间重排序优化。
性能:volatile读取速度跟普通变量相当,在写操作上会慢一些(会加入内存屏障,避免重排序)。
jmm对volatile的特殊规则。T----volatile变量:V/W。read/load/use/assign/store/write规则:
----线程T对V执行的前一个动作是load,线程T才能对V进行use,线程T对V的后一个动作是use的时候,线程T才能对变量V执行load。(use动作可认为与T对V的Load/read是相关连的,连续出现的)
----只有当前线程的前一个动作是assign,T才能对变量store.(store的前一个也是assign)-----------连续。
---A是线程T对变量V实施的use/assign操作,F是与A关连的load或store操作,P是与F关连的对V的read/write;B/G/Q--对w . 那么A先于B,P必定限于Q。
(4)对于long/double的特殊规则。允许对没有volatile修饰的这两个类型的变量分两次32位操作。
(5)原子性、可见性与有序性。JMM是围绕如何保证这三个特性保证的。
原子性--直接原子保证操作----read/load/assign/use/store/write. (对基本类型的都写是原子的). 更大原子保证需要lock/unlock.未把这两个操作直接提供使用,提供了高层次的monitorenter/monitorexit.(synchronized)
可见性--一个线程修改,其他线程能否立即可见(都是通过与主内存做为传递媒介)。 保证可见性的关键字:volatile/final/synchronized.
有序性---本线程观察操作有序,其他线程观察操作无序。volatile和synchronized提供有序保证。
(6)先行发生原则。(以是否存在数据竞争为依据) ,通过这写原则判断是否存在冲突可能。(内存模型的偏序关系) A先行于B,A的结果会在B之前看到。没有先行有序,就可以对代码重排序。
--程序次序原则(线程内)
--管程锁定原则(unlock先行发生于后面对同一个锁的lock操作。先后是时间先后)
--volatile变量规则。写操作发生于对这个变量的读操作。
--线程启动规则。start()发生于线程的其他操作。
--线程终止规则。其他操作发生于终止监测前(Thread.join()/Thread.isAlive()).
--线程中断规则. interrupt先行发生于中断监测。
--对象终结规则.对象初始化完成先于Finalize()
--传递性。A-》B B-》 C a->c.
时间上先后顺序与先行发生原则没什么关系。
2.java与线程。
(1)线程的实现。线程引入可以将资源分配和执行调度分开,java线程的实现依赖本地操作系统。
主流的实现方式有:
使用内核线程实现(程序一般不会直接用内核线程,而用LWP轻量级进程,缺点:系统调用代价高-内核态和用户态转换;轻量级进程要有内核线程支持--耗费内核资源);
使用用户线程实现(用户线程的使用在用户态内完成,效率高。优势:不需要内核支持,劣势-所有线程操作都自己实现)
混合实现。(多对多线程模型---soloris/hp-ux) --内核线程只作为桥梁。
java线程实现。(1.2前是用户线程,后来,操作系统线程) 。在windows/linux---1对1,solories 多对多。
(2)java线程调度. 主流的调度(协同式和抢占式)。协同式-线程执行时间由线程本身控制,线程执行完了后主动切换到另一个线程(好处是实现简单,lua是这样的)。缺点:线程执行时间不可控制,可能阻塞。
抢占式:线程由系统来分配执行时间,线程切换不由线程本身决定(thread.yield让出执行时间),执行时间是系统可控,不会导致整个阻塞java是这种。优先级--10个级别(但是不太靠谱)。
(3)状态切换:新建、运行、无限期等待wait()/join()、限期等待(带timeout)、阻塞、结束。
(二).线程安全与锁优化
1.线程安全
定义:多线程访问对象,如果不考虑线程运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何形式其他协调,调用这个对象的行为都可以获得正确的结果,这个对象是线程安全的).
[弱化定义,----单次调用安全即可]
(1)java语言的线程安全。各种操作共享数据的类别:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。
不可变:不可变对象一定先行安全。(对象行为不影响自己状态途径有多种--简单所有的状态变量都声明为final.) java不可变:String/Enum/Number部分子类/BigDecimal/BigInteger. atomicInteger和atomicLong是不可变的。
绝对线程安全--几乎没有。
相对线程安全--vector/HashTable/synchronizedCollections()等。
线程兼容---本身不安全,通过正确使用同步保证。大部分累都是。arrylist/hashmap.
线程对立--不管是否采取同步,都无法在多线程用。thread类的resume/suspend方法。
(2)线程安全的实现方法
--互斥同步。
临界区(critical section)、互斥量(mutex)、信号量(semaphore)是主要实现方式。(互斥是因,同步是果,互斥是方法,同步是目的)
最常用:synchronized.(都需要reference类型参数指明加解锁对象)。锁计数器(多次进入会递增) 可重入;进入的线程执行完前会阻塞。借用的os---状态转换时间很长。
还有:java.util.concurrent包中 reenterantlock.主要有三个高级功能(等待可中断、公平锁、锁定多个条件)
--非阻塞同步:指令集和硬件的发展实现(测试并设置;获取并增加;交换;比较并交换). sun.misc.Unsafe类中compareAndSwapInt()... JUC包里的整数原子累都用了它们。
无法适应所有场景,语义漏洞(ABA问题,通过带有标记的原子类解决)。
---无同步方案
可重入代码。纯代码--状态都有参数传入、不调用非可重入方法。
线程本地存储。ThreadLocal类实现。
2.锁优化
(1)自旋锁和自适应自旋。(os切换状态浪费性能。1.6默认开启) 自旋锁有一定时间,没等到就瓜期线程。默认自旋次数-10次,可以通过-XX:preBlockSpin来更改。
1.6引入自适应自旋锁,--自旋时间不固定,由上次自旋时间和锁拥有者状态来决定。【上次获取过-自旋等待时间长,自旋很少成功就直接不自旋】
(2)锁消除:主要是逃逸分析的数据支持。(不会逃逸,不会被其他线程访问,去掉锁). ----系统里有大量的锁定方法。(即便知道单线程,但无法改系统库).
(3)锁粗化:连续调用同步方法,通过粗化,只做一次锁定。
(4)轻量级锁。虚拟机对象头部分-两部分。 第一部份---存储对象自身运行数据,(hashcode/gc粉黛);存储方法去对象类型的数据的指针. markword的32位----存储hashcode-25/4bit-分代年龄/2bit-锁标志位/1 bit固定0.
进入对象内存布局---没有锁定(01状态)--虚拟机在当前线程栈桢建立所记录,存储所队形目前的Mark word拷贝。 用cas将要锁定对象的Mark word改为lock record的指针。(更新成功了表示有了锁,markword的标志为改为00) 。如果cas失败,先检查,如果已经指向了当前栈桢表示第二次进入。两个以上线程争用,就会变成互斥锁。
(5)偏向锁1.6引入。(消除数据无竞争情况下的同步原语). (偏向第一个获取它的线程)-XX:+UseBiasedLocking. 另一个线程再获取就表示失败。
总结:需要继续学习的东西jndi,osgi.