转《深入理解Java虚拟机》学习笔记之最后总结

  1. 云栖社区>
  2. 博客>
  3. 正文

转《深入理解Java虚拟机》学习笔记之最后总结

无信不立 2015-05-15 14:02:00 浏览1175
展开阅读全文

编译器

Java是编译型语言,按照编译的时期不同,编译器可分为:

  1. 前端编译器:其实叫编译器的前端更合适些,它把*.java文件转变成*.class文件,如Sun的Javac、Eclipse JDT中的增量式编译器ECJ;
  2. JIT编译器:虚拟机的后端运行期编译器(Just In Time Compiler),它把字节码转变成机器码,如HotSpot VMd C1、C2编译器;
  3. AOT编译器:静态提前编译器(Ahead Of Time Compiler),它直接把*.java文件编译成本地机器码,如GUN Compiler for the Java(GCJ)、Excelsior JET;

前端编译器做了许多针对编码过程的优化措施来改善程序员的编码风格和提高编码效率,如相当多新生的Java语法特性,都是靠前端编译器的“语法糖”来实现 的;而虚拟机设计团队把性能的优化集中到了JIT编译器,这样可以让那些不是有Javac产生的Class文件也同样能享受到编译器优化所带来的好处。

Java程序最初是通过解释器(Interpreter)进行解释执行的,后来在部分的商用虚拟机(Sun HotSpot、IBM J9)中,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码(Hot Spot Code)”,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器 称为即时编译器(Just In Time Compiler)。

尽管并不是所有的Java虚拟机都采用解释器和编译器并存的架构,但许多主流的商用虚 拟机如HotSpot、J9等都同时包含解释器和编译器:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。当程序运 行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。

HotSpot虚拟机中内置了两个即时编译器分别是Client Compiler和Server Compiler,或者分别称为C1和C2。但无论是采用的编译器是Client Compiler还是Server Compiler,解释器和编译器搭配使用的方式在虚拟机中都被称为“混合模式(Mixed Mode)”,不过可以通过参数-Xint强制虚拟机运行于“解释模式(Interpreted Mode)”,此时编译器完全不介入工作,全部代码都使用解释方式执行;另外也可以使用参数-Xcomp强制虚拟机运行于“编译模式(Compiled Mode)”,这时将优先采用编译方式执行,但解释器仍然要在编译无法进行的情况下介入执行。

编译优化

由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所花 费的时间就可能越长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行的速度也有影响。为了在程序启动响应速度 和运行效率之间达到最佳平衡,HotSpot虚拟机将会逐渐启用分层编译策略,分层编译的概念在JDK1.6时期出现,后来一直处于改进阶段,最终在 JDK1.7的Server模式虚拟机中作为默认编译策略开启。分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括:

  • 第0层:程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译;
  • 第1层:也称为C1编译,将字节码编译为本地代码,进行简单可靠的优化,如有必要将加入性能监控的逻辑;
  • 第2层(或2层以上):也称为C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化;

实施分层编译后,Client Compiler和Server Compiler将会同时工作,许多代码都可能会被多次编译,用Client Compiler获取更高的编译速度,用Server Compiler来获取更好的编译质量,在解释执行的时候也无需再承担收集性能监控信息的任务。

在运行过程中被即时编译器编译的“热点代码”有两类:被多次调用的方法和被多次执行的循环体,但不管是那种都是以整个方法作为编译对象的,因为这种编译方式发生在方法的执行过程中,因此be形象地称为栈上替换(On Stack Replacement,OSR)。

要知道一段代码是不是热点代码,是不是需要触发即时编译,这个行为称为热点探测,目前主要的热点探测判定方式有两种:

  • 基于采样的热点探测:虚拟机会周期性地检查各个线程的栈顶,如果发现某个(或某些) 方法经常出现在栈顶,那这个方法就是“热点方法”。基于采样的热点探测优点是实现简单高效;缺点是结果不精准。(比如某个线程阻塞了,栈顶一直是方法A, 虚拟机周期性采样都只是探测到这个方法A……)
  • 基于计数器的热点探测:采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认定它是“热点方法”。

HotSpot虚拟机中使用的是第二种——基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器。在确定了虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过了阈值,就会触发JIT编译。

方法内联编译器优化重要优化手段,因为它不仅消除了方法调用成本,更重要的意义是它是其他优化手段的基础,因为只有把代码集中了才能更方便和有效地进行优化。如果想查看即时编译的情况可以使用参数:-XX:+PrintCompilation。

JVM内存模型

主内存和工作内存

  • 所有的变量都存储在主内存中
  • 每个线程都还有自己的工作内存,拥有主内存的对象的拷贝
  • 线程只能操作自己的工作内存,线程间的交互只能通过主内存通讯

内存间交互操作:java内存模型定义了8种原子操作,jvm要报保证每一个操作为原子操作:

  1. lock(锁定,作用于主内存的变量);
  2. unlock(解锁,作用于主内存的变量);
  3. read(读取,作用于主内存);
  4. load(载入,放入到工作内存中);
  5. use(作用于工作内存的变量);
  6. assign(赋值,作用于工作内存);
  7. store(存储,作用于工作内存的变量);
  8. write(写入,作用于主内存的变量);

 

如果要把一个变量从主内容复制到工作内存,那就要顺序地执行read和load操作,如果把变量从工作内存同步回主内存,就要顺序地执行store和write操作。注意java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。
以上8中操作必须满足的规则:

  1. 不允许出现read和load,store和write操作之一单独出现
  2. 不允许一个线程丢弃他的最近的assign操作
  3. 不允许一个线程无原因的把数据从线程的工作内存同步到主内存中
  4. 一个新变量只能在主内存中”诞生”,不允许在工作内存中直接使用一个未被初始化的变量
  5. 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会解锁;
  6. 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作初始化变量的值;
  7. 如果一个变量事先没有被lock操作锁定,那就不允许对他执行unlock操作
  8. 对一个变量执行unlock之前,必须先把变量同步会主内存(执行store,write);

volatile
volatile--java虚拟机提供的轻量级的同步机制。2个重要特性。1是保证此变量对所有线程可见,2是禁止指令重排序优化,

  • 次 使用之前都要先刷新,执行引擎看不到不一致的情况,保证可见性;但volatile变量在各个线程的工作内存中不存在一致性问题(也可以存在不一致的情 况),但java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。(java内存模型规定,load和use动作连 续,store和write动作连续)
  • 指令重排从硬件上来讲,指令重排序指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。但并不是说指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。

性能方面:volatile的读操作的性能消耗与普通变量几乎没有差别,但是写操作则可能会慢一些,因为他需要在本地代码中插入许多内存屏蔽指令来保证处理器不发生乱序执行。

以下场景仍需要同步:

  • 运算结果并不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值;
  • 变量不需要与其他的状态变量共同参与不变约束;

原子性、可见性和有序性(总结):

  • 原子性:read,load,assign,use,write
  • 可见性:java内存模型是通过变量修改后将新值同步回主存,在变量读取前从主存刷新变量值这种依赖主存作为传递媒介的方式来实现可见性的。volatile,synchronized,final(this引用逃逸除外;)
  • 有序性:如果在本地线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的;

逃逸分析:当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。(来自互联网)

现行发生原则

指的是java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。

java内存模型下的“天然的”先行发生关系:

  1. 程序次序规则:一个线程,代码顺序(控制流顺序);
  2. 管程锁定规则:
  3. volatile变量规则:写操作先行发生于后面的读操作
  4. 线程启动规则:start()方法先行发生于此线程的每一个动作;
  5. 线程终止规则:join
  6. 线程中断规则:
  7. 对象终结规则;初始化先于finalize();
  8. 传递性;

线程状态

  • 新建new
  • 运行runable
  • 无期限等待waiting
  • 期限等待timed waiting
  • 阻塞blocked
  • 结束terminated

线程安全的实现方法

  • 互斥同步(阻塞同步):互斥是因,同步是果;互斥是方法,同步是目的
    java.util.concurrent.ReentranLock,比synchron增加了一些高级特性:等待可中断、可实现公平锁、以及锁可以绑定多个条件;
  • 非阻塞同步:(通俗的说就是不断地重试,知道成功为止),乐观的并发策略,需要硬件指令集的支持;

锁优化

    • 自旋锁与自适应自选(cas)
    • 锁消除(判定依据是逃逸分析),String类,字符串相加,JDK1.5之前转化为StringBuffer类(线程安全);JDK1.5及以后,之后会StringBuilder
    • 锁粗化,范围扩大
    • 轻量级锁,jDK1.6加入,
    • 偏向锁

网友评论

登录后评论
0/500
评论
无信不立
+ 关注