第三阶段、深入JVM内核—原理、诊断与优化

JVM

1.       虚拟机是通过软件模拟具有完整硬件系统的,运行在隔离环境中的完整计算机系统。

VMWare模拟的是实际物理机包括内存,cpu等指令集,而JVM则是用软件方式模拟java的字节码指令集。为了精简,cpu是有寄存器来加快存取速度,而jvm的模拟对寄存器的模拟是定制的(因为是软件级的)。

2.1996 JDK1.0 classic VM是纯解释运行,速度会非常慢,所以感觉是性能不高。1997 JDK1.1,内部类,JDBC,RMI,reflection。1998 JDK1.2 出现j2se,j2ee,j2me,可以做到精确的内存管理,了解内存的区间内容,编译,解释混合执行,gc改善。JDK 1.3 Hotspot 默认虚拟机。2002 JDK1.4,NIO,IPV6,日志API。2000 JDK1.5,泛型,注解,装箱。JDK1.6,脚本语言支持,JDBC4,java编译api(允许java调用编译)。2014,JDK8,Lambda表达式,函数编程减少行数和冗余,语法增强。

3.2006,Hotspot为sun jdk,open jdk中的虚拟机。2010 sun 被oracle收购后把2008收购的Bea jrockit VM进行整合,就是在hotspot基础上移植jrockit的优秀特性。

4.JVM 规范,java语言规范,JVM规范多数是二进制格式,变参语法结构,语法结构最后只要是UNICODE就是javaLetter。

5.类型和变量,基础数据类型就不是对象类型的。Java语言规范和JVM规范相对独立。JVM主要定义了二进制class结构和JVM指令集等等。比如Groovy,Scala,Clojure语言都可以在JVM中运行。

6.JVM规范,class文件格式,数字内部标示,java里面是没有无符号整数的。ReturnAddress指向操作码的指针,不对应java数据类型,不在运行时修改,finally实现需要。整数如何表达,

7.JVM中整数的存储都是用补码来存储的,与C语言类似的。正数的原码,反码,补码相同,负数补码原码符号位不变,反码+1.

原码:00000101,首位符号位(+0,-1)

反码:11111010,符号位不动,数字位取反

补码:00000101,正数补码就是原码

补码:10000110,-6的原码

补码:11111010,符号位不懂,反码+1

补码:10000001,-1的原码

反码:11111110,-1的反码

补码:11111111, -1的补码

为什么要补码表示呢?有什么好处呢?

a.对应0来说,不知道是正或负,但用补码可以没有歧义的表示00000000

整数         原码         反码         补码

0                00000000 01111111 00000000

-0               10000000 11111111 00000000

b.补码很好的参与2进制运算,只要简单的将补码相加即可(符号位是参与运算的,可以进位变化掉,原码符号位就不是我们想要的结果)

8.float的表示和定义(单精度为例)

$      eeeeeeee mmmmmmmmmmmmmmmmmmmmmmm

+/-    2^(e-127)(此处e也可能表示一些0.0000021212这样的数字,正负号,一半正,一半负,基本各占一半)

S*m*2^(e-127)

m的位数系数是2(-x)是小数,从左往右运算。

还定义了一些特殊化方法:类构造方法,实例构造方法。

9.JVM规范:

JVM没有寄存器的存在所以有入栈出栈的方法。JVM对java liberary也提供了一些支持,reflect,classloader,初始化class,inteerface,安全,多线程,弱引用等。

JVM汇编的格式:

Index opcode [<operand1>[<operand2>]] [comment] ,如java汇编之后的结构如下,在JVM中实际执行的是汇编后的指令代码。

Hotspot是JVM的一种实现,实际JVM只是一种文档和规范,平时加强对JVM虚拟机的理解。

Java xxx->config->jvm.dll->jvm JNIEnv接口->main 方法

Class->loader->内存(m,d,s,na)pc,ruun,native gc

Pc寄存器:线程指向下条指令的地址(类 常量 方法信息 方法字节码)基本认为保存类的原信息对类进行描述 通常和永久区关联。

Java堆:和程序开发比较密切,比如new 对象比较集中,是所有线程共享的。对不同的GC来说,堆也是分代的。

Java栈:栈是线程私有的,有一些帧组成。帧里面放的是局部量,操作数栈,常量池指针。每次方法调用都会创建新的帧并且压到栈中去。(这个就能解释struts1虽然是单例模式的,但调用方法的时候却是线程安全的)比如这个例子:一个槽位最多容纳32位,所以long占用2个槽位。这是静态的方法。非静态方法的首个槽位传递的是当前对象的引用。其他基本相同。不管是不是递归,每次方法调用都会产生一个帧栈。

Java栈:操作数栈

因为java没有寄存器,所有参数传递都必须依靠栈来完成,就叫操作数栈。通过这图说明局部变量和操作数栈是有区别的。

栈上分配内存使用完毕就会被回收,不太可能出现内存泄露的问题。因为JVM进行了一定的优化。但栈空间分配的对象内容比较小。栈上分配只能分配非逃逸的,如果逃逸,别的线程也使用的话,就只能堆中分配了。

栈上分配小对象是有好处的,栈是线程私有,函数调用完毕会自动回收减轻gc压力(说明gc是对堆进行操作的)

堆,方法区,栈交互方法。线程运行的变量引用是在栈中的,对象的实例分配是在堆当中的,所以当需要对象内容时,堆又会去方法区里读取信息。(因为class的字节码和类描述是在方法区中的)

如何让递归函数调用的次数多一些,该怎么做呢?

内存模型,线程有自己的工作内存和共享的主存,。线程操作的是自己的工作线程,工作内存和主存是有时差的。所以他们之间有值的交互。一个线程更新的值是不能马上反应到其他工作线程中。但是想其他线程也能立刻获得改动的值需要用到volatile关键字,其他线程就会去主存中拿去数据。

Volatile说性能可能比同步强,实际上不绝对,而且也不能替代锁的功能。语义是否满足需求。

可见性:线程修改变量后,其他线程可以立即知道。

保证可见性方法:

Volatile,synchornized解锁之前,数据会进行同步,回写信息到主存中。Final定义的变量在初始化后其他线程是可以立即可见的。

无序性,指令重排或工作内存和主存的同步延时问题,a线程做的事情对b线程来说可能是无序的。

如果能保证同个线程当中按顺序语义执行结果是相同的话,但其他线程中是不能保证的。

对于线程内的语句顺序由编译器保证,并不一定是有序的。

有一些前后的限定依赖。

解释执行,执行的是字节码。编译执行(是运行时编译字节码成机器码)有数量级的提升。保守估计性能差10倍以上。

JVM结构,功能实现机制,平时更注意些什么内容,减少避免一些低级的错误。

常用JVM配置参数

Trace跟踪参数

Def new

Eden

From tp

Tenured

The space

Compacting perm

The space

Ro rw space

Xloggc:log/gc.log可以重定向log日志,当服务关闭后还是可以追溯到日志文件的。

-XX:+PrintHeapAtGc后如下面的gc打印信息可以看出,eden内的部分信息移到了tenured区,而其他区内容被清除了。

-XX:+TraceClassLoading主要监控系统中类的加载,查看耿宗调试场合。

-XX:+PrintClassHistogram,ctrl+break后,打印类的信息,看看如果出现outmemmery可能就是byte数组引起的,是按序排序的。

-Xmx –Xms

最多和至少使用多少空间。Java的运行一般会限制在最小的空间左右运行,gc后实在不能保持在最小空间内就进行扩容。

Total mem是已经使用的空间,总空间-totalmem是freeme

Xmn-新生代大小

From to 是幸存区的意思survivorRatio

新生代不够可能会向老年代分配,tenured放一些classloader,线程等系统级别的对象。

合理的survivor大小有利于eden带的使用,GC的次数减少也能降低老年代的增加。

-XX:+HeapDumpOnOutOfMemoryError,出现OutOfMemoryError后做一个转存。正常可能会马上宕掉,重现,运行时间可能都比较长,在运行时就把堆信息。

-XX:+HeapDumpPath,导出OOM的路径。

甚至可以做一个脚本的调用工作,重启,发邮件呀,报警呀都是可以做的。

参数的调整是根据实际的情况具体调整的,官方推荐edge,3/8,survivo1/10,OOM时,记得dump出堆的信息,确保可排查现场问题,要么很难复现outofmemery情况。

-XX:PermSize –XX:MaxPermSize 永久区初始空间和最大空间。表示一个系统可以容纳多少类型。

使用 CGLIB等库时候可能产生大量的类,这些类可能会撑爆永久区导致OOM,发生fullGC后也,即使堆空间没有用完,永久区Perm gen使用完也是有可能导致溢出的。

-Xss栈大小分配,每个线程有且私有区域,栈帧内包括(局部变量表,参数分配在栈上),栈空间对线程运行是不可缺少的,栈一般还是比较小的,因为线程数可能会比较多。N*ss,为了增多线程数,减小栈的大小。但是栈空间又决定了调用深度(比如递归调用)。没有出口的递归必然导致栈溢出。为了增多递归调用次数,因为栈帧内局部变量表,所以减少局部变量可以减小栈帧空间。

GC算法

及时回收无用的内存,以确保系统有足够内存可用。Java里由gc线程来回收管理,目的是防止人为引起和防止内存的泄露。

堆和永久区是受GC管理的。如何释放,就由算法来处理了。下面是算法。

1.       引用计数

只要有人使用某对象a,就会+1,释放后-1,当为0时,这个对象就可以被释放。只要有路径可达对象,对象就被引用。否则就可以回收。

但垃圾对象的循环引用是不可被处理的,这是个根本问题。

2.       标记清除

  1. 标记阶段(区分是否可达)
  2. 清除阶段(未被标记的就可以清除)

3.       标记压缩(是对清除算法的一种改良)

使用存活对象比较多的情况。先从根对象进行一次标记,清理时是将存活的对象复制(移动)到一端,清理边界外空间(这个边界目前指的就是行尾)。

4.       复制算法

  1. 不适合存活对象多的场合,将正使用中的对象复制到未使用的内存块中,然后清理原使用区的所有对象,交换角色,完成垃圾回收。

一次只使用其中一半的空间,有些空间浪费。

那么怎么优化,大对象进入老年代,老年对象(经过屡次回收都没有回收掉计数增大)进入老年代。如果减少浪费。增加担保空间,总有一部分复制空间是不使用的。因为复制空间放一些小的对象,所以大对象一般放老年空间中。

现在对照这个堆空间信息来看eden就是新生代,from,to就是复制空间,我们实际计算new generation总大小(0x28d80000-0x27e80000)/1024/12014=15M,而eden大小不足12288k,不足总大小,原因就是有一部分被复制区占用了。

分代思想:短命的是新生代,长命的为老年代。少量对象存活,使用用复制算法(因为用到了复制)。大量对象存活(比如老年代,少部分是作为担保区进入的,大部分还是因为没有被回收而进入老年代,老年代存的多,时期长,复制的话机会把整个区都会复制一遍)所以用标记清理或标记压缩可能性能好些。

总结:引用计数没有采用。而标记清除老年代使用,复制算分新生代使用。这些算分如何识别垃圾对象呢,可触及性。

可触及性:从根开始,可以触及到这个对象。

可复活的:对象的引用丢了,但是可能复活,重新触及。虽然暂时不可达,但是是不能回收的。

不可触及:不可达,也不能复活,所以可以回收。

Finalize只会调用1次,有可能使对象复活。所以避免使用finalize方法,而且gc调用时调用,基本是不受控制的。倒是可以用try-catch-finally来替代,finalize什么时候调用并不确定,gc的调用点也不确定,所以不太推荐使用这个方法。

可触及性:根如何判断,栈中引用对象,静态或常量(全局对象),JNI方法栈中引用对象。

Stop-The-World:全局停顿,native代码虽然可以执行却不能交互,虽然dump线程,死锁检查,堆dump都可能出现全局停顿,但多半是由gc引起。为何要全局停顿?当gc在清理过程中,如果java还在不断产生垃圾的话,1是不会彻底的将垃圾清除,2是此时清理也会给gc造成很大负担,给gc算法也带来很大难度(不好判断到底是不是垃圾),因此gc一旦开始工作,其他所有java线程都要停下来进行垃圾标记,如果过程动荡,垃圾标记也不会很顺利,gc的新生停顿较短,老年花的时间可能就比较大了,有时可能没有相应。那有没有解决方法呢?使用主备的方式,但是同时服务也是很危险的。

JVM的gc,一屋不扫,何以扫天下,如果家里的垃圾不清理,程序是不可能长期运行的,负载是大的,空间和容量能力是有限的。

GC参数

1.       堆结构

一般分配对象都到新生区eden,如果过大的对象超过预值就放到tenured区。如果新生代回收后对象依然存在,就放到幸存代(s0-s1),from to大小相同,完全对称,功能相同,只是浪费空间,如果经历多次垃圾回收就会到老年代。

2.       串行收集器

useSerialgc古老而稳定,只不过是单线程的,启用后新生代复制算法,老年代用压缩算法(先标记可达对象,然后向一端复制,最后有多少是知道的,边界之外的就清除掉)。这就是串行执行的模型了。

3.       并行收集器

启用useparnewGC后只应用到新生代的收集,使用多线程的复制算法。下面就是模型了。这种方式使用多个线程进行回收,然后程序继续运行。

总结:都会出现stop the hole word.

Parallel收集器:

更关注吞吐量,新生代依然是复制,老年代是标记压缩,可以看成是一个新生代与老年代的并行化。-XX+UseParallelGC,-XX+UseParallelOldGC。

-XX:MaxGCPauseMills,是每次GC回收不超过的设定值.

-XX:GCTimeRatio和吞吐量有关,单位时间cpu分到了gc还是应用程序,应用程序执行时间越长,应答能力也越长,吞吐量就好过。所以这两个参数是矛盾的,我们要知道问题的主要矛盾在那,不可能全都提高。要么就优化GC算分。

4.       CMS收集器:concurrent mark sweep并发标记清除,垃圾回收器和应用程序一起执行,交替执行,停顿时间相对减少,可能降低吞吐量。因为要达到和应用程序并发执行,所以会比较复杂。a.从根快速做一次标记(快速标记s-h-wold)。B.并发标记。(全部标记)C.过程中有新的垃圾产生,所以修正重新标记(基本完善,快速stopholeworld)D.开启清理操作。用并发清除,而不是压缩,如果是压缩,应用程序很难继续执行了。

5.       它尽可能降低系统停顿,给gc的时间多,性能就差。因为cms和用户线程同时执行,如果应用程序内存不够花了。如果cms方式导致应用没有足够可用的内存。可以用串行回收器作为备用回收器,启用回收。这时可能就有长时停顿而且内存是消耗殆尽的状态。

6.       标记清除可能有大量碎片存在和标记压缩会复制到一端,然后清除边界外的空间。碎片多的会影响连续的内存空间分配。Cms之所以用标记清除,是因为它更关注停顿时间。如果用标记压缩不但停顿时间可能稍长,而且可能它会找不到那些可用对象的位置。CMS为了回收后的整理。-XX:+UseCMSCompactAtFullCollection Full GC(整理时间独占停顿)

-XX:+CMSFullGCsBeforeCompaction,对象多,一个大的堆,整理时停顿的时间将会变长。这些参数也只能保证大部分时间的停顿周期较短,如果数量过多,还是还是会造成较长的停顿时间的。-XX:parallelCMSThreads约等于cpu的数量。

如何减轻GC压力呢?

A.如何架构,

b.代码怎么写。

C.堆空间怎么分配。

JVM有一些串行cms的内存回收算法和策略,均衡和减少回收时付出的代价。

7.       Tomcat实例演示

FullGC是比较花时间的,没有涨的话,吞吐量会有个比较好的效果。如果堆会有个扩展过程,如果直接把堆设置到64m,那么GC数量就会大大减少,(因为JVM会尽量维持一个低内存的运作,内存不足会执行较多GC)。

同堆大小,新生代,老年待使用并行回收,吞吐量影响并不大。

减少堆大小,增加GC压力,使用Serial回收器。

Serial 646 串行回收(N+O)

Parallel 685 并行回收(N+O)

Parnew 660 只影响新生代(N)

还是都使用并行算法效率高呀

Tomcat7+JDK6 622.5

Tomcat7+JDK7 680

性能提升和JDK版本也是有关系的,JDK不是随随便便升的,要充分测试。

性能:

  1. 在于应用的架构。各种配置参数只是一个微调只是一个锦上添花的作用。
  2. 在良好应用程序的基础上,如果是堆大小设置不合理,那GC的调整会影响局部的部分,不太可能影响全局的性能。
  3. 参数只是个良好应用程序的配置项。当然设置不合理也会影响一些性能,产生大的延时。

Class Loader类装载器

加载

1.不管是文件还是网络中加载,都是二进制流。将内容转为方法区中的数据结构,在堆中生成java.lang.Class对象,这是加载基本过程。

连接

验证a.类名,方法信息,字段信息呀,要验证是否是个正常的格式。0xCAFEBABE开头,jdk版本号范围;元数据基本类型检查基本的语法和语义;字节码检验,操作数栈,局部变量是否和参数吻合,实际运行可能并非如此,字节码没发正常执行,还有跳转指定位置是否合理,检查不合理肯定是有问题的,通过也未必合理。也会做符号引用,继承和实现的肯能是不合理的,权限,包或子类是不能通过符号验证的。

准备阶段:分配内存,并设置初始值(方法区),比如初始阶段可能是0,初始才会被设成1.而常量准备阶段设置成1后期使用永远不变化。

解析:符号引用(java.lang.Object)替换为直接引用(指针或地址偏移量,引用对象一定在内存中)

执行类构造器:clinit,static变量赋值语句,static{}语句,父类clinit被调用,clinit是线程安全的。

Java.lang.NoSuchFieldError错误什么阶段抛出?

classLoader是一个抽象类,将读入java字节码将类装载到JVM中,可以定制满足不同字节码流的获取方式,只负责类的加载过程。

         loadClass(String className) defineClass(字节长度) findClass(String class)回调方法,自定义classLoader的推荐方法,这个怎么回调呢?findLoadedClass(String name),寻找已经被加载的类。

那些相关的BootStrap classloader,extension classloader,app classloader(和应用相关的),custom classloader,每个classLoader都有一个parent,bootstrap classloader是不存在的。

自底向上装载classloader,我们自己写的类会去app中找是否已经加载了这个类,如果没有找到,会将请求给父类,(Extension Classloader)现在自己层面去找,有就返回就好了,否则向上问,如果到bootstrap classloader一路都没有找到,那就开始加载,注意顺序是从顶向下进行加载的。上面的加载完毕,下面就轮不到加载了,bootstrap没有,再给exten,再给app,     。有些是在bootstrap classLoader,通常rt.jar核心包,是系统的核心,bootstrap classLoader就会加载。Java的扩展都会加载lib/ext,appcloassloader会在classpath下。有一种从基础开始架构和构建class类的意思。

举了个例子就是强制加载到apploader中,然后从下往上找,直接找到了。

将个类注入到启动class当中去,从下向上查找,从顶向下加载,rt.jar如何加载应用类呢,service provider interface spi,这个接口再rt.jar中可能是没有实现的,那么在app classloader中实现,而接口,工厂是在rt.jar中,那么如何在rt.jar中产生appclassloader中定义的class,如何解决。Thread.setContextClassLoader(),上下文加载器是一种角色,在顶层中传入底层的实例。所以把FactoryFinder放到一个正确的ClassLoader中就能突破双亲模式的局限性。

双亲加载是JVM的一种默认模式,但不是必须的。Tomcat有个WebappClassLoader,先加载自己的class,本身就是双亲模式的违背,找不到再给parent。正常是从parent开始加载。有点像从object根开始向下加载的过程。

OSGI模块化热加载,有一套查找类的机制,是网状的,完全不符合。

findClass,先查找类,找不到就查找类文件,流转换成2进制流,然后转换成class模板,class模板就可以分配对象空间。

加载类,没有定义父类,默认就是Object类,因为要初始化它。所以加载时会从Object开始加载,然后我们的OrderClassLoader却没有这个Object类,这原本属于rt.jar中的内容,这说明ClassLoader重定义后自行加载。倘若不重载默认是在appclassloader中加载rt.jar加载的。

热替换

Class文件替换后不重启系统让他生效,如何实现这个功能?

应该是监听文件是否修改,触发事件编译,然后在自定义classloader进行替换。

而这种实现是破坏了原有的双亲模式实现的热替换,使程序架构及其灵活有无限的想象空间。

性能监控工具:

1.       运行中可能出现些瓶颈,借用一些内部外部的工具确定系统整体运行的状态,基本从大方向上定位问题的所在,java自带工具查看java程序运行细节,进步一定位问题。

如果用到了swap,说明内存可能不很足够,可能涉及到了I/O读写。

说明系统有线程在频繁切换。

2.       pidstat,可以监控cpu,内存,i/o,这个可以查看到那个线程对I/O有影响。

3.       windows: perfmon,pslist(定时定点用脚本做些事情,命令行工具,自动化数据收集)

4.       java自带工具初步定位是那方面问题?

Jps:查看进程

Jinfo:可以查看一些参数的值,甚至可以在运行时修改某些参数的值。

Jmap: 打印堆内存信息

Jstack:打印线程堆信息

jConsole:图形化工具,可以查看类,堆,多个进程,可以查看各个硬件占用情况,甚至可以查看具体的内存待情况。一个好的thread名字有利于排查线程问题

VisualVM:基本都能实现jconsole的功能。各种图像,各种抽样统计。其实主要还是想看和跟踪一些线程。

来实践:jps找到id号,打印出线程的执行情况,基本可以发现问题所在。

自己占用资源,又不释放资源,相互等待对方的资源。像这种情况需要有一个对象消失。

我们想调优,或查找系统瓶颈,我们需要学会使用一些工具,否则很难查看系统运行细节信息,也就不能排除和定位问题所在,因此,掌握这些是个基本技能。

X.java堆分析

内存溢出(OOM),堆,永久区,线程栈,直接内存。

堆溢出

解决:增大值或释放内存空间。

永久区溢出,类数量过多,永久区过满。

解决方法:增大perm大小,允许class回收。

栈溢出

线程请求分配空间,如果操作系统不能无法给足够空间,也会报OOM。

减少堆的使用,让栈有跟多的空间可以使用。

减少栈的空间,可分配更多线程。

直接内存溢出(会导致OOM,但不会触发GC,)

         栈+堆+直接内存 <= 操作系统分配给内存的,是在堆外的。堆空间用的少,就会给直接内存更多空间。只不过手动触发gc是可以回收直接内存的。

支配树:

没有任何别的路径到某个节点,那就形成了支配和被支配的结构,支配者被回收,被支配者也会被回收。这个可以决定唯一路径的做法是可以估算究竟多少内存空间被占用的。

MAT如果出现内存问题,多数是由占用内存大的地方出现。

浅堆:内存结构占用内存的大小,和对象结构有关,和内容无关。只是指向内存的引用地址。

深堆:是实际对象存在占用的内存空间大小(GC主要也是收集这部分内容,和支配树是有关的,浅堆应该是记录了深堆的大小的)

Tomcat OOM案例分析:

将堆空间导出。CurrentHashMap是个高并发hashmap,使用是只加锁其中结构单元的segment(table(entry(seg_sessions(每个对象就是tomcat持有session的信息))))过多session可能导致OOM。有些属性值,访问时间,过期时间,创建时间也可以看出一段时间的平均压力情况。解决办法:堆增多,过期时间减少。(VISUAL VM,MAT工具的使用对数据的分析探索)

三:锁

1.       线程安全

  1. 统计网上人数。如果精确统计的话,多线程对一个变量操作,不加锁很可能是减少的。

多线程中,arraylist不是线程安全的,在扩充的不可用状态时是会抛出indexoutofboundException.

  1. 对象标记(hash,年龄,gc标记情况,偏向锁等信息)

2.       偏向锁,偏向已经占用这个锁的线程。为什么用偏向锁呢,认为大部分是没有锁竞争的,这时就把MARK放到偏向锁中,下次就直接进入了。当然竞争激烈的情况对性能是有影响的。偏向锁一般是在JVM启动后才启动的,启动阶段竞争会比较激烈。竞争小的情况下有5%的性能提升能力。

3.       BasicObjectLock,是放在线程栈中的一个锁。一个是对象头或指向持有这个锁的对象的指针。为了改善偏向锁的性能。

4.       如果轻量失败??

5.       自旋锁,如果我们假定线程会很快获得锁,就没有必要让操作系统进行挂起,多转几次即可。只要空转的代价小于挂起的代价,就是划算的。同步块短自旋锁成功率高。

线程会先尝试偏向锁,然后尝试轻量锁,然后尝试自旋转锁,然后普通锁。锁的级别和性能代价都是有关联的。

6.       减少锁粒度,偏向锁和轻量锁成功率就会提高,性能就会很好。 比如ConcurrentHashMap就被分成若干个Segment,说白了就是一个hashmap被拆成了多个数组,那么竞争就小了很多。

7.       减少锁的粒度也是有副作用的。

8.       锁分离也是一种方式。大家可以都拿读锁,就不需要等待了。写有排他性,比如arraylist扩展的时候禁止访问。再比如LinkedBlockedQueue就是一种读写锁分离的模式。

9.       有的时候锁的请求和释放可能是会更消耗资源的,此时我们反而减少锁的获得释放for(){synchornize(){}}可以调整为synchornized(){}

10.   通常情况下,局部变量或根本不需要加锁的代码块在运行时是会取消锁的。-xx:+DoEscapeAnalysis 进行逃逸分析,执行锁消除。

无锁:认为预期锁是不存在的,cas(compare and swap)是一种不断尝试的过程,和现实中有些事情有点像,有个预期的值和将会变化的值。更多的尝试是在应用层面判断多线程的干扰,如果有干扰则通知线程重试,程序变的复杂,但性能变的更好。比如有些原子的更新操作就是。无锁性能高于一般的有锁性能。

道可道,非常道,万事万物都没有放之四海皆准的道理,虽然一些情景下有与之相适应的方法手段,具体的还是看当时的情况来应对。

Class文件结构

1.       语言无关性,jvm直接运行的是.class文件。Jvm甚至推出了只有动态脚本语言可用的指令,甚至java都是不能使用的。单纯为jvm进行扩展。再自己实现的编译器中可以调用这个指令,以class为分界,java和jvm几乎是两块独立的部分。有很多很多语言都在jvm上得到完善的支持。

2.       class文件结构

这就是class的结构了

Magic u4 – 0XCAFFBABE表示是4byte的class文件,表明是java的class文件,   还有minor_version u2,major_version u2可以看出是那个版本编译出的class文件。

常量池:

注意,方法,字段都会引用和依赖于常量池具体内容。CONSTANT_NameAndType :是对字段的描述的。

CONSTANT_Utf8

字节长度,内容

CONSTANT_Integer

CONSTANT_String不会保存内容,保存的是索引

CONSTANT_NameAndType name名字指向,描述指向

CONSTANT_Class  也是指向的长度和具体的String值。

CONSTANT_Fieldref,CONSTANT_Methodref,CONSTANT_InterfaceMethodref,注意上面结构都是文件结构常量池中的

Access flag u2:类的标志符

在标志后面有个指向this_class u2 super_class u2指向常量池的class。

常量池的重要性,几乎所有的描述都会检索到常量池中的数据。

Interface_count u2接口每个interface是指向CONSTANT_Class的索引。

Field_count,字段访问标志。各种表示方式是不同的。Description_field,字段存放在那里。

Method_count,也是各种名称都被常量描述,简单几个字符就能表示清楚。

Attribute可以嵌套的加入很多。其中有个code就是method的字节码信息,Exceptions是method抛出的异常,LineNumberTable是用来表示行号和字节码的关系,,LocalVaribleTable,方法局部变量表描述。SourceFile,使用那个文件编译出来的,会有个说明,synthetic编译器产生的方法或字段。

Attribute的deprecated的名称。ConstantValue,大部分都是name和长度,constantvalue_index,常量值,指向常量池,可以是个自变量类型的值。

Code_attribute{

Name_index,

Length,

Statck,最大栈长

Locals,最大局部鼻梁

Code_length,字节码长度

Exception_table_length,异常数量

{

Start_pc,异常开始,code都有一些偏移量

U2 End_pc,

U2 handler_pc,

Catch_type,指向的是常量池的exception类

}exception_table(exception_table_length)

Attributes_count,属性数量lineNumberTable,VaribleTable等。

}

LineNumberTable{

Name,

Length,

{

Start_pc,

Line_number,

}line_number_table[line_number_table_length]

}

localVariableTable{

name,

length,

variable_table,length

{

Start_pc,

Length,

Name_index,

Descriptor_index,名称类型

Index , slot位置

}local_variable_table[local_variable_table_length]

}

上面是code属性中的。

Exceptions,throws部分和code属性是平级的

Name,length,num_exception,exception_table[number_of_exception]指向constant_class的索引

可以看出某些方法的属性,code值等异常信息。

SourceFile,name,length。

类型,个数,大小版本,数据类型常量,指向常量池执指针,指明长度偏移量等。

看出基本都是十六进制类型的偏移量:

根据各种偏移量及编码类型,定位到code的位置,各种结构的描述,包括最大的栈空间描述等。内部有各种code偏移量和最后由那个sourcefile编译的结果。

Class是个复杂的数据结构,是个jvm的基石,字节码指令。

JVM字节码执行过程:

Javap,字节码反汇编,然后执行。

计数器,操作数栈,局部变量表。解读下字节码指令。

字节码指令是个byte整数,左边是足助记符,右边是一个整数。所以一串字节码2A 1B B5 00 20 B1最后都能对应到相应的指令实现上。

对象操作指令:

Xxconst_xx代表xx入栈,原来底层是这么复杂的结构,局部变量入栈。

Xload,压栈

Xstore,出栈

类型转化

I2l

Invokevirtual

ASM生成java字节码,修改已有的class字节码,cglib就是用了ASM进行动态代理的。

这就是底层直接修改字节码的一种方式,基本到了贴近字节码汇编语言的状态。

如何模拟实现AOP字节码织入呢?

比如日志什么的。、

进行方法包装后

         织入字节码

然后覆盖原来的二进制class文件

有的热点代码,class执行性能可能并不理想,可以先编译成与本地平台相关的机器码,执行时性能要好的多。

热点代码,有方法体调用计数器,还有一个方法体内的循环调用计数器(可能出现栈上替换的时候)。

Xint解释执行,Xcomp全部编译执行(工作量很大),Xmixed默认混合。路漫漫其修远兮,吾将上下而求索。Jrt的编译还有很多并没有展开。

猜你喜欢

转载自my.oschina.net/u/1052786/blog/1531844
今日推荐