赞
踩
蚍蜉叹:这本书很经典,Java开发的一定要自己看看书本,有兴趣和精力的甚至可以看看源代码。这篇读书笔记实际上只能作为复习和参考查找用。
第一部分 走进Java
JDK三部分:Java程序设计语言、Java虚拟机、Java API类库。
JRE两部分:Java SE子集、Java虚拟机。
Jdk7源码下载地址:https://jdk7.java.net/source.html
编译jdk源码,并用IDE进行调试,没有进行尝试……
第二部分 自动内存管理机制
Java与c++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”。
Java虚拟机运行时数据区:
由所有线程共享的数据区:方法区、堆;
线程隔离的数据区:虚拟机栈、本地方法栈、程序计数器;
程序计数器(Program Counter Register):一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器在任何一个时刻都只能执行一条线程中的命令。
Java虚拟机栈(Java Virtual Machine Stacks):生命周期与线程相同,Java方法执行的内存模型,每个方法在执行的都收都会建立一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。规定了两种异常状况:如果线程请求的栈深度大于Java虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以扩展,扩展时无法申请到足够的内存,就会抛出OutOfMemoryEorror异常。
局部变量表:存放了编译期可知的各种基本数据类型(8种)、对象引用类型(引用指针或句柄或其他相关位置)、returnAddress类型(指向一条字节码指令的地址)。64位的long和double类型占用2个局部变量空间Slot,其余占用1个Slot。局部变量表所需的内存空间在编译期间完成分配,运行期间大小不会改变。
本地方法栈(Native Method Stack):为Native方法服务,与Java虚拟机栈类似,但无规定,可自由实现,如Sun HotSpot虚拟机就直接将本地方法栈和虚拟机栈合二为一。
Java堆(Java Heap):Java虚拟机所管理的内存中最大的一块,线程共享,在虚拟机启动时创建,唯一目的是存放对象实例,几乎所有的对象实例都在java堆中分配,但不是绝对的。Java堆是垃圾收集器管理的主要区域,因此也成为GC堆。从内存收集角度看:由于现在收集器基本采用分代收集算法,故而堆还分为新生代和老年代,再细致一些有Eden空间、From、Survivor空间、To Survivor空间等。从内存分配角度看:线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。划分是为了更好地回收内存或者更快地分配内存。Java堆可以在物理上不连续的内存空间中,只要逻辑上是连续的即可,大小可固定可扩展,主流的按可扩展来实现的,如果堆中没有内存完成实例分配,并且堆也无法再扩展,将抛出OutOfMemoryError异常。
方法区(Method Area):线程共享,用于用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(Just-in-time compiler,JIT编译器)编译后的代码等数据,别名为Non-Heap(非堆),以区别Java堆,也有称为“永久代”的(习惯HotSpot的,实际是使用永久代来实现方法区,永久代有-XX:MaxPermSize的上限)。方法区限制宽松,不需要连续内存、大小可固定可扩展、还可以选择不实现垃圾收集。无法满足内存分配需求时抛出OutOfMemoryError异常。
运行时常量池(Runtime Constant Pool):方法区的一部分。Class文件中除了有类的版本、字段、方法、接口、等描述信息外,还有一项是信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用。Java虚拟机对Class文件的每一部分的格式都有严格要求,但是运行时常量池则没有任何细节要求,具有动态性,运行时也可返给常量。有OutOfMemoryError异常。
直接内存(Direct Memory):不属于Java虚拟机。JDK1.4中新加入NIO类,引入了一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存然后通过一个在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在Java堆和Native堆中来回复制数据,因此在一些场景中能显著提高性能。
对象的创建(普通对象,不包括数组和Class对象等):虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的是否已被加载、解析和初始化过,如果没有,那么必须先执行相应的类加载过程;在类加载通过后,接下来虚拟机将为新生对象分配内存,对象所需内存大小在类加载后便可完全确定。内存分配完后,虚拟机需要将分配的内存空间都初始化为零值(不包括对象头),如果使用TLAB这一工作也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋值就直接使用,程序能够访问到这些字段的数据类型所对应的零值。接下来,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息放在对象头中。至此,从虚拟机角度看,一个新的对象已经产生了,但是从Java程序的角度看,对象创建才刚刚开始,init方法还没有执行,所有的字段都还为零,所以一般来说(由字节码中是否跟随invokespecial指令决定),执行new指令后会接着执行init方法,把对象按照程序员的医院进行初始化,这样一个真正可用的对象才算完全产生出来了。
指针碰撞(Bump the Pointer):假设Java堆中内存是规整的,用过的内存和空闲的内存分别放两边,中间放一个指针作为分界点指示器,那么内存分配仅仅是把指针向空闲那边移动一定距离,这种分配方式成为“指针碰撞”。
空闲列表(Free List):Java堆内存不是规整的,虚拟机维护一个表,记录哪些内存是可用的,分配的时候找到一块足够大的空间分配出去,并且更新表中的记录,这种分配方式成为“空闲列表”。
选择哪种方式由Java堆内存是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带有Compact过程的收集器时,系统采用指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
内存分配时并发情况下的线程安全问题:一种是堆分配内存空间的动作做同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,成为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),TLAB用完并且分配新的TLAB时才需要同步锁定。虚拟机是否采用TLAB可以通过-XX:+/-UserTLAB参数来设定。
对象的内存布局:在HotSpot虚拟机中,对象在内存中存储的布局分为:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。对象头两部分:第一部分用于存储对象自身的运行时数据,另一部分是类型指针,即对象指向它的类元数据的指针(元数据可以确定Java对象大小),虚拟机通过这个指针来确定这个对象是哪个类的实例,如果对象是Java数组,还需要一块用于记录数组长度的数据。实例数据是对象真正存储的有效信息,即各种类型的字段内容,不论父类继承下来的还是在子类定义的都要记录起来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles, ints, shorts/chars, bytes/Booleans, oops(Ordinary Object Pointers),可以看出相同宽度的字段被分配在一起,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。 如果CompactFields参数值为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变量的空隙之中。对齐填充并不是必然存在的,仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。
对象的访问定位:主流的有使用句柄、直接指针两种。如果使用句柄访问的话,Java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。句柄访问好处是reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变。使用直接访问最大的好处是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,累积后相当可观。HotSpot采用直接指针访问。
引用计数法(Reference Counting):很难解决对象之间相互循环引用的问题。
可达性分析算法(Reachability Analysis):从GC Roots不可达对象判定为可回收。可作为GC Roots的对象:虚拟机栈(栈帧中的本地变量表)中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI(即一般说的Native方法)引用的对象。
引用分为:强引用(不被回收)、软引用(有用但非必须)、弱引用(非必须、只能生存到下一次垃圾回收之前)、虚引用(也称幽灵引用或幻影引用,对象被回收时收到系统通知)。
可达性分析中不可达的对象被第一次标记后会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过了,虚拟机将这两种情况都视为没有必要执行,如果被判定为有必要执行,则对象会放在一个F-Queue的队列中,并在稍后由一个虚拟机自动建立的、优先级低的Finalizer线程去执行它,执行指的是虚拟机会触发它,但并不承诺会等待它运行结束,如果一个对象在finalize()方法中执行缓慢或者死循环将可能使其他F-Queue对象永久处于等待,甚至整个内存回收系统崩溃,而finalize()方法中对象可以通过与引用链上任何对象建立关联来拯救自己不被回收。任何对象的finalize()方法都只会被系统自动调用一次,finalize()方法运行代价高、不确定性大、无法保证各个对象的调用顺序,应该避免使用。
回收方法区:可以理解为永久代回收,在堆中尤其是新生代,常规应用进行一次垃圾回收一般可以回收70%-95%的空间,而永久代的垃圾收集效率远低于此。永久代回收内容:废弃常量和无用的类。废弃常量没有引用则回收。无用类的判断条件则严苛很多需要满足:该类所有的实例都已经被回收,加载该类的ClassLoader已经被回收,该类对应的java.lang.Class对象没有任何地方被引用无法在任何地方通过反射访问该类的方法。而且满足条件后仅仅是“可以”回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。在大量使用反射、 动态代理、 CGLib等ByteCode框架、 动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
标记-清除算法(Mark-Sweep):最基础的垃圾收集算法,很多其他方法是基于此改进的。不足:标记和清除的效率都不高,标记清除后会产生大量不连续的内存碎片。
复制算法(Copying):将内存按容量分为大小相等的两块,每次用一块,一块内存用完了活着的对象就复制到另一块,把用过的一般内存全部清除,实现简单运行高效,但是内存缩小为原来的一半了。现在的商用虚拟机都采用这种算法来回收新生代,IBM公司的专门研究表明,新生代的对象98%是朝生夕死的,所以并不需要1:1比例来分配空间,而是分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间,然后清理Eden和用过的Survivor。HotSpot虚拟机默认Eden和Survivor比例为8:1,这样只有10%的内存空间会被浪费,当Survivor空间不够用时需要依赖其他内存(这里指老年代)进行分配担保。
标记-整理算法(Mark-Compact):标记后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法(Generational Collection)(当前商业虚拟机的垃圾收集算法):根据对象的存活周期不同将内存划分为几块,一般把Java堆分为新生代和老年代,新生代用复制算法,老年代用“标记-清除”或“标记-整理”算法。
HotSpot的算法实现(对象存活判定算法和垃圾收集算法):
枚举根节点:准确式GC需要停顿所有Java执行线程(Sun称其为Stop The World),即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点也是必须要停顿的。OopMap数据结构辅助完成HotSpot。
安全点:程序执行时可以停顿下来进行GC的位置,选定标准:是否具有让程序长时间执行的特征。
安全区域:一段代码中,引用关系不会发生变化。
七种垃圾收集器:连线的可以搭配使用,没有一种收集器是任意场景都可用的。
Serial收集器:单线程收集器,一个CPU或者一条收集线程,收集时其余线程需要停止。简单高效。是Client模式下虚拟机的默认新生代收集器。
ParNew收集器:Serial收集器的多线程版本,其余与Serial收集器完全相同。是许多Server模式下虚拟机首选的新生代收集器。在单CPU环境中绝对不会比Serial收集器有更好的效果,甚至由于存在线程交互的开销,超线程技术实现的两个CPU下都不能保证百分之百超过Serial收集器。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程和垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行在另一个CPU上。
Parallel Scavenge收集器:新生代收集器,使用复制算法,并行的多线程收集器。关注目标为一个可控制的吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集时间))。提供GC自适应调节策略。
Serial Old收集器:Serial收集器的老年代版本,与PS Mark Sweep实现很接近。
Parallel Old收集器:Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS收集器(Concurrent Mark Sweep):一种以获取最短回收停顿时间为目标的收集器。分为四个步骤:初始标记,并发标记,重新标记,并发清除。其中初始标记和重新标记仍然需要Stop the world。初始标记仅仅是标记GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间用户程序继续运行而导致标记产生变动的那一部分的标记记录,这个阶段的停顿时间一般会比初始标记时间长,但是远比并发标记的时间短。也成为并发低停顿收集器(Concurrent Low Pause Collector)。缺点:对CPU资源非常敏感,无法处理浮动垃圾(Floating Garbage)可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生,会有大量空间碎片产生。
G1收集器(Garbage First):面向服务端应用,并行与并发,分代收集,空间整合,可预测的停顿。收集范围以大小相等的独立区域为准,可以有计划地避免在整个Java堆中进行全区域的垃圾收集,使用Region划分内存空间并按优先级回收。不计算维护Remember Set操作,步骤分为:初始标记,并发标记,最终标记,筛选回收(Live Data Counting and Evaluation)。初始标记:标记GC Roots能直接关联到的对象,修改TAMS(Next Top at Mark Start)值,让下一阶段的用户程序可以并发执行,能在正确可用的Region中创建新对象, 这个阶段需要停顿线程,但时间很短。并发标记:从GC Root开始对堆中对象进行可达性分析,找出存活对象,这个过程较长,并发执行。最终标记:修正正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分的标记记录,虚拟机将这段时间对象变化记录在线程Remember Set Logs里面,然后整合到Remember Set中,需要停顿线程,但是可以并发执行。筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,可以并发执行。
理解GC日志:
33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs]3324K->152K(11904K),0.0031680 secs]
1 0 0.6 6 7:[F u l l G C[T e n u r e d:0 K->2 1 0 K(1 0 2 4 0 K),0.0 1 4 9 1 4 2 s e c s]4603K->210K(19456K),[Perm:2999K->
2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]
最前面的“33.125”和“100.667”代表GC发生的时间,为虚拟机启动以来经过的秒数,[GC [Full GC表示这次垃圾收集的停顿类型,Full说明这个GC发生了Stop the world,如果是调用了System.gc()方法触发的收集将显示[Full GC(System)。接下来的[DefNew、[Tenured、[Perm表示发生的区域。Serial收集器中新生代为Default New Generation所以显示为[DefNew,如果是ParNew收集器则新生代为[Parnew,Parallel Scavenge收集器新生代则为[PSYoungGen。后面方括号内部的“3324k->152k(3712k)”表示“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”,方括号之外的“3324k->152k(11904k)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”,再往后0.0025925secs表示该内存区域GC使用时间,单位为秒。
内存分配与回收策略:几条普遍的内存分配规则(Serial/Serial Old收集器下):对象优先在Eden分配,Eden无足够空间将发起一次Minor GC;大对象(需要大量连续内存空间典型为很长的字符串和数组)直接进入老年代;长期存活的对象将进入老年代(每经过一次Minor GC年龄加1,一定程度默认为15后晋升到老年代);动态对象年龄判定;空间分配担保,老年代最大可用连续空间是否大于新生代所有对象总空间。
新生代GC(Minor GC)频繁速度快,老年代GC(Major GC / Full GC)伴随至少一次Minor GC速度比Minor GGC慢10倍以上。
给一个系统定位问题时,知识经验是关键基础,数据是依据,工具是运用知识处理数据的手段,数据包括:运行日志、异常堆栈、GC日志、线程快照(thread dump/java core文件)、堆转储快照(heap dump/h prof文件)。
jps(JVM Process Status Tool):虚拟机进程状况工具,列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID。-l、-q、-m、-v。
jstat(JVM Statistics Monitoring Tool):虚拟机统计信息监视工具,用于监视虚拟机各种运行状态信息的命令行工具,显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译器等运行数据。-gc、-class等。
jinfo(Configuration Info for Java):Java配置信息工具,实时地查看和调整虚拟机各项参数。
jmap(Memory Map for Java):java内存映像工具,用于生成堆转储快照(一般为heapdump或dump文件),还可以查询finalize执行队列、Java堆和永久代的详细信息。
jhat(JVM Heap Analysis Tool):虚拟机堆转储快照分析工具,与jmap搭配使用,不过一般不直接使用jhat,因为分析工作是一个耗时而且耗资源的并且有更多更好的分析工具。
jstack(Stack Trace for Java):Java堆栈跟踪工具,用于生成虚拟机当前时刻的线程快照,主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待。
HSDIS:JIT生成代码反汇编,生成汇编代码以及一些有价值的注释。
JDK的可视化工具:JConsole和VisualVM。
JConsole(Java Monitoring and Management Console):一种基于JMX的可视化监视、管理工具,管理部分是针对JMX Mbean进行管理。
VisualVM(All-in-One Java Troubleshooting Tool):多合一故障处理工具,随JDK发布的功能强大的运行监视和故障处理程序,优点:不需要被监视的程序基于特殊Agent运行,因此对应用程序的影响很小,使得它可以直接用于生产环境。这个优点是JProfiler、YourKit无法与之媲美的。
第三部分 虚拟机执行子系统
Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8字节以上空间的数据项时,则会按照高位在前的方式分割成若干8位字节进行存储。类似结构体的伪结构,只含有无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8代表1、2、4、8个字节的无符号数,无符号数可以描述数字、索引引用、数量值、或者按照UTF-8编码构成字符串值。
表是由多个无符号数或其他表作为数据项构成的符合数据类型,所有表都习惯性地以“_info”结尾。整个Class本质上就是一张表。
魔数(Magic Number):每个Class文件头4个字节,唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。使用魔数而不用文件扩展名是因为扩展名可以改。Class文件魔数值为:0xCAFEBABE。
Class文件版本号:紧接在魔数的4个字节,第5、6字节是次版本号(Minor Version),第7、8字节是主版本号(Major Version)。JDK向下兼容以前版本的Class文件。
常量池:主次版本号之后是常量池入口,常量池可以理解为Class文件中的资源仓库。constant_pool_count为常量数量,从1开始计数。存放:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近Java语言层面的常量,如文本字符串、声明为final的常量值等,而符号引用则属于编译原理方面的概念,包括:类和接口的全限定名(形如org/fenixsoft/clazz/TestClass;),字段的名称和描述符,方法的名称和描述符。(动态链接,类创建或运行时解析、翻译到具体的内存之中)
14种表的第一位都是u1类型的标志位tag,代表当前这个常量属于哪种常量类型。14种表各自有自己的结构。书中有展示。
访问标志:用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是类是否被声明为final等。access_flags中一共有16个标志位可以用,但是当前只定义了8个,
类索引(this_class)用于确定这个类的全限定名,父类索引(super_class)用于确定这个类父类的全限定名,接口索引(interfaces)包含u2类型的接口计数器。
字段表集合(field_info):用于描述接口或类中声明的变量,包括类级变量和实例级变量,但不包括方法内部声明的局部变量。descriptor_index为描述符,描述字段的数据类型、方法的参数列表(数量、类型和顺序)和返回值。
方法表集合:结构和字段表一样,方法里的代码放在方法属性表集合中一个叫Code的属性里面。如果父类方法在子类里没有重写就不会出现在方法表集合中,但编译器可能自动添加方法如类构造器和实例构造器方法。重载一个方法,除了要与原方法相同的简单名称之外还要有不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不包含在特征签名中,所以Java语言无法仅仅依靠返回值的不同来对一个方法进行重载。在Class文件格式中,特征签名的范围更大一些,只要是描述符不是完全一致的两个方法也可以共存,也就是说仅仅是返回值不同也可以在Class文件中共存。
属性表(attribute_info)集合:Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。限制不那么严格,最具可扩展性,只要不与已有属性名重复,属性值得结构是完全自定义的,但是需要一个u4长度属性说明属性值占用的位数。
Code属性:方法体中的代码编译后的字节码,接口或者抽象类方法不存在Code属性,
attribute_length表示属性值的长度(整个属性表长度减去6字节的,6字节为属性表名称索引和属性长度和),max_stack为操作数栈深度的最大值,max_locals为局部变量表所需要的存储空间(单位为Slot,byte、char、float、int、short、boolean、returnAddress等长度不超过32位的数据类型占用1个Slot,double、long为64位数据类型占用2Slot),code_length和code用来存储java源程序编译后生成的字节码指令(可有256条指令,目前定义了200条,虚拟机规范中限制一个方法不能超过65535条字节码指令,某些特殊情况如复杂的JSP内容和页面输出会编译到一个方法中可能导致编译失败)。
Exception属性:列举出方法中可能抛出的受查异常(Checked Exception)。
LineNumberTable属性:用于描述Java源代码行号和字节码行号(字节码的偏移量)之间的对应关系。不是运行时必须的属性。
LocalViriableTable属性:用于描述栈帧中局部变量表中的变量与Java源代码定义的变量之间的关系,不是运行时必须的。
SourceFile属性:用于记录生成这个Class文件的源码文件名称。可选。
ConstantValue属性:通知虚拟机自动为静态变量赋值。Static变量才能有这个属性。
InnerClasses属性:记录内部类与宿主类之间的关联。
Deprecated属性:是否已经被程序作者定为不再推荐使用。
Synthetic属性:表示此属性或方法不是由Java源码直接产生的,而是编译器添加的。
StackMapTable属性:虚拟机类加载的字节码验证阶段被新类型检查验证器使用。
Signature属性:记录泛型类型签名信息。
BootstrapMethods属性:用于保存invoke dynamic指令引用的引导方法限定符。
字节码指令简介:由一个字节长度的代表着某种特定操作含义的数字(操作码)以及跟随其后的零至多个代表此操作所需参数(操作数)而构成。由于Java虚拟机采用面向操作数而不是寄存器的架构,所以大多数指令不包含操作数,只有一个操作码。加载和存储指令(将数据在栈帧中的局部变量表和操作数栈之间来回传输),运算指令(两个操作数栈上的值运算后重新存入栈顶,分为整型和浮点型),类型转换指令,对象创建和访问指令,操作数栈管理指令,控制转移指令,方法调用和返回指令,异常处理指令(现在用异常表来完成),同步指令。
虚拟机的类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。类加载在运行时完成。
类的生命周期:加载、验证、准备、解析、初始化、使用、卸载,7个阶段。其中验证准备解析统称为连接。这些阶段按顺序开始,但是可能互相交叉混合进行着。
类加载的时机:1、遇到new、get static、put static、invoke static这四条指令时。如果类没有进行过初始化,则需要先触发其初始化,常见场景:new实例化对象、读取或设置一个类的静态字段(final修饰和编译期把结果已经放入常量池的静态字段除外),调用一个类的静态方法时。2、使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行过初始化则需要先触发其初始化。3、当初始化一个类时,如果发现其父类没有初始化,则触发父类初始化。4、虚拟机启动时,用户需要指定一个主类,先初始化主类。5、如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。所有引用类的方式都不会触发初始化,称为被动引用。(通过子类引用父类静态字段只初始化父类,通过数组定义来引用类不进行初始化)接口初始化时不要求其父接口全部都完成初始化,只有在真正使用到父接口时(如引用接口定义的变量)才会初始化。
加载:通过类的全限定名来获取定义此类的二进制字节流(开发人员可控性最强,类加载器),将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的java.lang.Class对象作为方法区这个类各种数据的访问入口。不一定要从Class文件获取,还可以从Zip包中读取如Jar、Ear、War,从网络获取如Applet,运行时计算如动态代理技术java.lang.reflect.Proxy,由其他文件生成如JSP,从数据库中读取如SAP Netweaver。
验证:确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全(恶意代码)。大致4个阶段:文件格式验证、元数据验证、字节码验证(最复杂阶段,数据流和控制流分析,确保程序语义合法合逻辑)、符号引用验证(确保解析动作能正常进行)。重要但不必要。
准备:正式为类变量分配内存并设置变量初始值得阶段。
解析:虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用Symbolic References以一组符号描述所引用的目标,可以是任何形式的字面量。直接引用Direct References是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄,与内存布局相关。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号引用进行。
初始化:类加载最后一步,真正执行类中定义的Java程序代码(字节码)。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块的语句合并产生的,顺序由语句在源文件中出现顺序决定,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量可以赋值但是不能访问。()方法与类的构造函数()方法不同,它不需要显示调用父类构造器,虚拟机会保证父类()方法先执行。接口则使用父接口变量是才会调用()方法,接口实现类初始化不会执行接口()方法。虚拟机会保证一个类的()方法在多线程环境中被正确加锁同步,只有一个线程去执行。
类加载器:在类层次划分、OSGi、热部署、代码加密等领域大放光彩。启动类加载器(由C++语言实现,是虚拟机自身一部分),扩展类加载器,启动程序类加载器。
运行时栈帧结构:栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,存储了方法的局部变量表、操作数栈、动态链接、方法返回地址和一些额外附加信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。一个线程的方法调用链可能很长,很多方法同时处于执行状态,活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与之关联的方法称为当前方法。
局部变量表:一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,容量以变量槽Variable Slot为最小单位(未规定大小)。局部变量表中的Slot是可以重用的。
操作数栈:
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。类加载时符号引用转换为直接引用是静态解析,另一部分在每一次运行时期转化为直接引用成为动态链接。
方法返回地址:方法退出有正常完成出口额异常完成出口。
方法调用:不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时不涉及方法内部的具体运行过程。编译器可知运行期不可变的方法有静态方法和私有方法两大类。解析,分派,动态类型语言支持。
第四部分 程序编译与代码优化
前端编译器:.java文件变成.class文件的过程(Sun的Javac,Eclipse JDT中的增量式编译器ECJ)。后端运行期编译器JIT编译器:字节码转化为机器码(HotSpot VM的C1、C2编译器)。静态提前编译器:AOT编译器,直接把*.java文件编译成本地机器代码的过程(GNU Compiler for java(GCJ),Excelsior JET)。
Javac编译器:解析与填充符号(词法语法分析、填充符号表),注解处理器,语义分析与字节码生成(标注检查与常量折叠、数据及控制流分析、解语法糖、字节码生成)。
解释器与编译器并存:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行;程序运行后,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后可以获得更高的执行效率;当程序运行环境中内存资源限制较大可以使用解释执行节约时间,反之可以用编译器来提升效率;同时,解释器还可以作为编译器激进优化时的一个“逃生门”。
HotSpot虚拟机内置两个即时编译器:Client Compiler和Server Compiler,也称C1编译器和C2编译器。使用哪个编译器取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,也可以用-server或-client强制指定。
混合模式(Mixed Mode):解释器和编译器搭配使用的方式,也可以强制指定-Xint为解释模式,-Xcomp为编译模式。
分层编译策略:第0层:程序解释执行,解释器不开启性能监控功能,可触发第1层编译。第1层:也称C1编译,将字节码编译为本地代码,进行简单可靠的优化,如有必要将加入性能监控的逻辑。第2层(或2层以上):也称C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。实施分层编译后,Client Compiler和Server Compiler将会同时工作,许多代码可能会被多次编译,用Client Compiler获取更高的编译速度,用Server Compiler来获取更好的编译质量,在解释执行的时候也无须再承担性能监控信息的收集任务。
在运行过程中会被即时编译器编译的“热点代码”(Hot Spot Code)有两类:被多次调用的方法,被多次执行的循环体。都以整个方法作为编译对象,这种编译方式因为发生在方法执行过程中,因此形象地称之为栈上替换(On Stack Replacement,OSR编译)。
热点探测:判断一段代码是不是热点代码,是否需要触发即时编译。分为两种:基于采样的热点探测(周期性检查各个线程栈顶某个方法是否经常出现,简单高效但无法确定热度)和基于计数器的热点探测(为每个方法建立计数器统计执行次数用阈值判断)。其实还有其他的如:基于踪迹的热点探测。
HotSpot热点探测:使用基于计数器的热点探测,方法调用计数器和回边计数器(循环体)之和与阈值比较(Client模式下1500次Server模式为10000次)。
方法调用计数器热度的衰减:一定时间后方法的调用次数仍然不足以提交给即时编译器,方法计数器减半,时间称为半衰周期。
Client Compiler编译:主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段。第一阶段:一个平台独立的前端将字节码构成一种高级中间代码表示(High-Level Intermediate Representation,HIR),HIR使用静态单分配(Static Single Assignment,SSA)的形式来代表代码值。在此之前编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等优化将会在字节码被构造成HIR之前完成。第二阶段:一个平台相关的后端从HIR中产生低级中间代码表示LIR,而在此之前会在HIR上完成另一些优化,如空值检查消除、范围检查消除等,以便让HIR打到更高效的代码表示形式。第三阶段:在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窥孔(Peephole)优化,然后产生机器代码。
Server Compiler编译:专门面向服务端的典型应用并为服务端的配置特别调整过的编译器,也是一个充分优化过的高级编译器,几乎能达到GNU C++编译器使用-O2参数时的优化强度,会执行所有经典的优化动作,如无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重组排序等,还会实施一些与Java语言密切相关的优化技术,如范围检查消除、空值检查消除等,另外还可能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,如守护内联、分支频率预测等。Server Compiler的寄存器分配器是一个全局图着色分配器,它可以充分利用某些处理器架构(如RISC)上的大寄存器集合。以即时编译标准看,Server Compiler是比较缓慢的,但是依然远远超过传统的静态优化编译器,比Client Compiler编译输出的代码质量高,可以减少本地代码执行时间,也有很多非服务端的应用选择使用Server模式的虚拟机运行。
逃逸分析:为其他优化手段提供依据的分析技术,基本行为就是分析对象动态作用域。方法逃逸:当一个对象在方法中定义后可能被外部方法所引用,例如作为调用参数传递到其他方法中。线程逃逸:当一个对象在方法中定义后可能被外部线程所引用,例如赋值给类变量或可以在其他线程中访问的实例变量。基于此的优化如:栈上分配、同步消除、标量替换等。
Java虚拟机的即时编译器与C/C++的静态优化编译器相比,可能会由于下列原因导致输出的本地代码有一些劣势:1、即时编译器占用用户程序的运行时间,由于时间压力优化手段受到限制。不敢随便引入大规模优化技术。2、Java语言是动态的类型安全语言,虚拟机要确保程序不会违反语言语义或访问非结构化内存,更加耗时。3、Java语言没有virtual关键字,但是虚方法的使用频率却远远大于C/C++,意味着多态选择更频繁,优化难度更大。4、Java语言是可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,全局优化难度增加。5、Java语言中对象的内存分配都是堆上进行的,只有方法的局部变量才能在栈上分配,垃圾回收效率降低。
Java虚拟机即时编译器优势:动态性带来的很多优化手段,以及其他优点如别名分析等。
第五部分 高效并发
缓存一致性:协议:MSI、MESI、MOSI、Synapse、Firefly、Dragon Protocol。
Java内存模型:主要目标是定义程序中各个变量的访问规则(规定变量都存储在主内存中),即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。变量包括:实例字段、静态字段、构成数组对象的元素,不包括(线程私有不共享):局部变量、方法参数。
Java内存模型中主内存与工作内存交互的8种原子操作:lock(锁定,作用于主内存的变量,把一个变量标识为一条线程独占的状态),unlock(解锁,作用于主内存,把锁定状态变量释放),read(读取,作用于主内存,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用),load(载入,作用于工作内存,把read操作从主内存中得到的变量值放入工作内存的变量副本中),use(使用,作用于工作内存,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作),assign(赋值,作用于工作内存,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个需要给变量赋值的字节码指令时执行这个操作),store(存储,作用于工作内存,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用),write(写入,作用于主内存,把store操作从工作内存中得到的变量的值放到主内存的变量中)。read和load、store和write必须按顺序执行且成对执行但不保证连续执行。
Java内存模型还规定了一些规则(8条):不允许read和load、store和write操作之一单独出现,不允许一个线程丢弃它的最近的assign操作(变量在工作内存中改变后必须同步到主内存),不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程工作内存同步到主内存,一个新变量只能在主内存中诞生且不允许在工作内存中直接使用一个未初始化的变量(use和store之前必须先assign和load),一个变量在同一时刻只允许一个线程对其进行lock操作但lock操作可以被同一条线程重复执行多次且只有执行相同次数的unlock才能解锁,如果对一个变量执行lock操作将会清空工作内存中此变量的值且执行引擎使用此变量前需要重新load或assign初始化变量值,如果一个变量事先没有被lock那么也不允许unlock也不允许去unlock一个被其他线程lock的变量,对一个变量执行unlock操作之前必须先把此变量同步到主内存中。
volatile变量(的特殊规则):Java虚拟机提供的最轻量级的同步机制。Volatile变量特性:对所有线程的可见性(很容易误解),禁止指令重排序优化。volatile变量在读取前会更新其值,在各个线程的工作内存中不存在一致性问题,但是其操作并不是原子的。在不符合以下两条规则的运算场景中,仍然需要对volatile变量加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性:运算结果并不依赖变量的当前值或这能够保证只有单一的线程修改变量的值,变量不需要与其他的状态变量共同参与约束。volatile变量读操作与普通变量性能差不多,而写操作因为要插入许多内存屏障指令放置乱序所以会慢一些,但是大多数情况下比其他同步工具总开销小,volatile与锁的唯一选择依据就是volatile的语义能否满足使用场景的需求。
long和double型变量(的特殊规则):允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行(非原子性协定)。虽然是允许但是虚拟机一般都会实现为原子操作。
先行发生原则:操作A先行发生于操作B,操作A的影响能被操作B观察到,影响包括修改了内存中的变量、发送了消息、调用了方法等。程序次序规则:在一个线程内,按照代码顺序,前面的操作先行发生于后面的操作。管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作(后面是指时间上)。Volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作(后面是指时间上)。线程启动规则:Thread对象的start方法先行发生于此线程的 每一个动作。线程终止规则:线程中的所有操作都先行发生于此线程的终止检测(Thread.join()、Thread.isAlive()等)。线程中断规则:对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。对象终结规则:一个对象的初始化完成(构造函数执行完成)先行发生于它的finalize方法的开始。传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。
并发并一定要依赖多线程(如PHP中很常见的多进程并发),但是Java里谈到并发,大多数与线程脱不开关系。Java的Thread类的关键方法都声明为Native的。
线程的实现:使用内核线程实现(操作系统需为多线程内核,但一般用其高级接口轻量级进程,1:1),使用用户线程实现(用户空间实现系统内核无感知,实现复杂,基本放弃,1:N),使用用户线程加轻量级进程混合实现(N:M)。Sun JDK的windows版和Linux版都是1:1的线程模型实现的。Solaris平台支持1:1和N:M线程模型。
线程调度:协同式线程调度(一条线程工作完成后通知另一条线程),抢占式线程调度(系统分配)。Java语言设置了10个级别的线程优先级,但仍然取决于系统。
5种线程状态:新建(New,创建后未启动),运行(Runnable,正在执行或者等待CPU分配),无限期等待(Waiting,需要其他线程显式地唤醒,未设置Timeout的Object.wait()与Thread.join()、LockSupport.park()可使线程进入此状态),限期等待(Timed Waiting,Thread.sleep()、LockSupport.parkNanos()、LockSupport.parkUtil()、设置了Timeout的Object.wait()与Thread.join()可使线程进入此状态),阻塞(Blocked,等待着获取到一个排他锁),结束(Terminated)。
线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。特征:代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。
5类操作共享数据:不可变(线程安全),绝对线程安全(满足定义),相对线程安全(对象单独的操作是线程安全的),线程兼容(在调用端正确地使用同步手段则线程安全),线程对立(无论调用端是否采取同步措施,都无法在多线程环境下并发使用的代码,但很少,如Thread的suspend()和resume()方法就是但被废弃了,还有System.setIn()、System.setOut()、System.runFinalizersOnExit()等)。
互斥同步:多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或是一些,使用信号量的时候)线程使用。互斥是实现同步的一种手段,临界区、互斥量、信号量都是主要的互斥实现方式。互斥是因、同步是果,互斥是方法、同步是目的。Java中synchronized关键字(编译后为monitorenter和monitorexit字节码指令,线程可重入,重量级),java.util.concurrent包下的重入锁ReentrantLock(吞吐量大于synchronized):等待可中断(可以放弃等待)、可实现公平锁(按照申请锁的时间顺序依次获得锁)、锁可以绑定多个条件(绑定多个Condition对象)。互斥同步问题在于线程阻塞和唤醒带来的性能问题。
非阻塞同步:基于冲突检测的乐观并发策略,冲突则不断重试(硬件指令集的发展为前提)。CAS操作。
无同步方案:可重入代码,线程本地存储。
自旋锁:为了让线程等待,让线程执行一个忙循环(自旋),以避免线程切换的开销,锁被占用的时间不长才有意义。自适应自旋:自旋时间不固定,而是由前一次在同一锁上的自旋时间及锁的拥有者的状态来决定。
锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。依据来源于逃逸分析的数据支持。
锁粗化:我们一般推荐同步块的作用范围尽量小,但是如果出现在循环体内就会增加不必要的重复加锁解锁开销,所以如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
轻量级锁:轻量级是相对于使用操作系统的互斥量来实现的传统锁而言的。轻量级锁并不是用来替代重量及锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。在代码进入同步块的时候,如果此同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向锁记录的指针。如果这个更新动作成功了那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位将转变为“00”;如果这个更新动作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志位变为“10”,Mark Word冲存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。轻量级锁的解锁过程也通过CAS操作来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了,如果替换失败说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。
偏向锁:目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争情况下把整个同步都消除掉,连CAS操作都不做了。偏是指锁偏向与第一个获得他的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。锁对象第一次被线程获取的时候,虚拟机将会把对象头的标志位设为“01”(偏向模式),同时使用CAS操作把获得锁的线程ID记录在对象Mark Word中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁的相关同步块,虚拟机都可以不再进行任何同步操作了。当有另一个线程去尝试获得这个锁时,偏向模式就宣告结束了。根据对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定(标志位“01”)或轻量级锁定(标志位“00”)的状态,后续步骤就是轻量级锁那样的操作了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。