什么时候看java虚拟机规范第二版比较好

当前位置: →
→ 深入Java虚拟机
深入Java虚拟机
& 作者及来源: wanghongjun - 博客园 &
&收藏到→_→:
摘要: 深入Java虚拟机
"深入Java虚拟机"::
一、走进java
未来:、混合语言、多核并行、进一步丰富语法lamda-函数式编程、64位虚机。
(一).java内存区域与内存溢出异常
1.区域:程序计数器/java栈/本地 栈/java堆/ 区(运行时常量池)
还有直接内存(不在区域里但是能访问,通过nio,可用native函数库直接分配堆外内存),通过directbytebuffer访问。
2.对象访问。一般一条语句至少涉及 区/栈/堆,如string a =new string("abc"); &
引用的定位方式:句柄访问方式和直接指针访问方式。(java的hotspot用的直接指针方式)
3.outofmemoryerror异常。
(1)&堆溢出:&不停的建对象超过了xmx和xms指定的内存大小。 &-xmx最大值 -xms最小值。 -xx:+heapdumponoutofmemoryerror.
& &&& &&eclipse运行时,通过run configurations设置:xmx/xms &-xx等参数。(-verbose:gc -xms20m -xmx20m -xmn10m -xx:+printgcdetails -xx:survivorratio=8)
& &&& &&java.lang.outofmemoryerror:java heap space. &解决堆异常首先可以用eclipse memory analyzer对dump出来的堆转储文件分析(重点确认内存中的对象是否是必要
& &&& &&& &&& &的)分清楚是内存泄露(memory leak)还是内存溢出(memory overflow)。
& &&如果是内存溢出可以通过查看泄露对象到gc roots的引用链。(看到泄漏对象通过怎样路径与gc roots相关联,导致垃圾回收器无法删除)。通过泄露对象的类型信息,gc roots的引用链信息,就可以比较准确的定位泄露代码的位置。
&&& 如果不泄露(内存中对象还必须活着),检查-xmx与-xms(与机器物理内存看是否还可以调大)。检查是否某些对象太长,持有状态时间太久。
& &&& &&& &&& &(处理对内存问题的思路---知识/盒经验是后面三章主题)
(2)栈和本地 栈溢出。(栈深度超过虚机要求深度stackoverflowerror,无法申请新内存outofmemoryerror)
hotspot不分这两者,对hostspot说本地 栈大小参数-xoss虽存在,但实际无效;栈容量只由-xss参数指定。
& &【-xss减少栈内存大小;定义大量本地变量增加栈中本地变量表长度】 & & &在单线程时着来年各种 都是stackoverflowerror。 如果多线程倒能产生outofmemoryerror。
& &多线程产生内存溢出。(这里内存溢出与栈空间是否足够大没关系,给每个线程里栈分配空间越大,反而越容易内容溢出。)
& &给每个进程的内存有限制(32位windows限制2gb,有参数控制堆和 区。-xms -xmx &-xx:permsize -xx:maxpermsize=10m.
& &程序计数器内存很小,剩下的进程内存由栈瓜分---每个线程分配到的栈容量越大,可以建立的线程数就越少。(栈深度一般在没问题,建立过多的线程的内存溢出解决办法:在不减少线程数和改由64位jvm情况下,只能用减少最大堆和减少栈容量方式换取更多线程)。-----减少内存的 。
& &【多线程内存报错:java.lang.outofmemoryerror:unable to create new native thread 】 ---java的线程会转成os线程。
(3) & 运行时常量池溢出。可以通过string的intern 获得(如果常量池有就返回对象,没有就放入常量池返回对象)。通过&-xx:permsize -xx:maxpermsize=10m 限制 区来限制常量池容量。 &与 区溢出java.lang.outofmemoryerror:permgen space 相同。
(4) 区溢出。借助gclib直接操作字节码运行时产生大量动态类。
存储类名、访问修饰符、常量池、字段描述、 描述等(测试运行大量类填充 区,直至溢出)可以直接用j2se api动态产生类generatedconstructoraccessor和动态代理(但这个实验麻烦)。这里用cglib操作字节码运行时。(spring, 用到cglib增加字节码技术,增强类越多需要更大的 区)。
&&while(true){
& & & & & &enhancer enhancer=new&enhancer();
& & & & & &enhancer.setsuperclass(oomobject.class);
& & & & & &enhancer.setusecache(false);
& & & & & &enhancer.setcallback(new&methodinterceptor() {
& & & & & & & &
& & & & & & & &@override
& & & & & & & &public&object intercept(object obj, method method, object[] args,
& & & & & & & & & & & &methodproxy proxy)&throws&throwable {
& & & & & & & & & &//&todo&auto-generated method stub
& & & & & & & & & &return&proxy.invokesuper(obj, args);
& & & & & & & &}
& & & & & &});
& & & & & &enhancer.create();
动态生成大量class的中,特别注意类的回收状态。(除了cglib字节码增强,还有大量 p货动态产生 p的,基于osgi等)osgi(open service gateway initiative)技术是面向java的动态模型。(不同加载器加载会视为不同类)
(5)本机直接内存溢出。
-xx:maxdirectmemorysize指定,不指定默认与java堆最大值(-xmx)一样。
通过反射获取unsafe实例(有getunsafe() 使用unsafe功能)。因为directbytebuffer分配内存也会报内存溢出,但是没有真正申请,只是计算知道无法分配就手动抛出。真正申请分配内存用unsafe.allocatememory().
& &private&static&final&int&_1mb=;
& &public&static&void&main(string[] args)&throws&securityexception, illegalargumentexception, nosuchfieldexception, illegalaccessexception {
& & & &field theunsafeinstance=unsafe.class.getdeclaredfields()[0];
& & & &theunsafeinstance.setaccessible(true);
& & & &unsafe&unsafe=(unsafe)theunsafeinstance.get(null);
& & & &while(true){
& & & & & &unsafe.allocatememory(_1mb);
会报错不要理他。
(二)垃圾回收器与内存分配策略。
1.几个内存区域,程序计数器/java 栈/本地 栈都是线程相关的,编译时就能确定要分配多少内存和在何时收集( 结束时)。主要考虑的是java堆和 区的内存回收。(多个实现类需要的内存不同, 的多个分支用的内存也不同,运行时才知道创建多少对象,这部分分配和回收都是动态的)
2.回收对象查找算法。
(1)引用计数算法。看对象上的计数,缺点:很难解决循环引用问题。
(2)根搜索算法。通过一系列名为gc roots的对象做为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链(一个对象对gc root没有任何引用链就可以回收)
gcroot:栈中引用的对象, 区中静态属性和常量引用的对象;本地 栈jni(一般的native )引用的对象。
(3)再谈引用。强引用(obj=new object()) &; 软引用(在内存溢出之前会回收)--弱引用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框架的场景,以及动态生成大量 p和osgi这类频繁自定义classloader场景需要具备类卸载功能,保证永久代不溢出。
3.垃圾收集算法:(涉及程序细节且操作内存方式不同,不讨论细节)----内存回收 论。
(1)标记-清除算法:容易产生碎片。
(2)复制算法:易产生浪费,新生代的主要算法。
(3)标记-整理算法:速度较慢。
(4)分代收集算法:分新生代和老年代。
4.垃圾收集器。
(1)serial收集器。 useserialgc. &单线程,阻塞。clent模式下默认的模式。
(2)parnew收集器. useparnewgc. 多线程,阻塞。-xx:survivorratio & -xx:pretenuresizethreshold(多大的对象直接放入永久代) &-xx:handlepromotionfailue(决定fullgc的频率--打开选项会减少fullgc). server模式运行首选的收集器(只有它能与 一起工作,性能).
用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搭配使用; 收集器的后备预案(发生concurrnt mode failure)时用。
(5)parallel old收集器. useparalleloldgc 标记-整理。jdk6中有了才能和parallel scanvage提供吞吐量优先的收集器。
(6) 收集器(concurrent mark sweep)。useconcmarksweepgc.强交互划时代的收集器--真正意义并发。
四步:初始标记
initial mark(扫描与根直接关联的对象)、
concurrent mark(初始标记的基础上向下追溯标记)、
remark(扫描 堆中的对象,重新标记,并并处理对象关联)、
concurrent sweep. &(两个并发操作可以与用户线程一起用)。 &除此外还有concurrent precleaning(超找在并发标记阶段新进入老年代的对象,减少remark阶段压力).
remark( 堆重置)。
缺点:cpu资源敏感;无法处理浮动垃圾,并行 中产生的垃圾,不能等满了清除。-xx: initiatingoccupancyfraction提高百分比,比例高引发concurrent mode failure.
& &;碎片(标记清理) -xx:+use compactatfullcollection清理完碎片整理.,-xx: fullgcsbeforecompaction.
(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中的东西自己写监控。
p显示java执行主类。-l(全名) -q(只查进程号) &-v(启动时参数) -m(输出main的参数)
(2) tat:监视性能(显示进程中类装载、内存、运行时编译信息)。
参数-class & & -gc -gccapacity &-gcutil -gccause -gcnew -gcnewcapacity -gcold -gcoldcapacity &-gcpermcapacity -compiler -printcompilation.
tat option vmid [interval [s|ms]] [count]
(3)jinfo:java配置信息的。 java参数的默认值显示 (java -xx:+ 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) tack:java堆栈跟踪。成为java threaddump或者javacore文件。每条线此文来自: 马开东博客
转载请注明出处 网址:
程正在执行 堆栈集合(照线程卡顿原因)。
tack [option] vmid &. & -f强制输出 -l还有锁信息 -m显示本地 c/c++堆栈。 &java.lang.thread添加了getallstacktraces() 用于显示所有线程的stacktraceelement对象。
jhat dmp文件 &会启动一个服务,可以在网页中看。包为单位分组展示;heap histogram与jmap-histo功能类似,ool页签。
2.jdk可视化。
(1)jconsole.默认概述/内存/线程/类/vm摘要/mbean.六个页签。
针对jmx mbean进行管理。内存- tat ,线程 tack.
(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存储),可以考虑用集中缓存。 &最后用
起进行内存回收提高老年代的回收速度。
(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.) & (经常用 tat -gccause查询最近gc原因。
(5)选择收集器降低延迟。(编译代码时老年代时间久)。
-xx:+useconcmarksweepgc和-xx:+useparnewgc. & (提高 收集门槛默认68%. -xx: initiatingoccupancyfraction=85.
的调忧还会有 、资源池、磁盘io.
三、执行子
(一)类文件结构(机器指令集无关、平台中立的格式)
1.无关性此文来自: 马开东博客
转载请注明出处 网址:
的基石。平台无关+语言无关。实现无关的基石是字节码和。(字节码命令提供的语义描述能力比 本身更强大,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动态连接,class不存 字段的内存布局信息,这些符号引用不经过转换没法用;运行时从常量池获得符号引用,类创建时/运行时解析翻译到具体内存地址中--创建和动态连接)。 每一个常量都是一个表。11中结构不同的数据(1-12,缺2).
-----constant_class_info型常量结构:u1-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/其他文件 p/ 中读取。
这是开发期可控性最强的阶段。&
区存储格式由各自己定义;加载阶段和连接阶段是交替进行的。(加载未完成,连接已经开始)
(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的实现接口中找/抽象--否则nosuchmethoderror.
d)接口 解析。如果class_index不是接口就报错;看接口c中是否有,有就返回;父接口和object找;否则nosuchmethoderror.
(5)初始化.开始真正执行java代码。执行&clint&()。-----收集类中赋值动作,静态块,顺序遵循原文件顺序,静态块只能访问之前定义的变量;&clinit&不需要显式调用负累构造器,会保证子类clint前父类已经执行;父类的静态块优先与子类的;clint对类和接口不是必须的;接口中不用静态语句块,可以有变量初始化,不会先执行父接口的会保证多线程下的加锁同步。
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的fragmendynamic import bundle的否则失败)
(三)字节码执行引擎。自己制定的指令与执行引擎。(解释执行和编译执行)
1.运行时栈桢结构(支持 调用和执行的)
每个 调用到执行完都是一个栈桢的入栈和出栈。(需要内存提前已经确立好了,不会受运行时影响),活动线程只有栈顶的栈桢有效(当前栈桢--当前 ) 。 线程还有一个指令计数器。
(1)局部变量表。 参数和内部定义变量值。变量槽slot.(一般每个变量一个槽,double/long两个)。----return address类型为 r/ r_w/ret服务(指向字节码指令地址)。
非static --0slot指的是其余参数按顺序排列;slot可重用;(slot复用会影响gc行为);没有初始化阶段。
(2)操作数栈。也叫操作栈(max_stacks). 指令向里面写入或者提取内容(类型要与指令严格匹配)。 &解释执行引擎---基于栈的执行引擎。
(3)动态连接。栈桢包含一个指向运行时常量池该栈桢所属 的引用,持有它支持调用 的动态连接。(每一次运行时转为直接连接的叫动态连接。 类加载阶段/第一次运行转化--静态解析)
(4) 返回地址。(正常和抛异常)。
都要返回到 调用位置,返回时在栈桢保存一些信息,恢复上层 的执行状态。(正常退出,pc计数器值可以作为返回地址,栈桢会保存这个计数器值;异常推出,返回地址需要一查功能处理器表确定,栈桢中不保存这部分信息)。推出时--恢复上层 局部变量表和操作数栈,把返回值压入调用者栈桢的操作数栈中,调整pc计数器指向调用指令后面一条指令。
(5)附加信息。
(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)tomcat--正统类加载(隔离/共享类/ p_hotswap/自身不受影响。)
/common &/share &/server &/webapp/webinf/
commonclassloader & (catalinaclassloader独立) & sharedclassloader & webappclassloader. 剩下三个都是父子关系。 perclassloader.
(2)osgi. 交叉加载可能会死锁(加载的时候--需要锁定)
(3)字节码生成和动态代理实现。(javasist/asm/cglib)javac是字节生成的祖宗。用到字节码生成(aop/ p/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
实现执行类。
四、程序编译与
(一)早期编译优化(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
& &&& &&& &&& &&& &&& &&& &)
& &&& &&& &)
& &case by_
& &&& &while(!todo.isempty())
& &&& &&& & generate(desugar(flow(attribute(todo.remove()))); & & 标注-数据流分析-解语法糖-生成字节码。
(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.p.enter类实现。
的出口是一个待处理的列表(to do list),包含了每一个编译单元ast的顶级节点和package-info.java的顶级节点。【根据上面的ast,把符号的地址--符号名称用k-v表示,后面语法分析来检查】
(3)注解处理器
annotation与普通java一样在运行时发挥作用,jdk1.6 - r-269规范提供了插入式注解处理器api在编译期间对注解进行处理,可以看做一组插件,可以读取、修改、添加ast中的任意元素【如果改了ast元素,将回到解析及填充符号表的 重新处理,知道没有注解处理器继续处理,一次循环成为一个round】。
有了注解处理器可以做一些干涉的行为,(语法树的任意元素在插件中都可以访问到),可以给插件功能上很大发挥空间。创意--可以用它完成只能在编码中完成事情。
initprocessannotations() ,执行在processannotations()完成--判断是否有新的注解处理器,有的话通过com.sun.tools.javac.processing.javacprocessingenvironment类的doprocessing 生成一个javacompile对象对后续步骤处理。
(4)语义分析与字节码生成(获得了ast后,无法保证源程序符合逻辑)
语法分析:对结构上正确的源程序进行上下文性质的审查,int a =1; boolean b=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.p.attr和com.sun.p.check类完成。
--数据流及控制流分析。对上下文逻辑进一步验证,检查程序局部变量使用前是否赋值、 的每条路径是否有返回值、是否有受检查的异常没有处理等问题。
& 编译时的数据流及控制流分析 与 类加载时数据流及控制流分析目的一致,但校验范围有区别,有些校验项只在编译或运行时期才进行。(如final修饰符分析,因为局部变量在字节码中没有修饰符,需要在编译时处理好,有无final的字节码结果是一致的。 局部变量的不变性仅提供编译时保障)具体实现com.sun.p.flow类完成。
---解语法糖(syntactic sugar).
语法糖对功能没有影响,只是方便使用,减少出错机会。java属于低糖语言,最常用的语法糖是泛型、变长参数、自动拆箱装箱等。jvm运行不支持这些语法,编译阶段被还原为简单语法结构(解语法糖)com.sun.tools.javac.transtype类和com.sun.p.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.p.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)其他实例。基于注解处理器的项目有用于校验 标签使用正确性的
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.
& &回边计数器没有衰减 。 当它溢出时会把 计数器也调整到溢出,执行标准编译 。
编译动作在后台执行,如果禁止-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/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) 的线程安全。各种操作共享数据的类别:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。
不可变:不可变对象一定先行安全。(对象行为不影响自己状态途径有多种--简单所有的状态变量都声明为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类实现。
(1)自旋锁和自适应自旋。(os切换状态浪费性能。1.6默认开启) &自旋锁有一定时间,没等到就瓜期线程。默认自旋次数-10次,可以通过-xx:preblockspin来更改。
&&&&1.6引入自适应自旋锁,--自旋时间不固定,由上次自旋时间和锁拥有者状态来决定。【上次获取过-自旋等待时间长,自旋搜索此文相关文章: 此文来自: 马开东博客
网址: 站长QQ
深入Java虚拟机_博客园相关文章
博客园_总排行榜
博客园_最新
博客园_月排行榜
博客园_周排行榜
博客园_日排行榜

我要回帖

更多关于 java虚拟机规范 mobi 的文章

 

随机推荐