初识

Java SE + 扩充 = Java EE
扩充一般以 javax. 作为包名,java. 均为Java SE API的核心包,由于历史原因,核心包中也包含不少 javax.*。

JDK 1.4,引入NIO类。

2004.9.30 发布 JDK 1.5,引入java.util.concurrent 包。

JDK 1.7,引入java.util.concurrency.forkjoin。

Apach Hadoop Map/Reduce: 分布式并行运算框架。

Scale, Erlang, Clojure: 天生具备并行运算能力。

JDK 1.6 Update 14 后,提供了普通对象指针压缩功能以减缓64位虚拟机的内存消耗与性能问题(指针膨胀和数据类型对白补齐引起)。

自动内存管理机制

Java 内存区域与溢出

JVM将管理的的内存划分为:

  1. 程序计数器: 当前执行字节码的行号指示器。执行native方法时,值为空。
  2. 虚拟机栈: Java方法执行的内存模型,当执行时创建栈帧,用于存储局部变量表,操作栈,动态链接,方法出口等。调用方法则入栈,结束出栈。局部变量所需内存在编译期完成分配。大小由-Xss设置。无限递归,定义大量本地变量可发生StackOverflow(由OOM引起)。定义大量线程可发生OOM。
  3. 本地方法栈: Native方法。Sum HotSpot 将其与虚拟机栈合二为一,故-Xoss参数(设置本地方法栈大小)存在但无效。
  4. Java堆(线程间共享): 存储对象实例及数组。物理上不连续,逻辑上连续,一般设计成可拓展(通过-Xmx和-Xms实现)。GC主要区域。不断生成对象并加入List可发生OOM。
  5. 方法区(线程间共享): 存储已被加载的类信息,常量,静态变量,即时编译后产生的代码等。GC较少,JVM规范限制非常宽松。-XX:PermSize和-XX:MaxPermSize。使用反射,动态代理,CGLib可实现OOM。
  • 运行时常量池: 存放编译期生成的字面量和符号引用。具有动态性。使用Native方法String.intern()(动态生成字符串并加入运行时常量池)生成大量常量并加入List可发生OOM。
直接内存: 不属于JVM数据区。NIO使用Native函数直接分配外内存,通过存储在Java堆中的DirectByBuffer对象作为此块内存的引用。这样避免Java堆与Native堆间数据复制。其不受Java堆大小限制(-Xmx参数)。其大小由-XX:MaxDirectMemorySize控制,当不指定此值时,其默认值与-Xmx一样)。根据反射获得Unsafe实例并进行内存分配可发生OOM。

Socket缓冲区

Object object = new Object()

  1. object 以reference类型存储于Java栈的本地变量表中。
  2. Object类型所有的实例数据值以一个结构化内存形式存储于Java堆。同时包含此对象的类型数据(类,父类,实现的接口,方法等)的地址。
  3. 类型数据存储于方法区中。

reference 定位对象的方法

  1. 直接指向Java堆的地址,其中还存放指向类型数据的地址信息。Sun HotSpot采用此方法。优点: 寻址快。
  2. 指向句柄池,Java堆划分一块内存作为句柄池,其中包含对象地址(Java堆)及对象类型地址(方法区)。优点: GC代价小,对象被移动时无需更改reference.

JDK1.2 后,Java对引用进行了扩充

  1. Strong Reference: 正常的引用。
  2. Soft Reference: 系统将要发生OOM时触发第二次GC回收并将Soft Reference列入回收范围,依旧内存不足则触发OOM。
  3. Week Reference: 仅能存活到下次GC。
  4. Phantom Reference: 无法通过虚引用取得实例对象。其唯一目的是在对象被回收时收到一个系统通知。

垃圾收集器与内存回收策略

GC重点关注Java堆和方法区。

识别无用对象

引用计数

当对象相互引用时,就是二者均未再被外界引用,计数器均为1,不会被回收。Java未用此方法。

根搜索

以一系列GC root为起点搜索引用链,未与任何引用链相关联的对象为可回收。Java及其他主流商用语言(如C#)采用此方法
GC root对象包括以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈的JNI(即Native方法)引用的对象

GC过程

发现不可达到到真正GC,至少要经历两次标记过程。
  1. 发现未与GC Root相连,被第一次标记并进行筛选。对象未覆盖finalize方法或finalize已被虚拟机调用过,则无必要执行finalize。
  2. 若有必要执行finalize,将放入F-Queue。稍后由一个虚拟机建立的,低优先级的Finalize线程执行。(不承诺等待其执行完毕)
  3. 稍后GC对F-Queue进行二次标记,如果对象在finalize函数中重新与引用链建立联系,则就将在此次标记时移出“即将回收”集合。

方法区(或者HotSpot虚拟机的永久代)的垃圾回收

主要分为
*. 判断弃用常量:与回收Java堆的对象类似。
*. 判断无用类:

  1. 该类所有实例均被回收,即Java堆中不存在该类的实例。
  2. 加载该类的ClassLoader已被回收。
  3. 该类对应的java.lang.Class 未被引用。无法在任何地方通过反射访问到该类。

与对象不同,被判断为可回收后,并不一定被回收,HotSpot中可由-Xnoclassgc控制。-verbose:class, -XX:+TraceClassLoading, -XX:+TraceClassUnloading(具体可否使用有虚拟机版本限制)可查看类的加载和卸载信息。在某些场景下,类卸载是保证永久代不会溢出的关键。

垃圾收集算法

  1. 标记 - 清除(Mark-Sweep):缺点:标记和清除过程效率都不高。空间碎片太多,分配大对象时会提前触发另一次GC。多应用于老年代。
  2. 复制算法:将内存划分为大小相同的两块,每次只使用一块,只移动堆顶指针,顺序分配。当内存不足时,将存活的对象复制到另一块内存。优点:无需考虑碎片问题,实现简单,运行高效。缺点:内存缩小一半,对象存活率较高时,复制操作变多,效率变低。基于多数情况下,98%的新生代对象均为朝生夕死,商用虚拟机使用此方法回收新生代。实际使用中,将内存划分为一块较大的Eden和两块较小的Survivor,当回收时,将Eden和使用的Survivor中的存活对象拷贝到另一块Survivor并清空二者。HotSpot中,二者大小比为8:1,即仅10%会被浪费。当可回收的对象大于10%时(无法完全被拷贝到一个Survivor),需要依赖老年代进行分配担保。
  3. 标记 - 整理:标记后将存活的对象向一端移动,然后清理掉边界以外的内存。多应用于老年代。
  4. 分代收集:一般将Java堆划分为新生代和老年代。

垃圾收集器

  1. Serial:使用复制算法,利用一个线程且暂停其他所有工作线程。JDK 1.3.1之前是虚拟机新生代的唯一选择。对于单线程环境,其没有线程交互开销,专心GC使其简单而高效。时至今日,它依然是Client模式下运行的虚拟机的新生生代收集器。在用户的桌面环境,分配给虚拟机的内存一般不大,停顿时间可以接受。
  2. ParNew:使用多线程进行回收,其他与Serial相同。-XX:ParallelGCThreads可限制回收线程数目,默认与CPU数量相同。它是许多Server模式下的虚拟机首选的新生代收集器(因为只有它和Serial能与CMS配合),也是使用-XX:+UseConcMarkSweepGC后的默认新生代收集器,也可使用-XX:UsePerNewGC强制指定。
  3. Parallel Scavenge:同2一样,为新生代收集,复制算法、并行多线程。其以提高吞吐量而非停顿时间为目标(吞吐量优先收集器),适合在后台运算而不需要太多交互。-XX:MaxGCPauseMillis 控制垃圾回收最大停顿时间,-XX:GCTomeRatio 控制设置吞吐量大小(用户运行时间/GC时间)。-XX:+UseAdaptiveSizePolicy 开启后无需手动指定 -Xmm 新生代大小, -XX:SurvivorRatio Eden与Survivor比例,-XX:PetenureSizeThreshold 晋升老年代年龄等。
  4. Serial Old:Serial的老年代版本,使用 标记 - 整理 算法,依然主要用于Client模式。另Server模式下,在JDK 1.5 及之前与Parallel Scavenge 配合使用(Parallel Scavenge架构中有PS MarkSweep进行老年代收集,其以Serial Old 为模版且非常相近,所以许多常直接用Serial Old进行讲解)。同时作为CMS的后备预案,在并发收集发生Concurrent Mode Failure时使用。
  5. Parallel Old:JDK 1.6 中释出,Parallel Scavenge的老年代版本,使用多线程 标记 - 整理算法。
  6. CMS(Concurrent Mark Sweep / Concurrent Low Pause Collector):HotSpot于JDK 1.5时期推出的老年代收集器,以最短回收时间为目标,采用 标记 - 清理 算法。其运作过程为:1. 初始标记,标记GC Roots可达的对象,速度很快,需要暂停其他工作线程。2. 并发标记,进行GC Roots Tracing的过程,相对耗时较长。3. 重新标记,修正并发标记阶段因程序继续程序运作而导致标记产生的变动,比初始标记稍长,需要暂停其他工作线程。4. 并发清除。耗时较长的2和4均采用与用户工作线程并发运作的方式。缺点:对CPU敏感,其启用(CPU MUBER + 3) / 4个回收线程,当CPU不足4时,有一半被占用,严重影响吞吐量。虚拟机提供 增量式并发收集器(Incremental Concurrency Mark Sweep / i-CMS / Deprecated 不推荐使用)使用单CPU年代的抢占式模拟多任务机制,使垃圾收集周期增长,从而减小多用户的影响。无法处理浮动垃圾。可通过 -XX:CMSInitialingOccupancyFraction设置触发回收的内存余量阀值(默认68%)。-XX:+UseCMSCompactAtFullCollection开关用于在GC(标记 - 清除算法)后整理产生的碎片,此灰过程无法并发,使停顿时间变长。-XX:CMSFullGCsBeforeCompaction可设置多次GC对应一次整理。
  7. G1:于6,使用 标记 - 整理 机制。可精确控制停顿,也即在一定时间中GC消耗的时间。其将Java堆(老年代和新生代)划分为大小固定的独立区域,并跟踪区域的垃圾堆积程度,维护一张优先列表,在GC优先回收高优先级区域。它与Parallel Scanvege未使用传统GC代码框架,故无法与其他收集器配合工作。
Minor GC指新生代GC,通常小较快。Full GC / Major GC 指老年代GC,通常伴随Minor GC(Parallel Scavenge就有直接Full GC的策略选择过程),速度慢10倍以上。

其他配置:

  1. UseSerialGC: Client模式下的默认值,使用Serial + Serial Old。
  2. UseParNewGC:ParNew + Serial Old。
  3. UseConcMarkSweepGC: 使用ParNew + CMS + Serial Old(作为预备方案)。
  4. UseParallelGC:Service模式下的默认值,使用Parellel Scavenge + Serial Old(PS MarkWeep)。
  5. UseParallelOldGC:Parallel Scavenge + Parallel Old。
  6. PretenureSizeThreshold:直接晋升老年代的对象大小阀值。避免复制算法中Survivor和Eden间发生大量复制。仅Serial 和 ParNew 有效。
  7. MaxTenuringThreshold:晋升老年代的年龄。默认15。
  8. HandlePromotionFailure:是否允许担保失败。发生Minor GC时,虚拟机会检测之前每次晋升的平均对象大小是否大于老年代剩余空间,若大于则进行Full GC,若小于且此值为允许则不进行,小于且允许则进行。大多数情况打开此开关避免频繁的Full GC。
  9. PrintGCDetails:输出GC过程。
  10. -Xms:堆最小值。
  11. -Xmx:堆最大值。
  12. -Xmn:分配给新生代的大小。
  13. HeapDumpOnOutOfMemorryError:出现内存溢出时Dump出当前内存堆转储快照。
  14. -XX:+DisableExplicitGC:禁用手动触发GC(System.gc())。

内存分配与回收策略

  1. 对象优先在Eden分配。
  2. 大对象直接进入老年代:避免新生代内存间出现大量的大文件拷贝操作。
  3. 长期存活对象进入老年代。
  4. 动态对象年龄判断:如果Suvivor中相同年龄的对象占用大于一半以上空间,则此年龄及以上的对象直接进入老年代。

虚拟机性能监控与故障处理工具

主要数据来源:运行日志,异常堆栈,GC日志,线程快照(threaddump / javacore文件),堆转储快照(heapdump / hprof文件)等。

Sun JDK监控和故障处理工具:

  1. jps:JVM Process Status Tool,显示指定系统内的所有HotSpot虚拟机进程。
  2. jstat:JVM Statistics Monitoring Tool,收集HotSpot各方面的运行数据。
  3. jinfo:Configuration Info for Java,显示虚拟机配置信息。
  4. jmap:Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件)。
  5. jhat:JVM Heap Dump Brower,用于分析heapdump文件并建立服务器让用户可以在浏览器上查看分析结果。
  6. jstack:Stack Trace for Java,显示虚拟机线程快照。

可视化工具:

  1. JConsole:JDK 1.5时期提供。
  2. VisualVM:JDK 1.6首发。

调优案例分析及实战

虚拟机执行子系统

类文件结构

平台与语言无关性的基石:字节码存储格式(Class文件)。

虚拟机类加载机制

将类的描述数据从Class文件加载到内存并进行一系列操作形成可被JVM直接使用的Java类型的过程。
与编译时连接的语言不同,Java类型的 加载 和 连接 过程是在程序运行期间完成的,使其拥有动态拓展的特性。例如,编写一个使用接口的应用程序,可待运行时再确定具体实现。

类加载的时机

过程:加载 - 验证 - 准备 - 解析 & 初始化 - 使用 - 卸载。

解析和初始化顺序不定是为了支持运行时绑定(动态绑定,晚期绑定)。
验证 - 准备 - 解析 合称为连接。各过程可同时进行但开始顺序一定。

各过程的时机:
加载:虚拟机规范未强制规定。
初始化:

  1. new, get static, put static, invoke static四条字节码指令(static指代类的除final修饰、已在编译期把结果放入常量池的静态字段和静态方法)。
  2. 使用java.lang.reflect包的方法对类进行反射调用。
  3. 子类被初始化之前(与类不同,对于接口,只有真正被子类使用时才会初始化,如使用接口定义的常量)。
  4. 程序执行的主类。

所有引用类的方式,不会触发初始化,即被动引用。

  1. 通过子类引用父类的静态字段。(Sun HotSpot中,通过-XX:+TraceClassLoading 可发现子类被加载)
  2. 定义类的数组。(此时虚拟机中会使用newarray字节码指令初始化自动生成的继承于Object的名为 “[$类名” 的类。 )
  3. 访问类中的常量(因其在编译期已进入调用类的常量池,未直接引用类)。

类加载过程

  • 加载:
  1. 通过类的 全限定名 获取 定义此类的二进制字节流。
流来源有很多,如Class文件,ZIP包中获取(JAR, EAR, WAR文件),网络中获取(Applet),运行时计算(动态代理),其他文件生成(JSP)等。
  1. 将此二进制流所代表的 静态存储结构 转换为 方法区的运行时结构。
  2. 在 Java堆 中生成一个代表此类的 java.lang.Class对象 作为方法区访问此类的入口。
  • 验证:

虚拟机的一项自我保护工作,工作量占比较大。对于可信的代码集,可使用-Xverify:none关闭大部分的类验证措施。大致分为分为:

  1. 文件格式验证:经此验证后字节流才会进入内存的方法区进行存储。
  2. 元数据校验:
  3. 字节码校验:最复杂的阶段。
  4. 符号引用验证:
  • 准备:正式为类变量(被static修饰的变量,不包含实例变量)分配内存并设置类变量初始值(类型的初始值,并不进行赋值,如int初始化为0,赋值将在初始化阶段进行。对于被static final修饰的变量,在编译时已为其值生成ConstantValue属性,从时则会直接使用该值进行准确的赋值)。
  • 解析:将常量池中的符号引用(引用目标不一定已被加载到内存)引用转换为直接引用(直接指向目标的指针、相对偏移量、能间接定位到目标的句柄)。

对同一个符号引用进行解析时虚拟机可能会对结果进行缓存,在运行时常量池中记录直接引用,并把常量标记为已解析。

当子类和父类声明同名Static字段,编译器将拒绝编译。接口均为public故针对接口的解析一般不会抛出java.lang.IIlegleAccessError。
  • 初始化: 执行类构造器<client>()方法。其在编译期收集所有类变量的赋值语句和静态代码块(static{})中的语句。

类加载器

类加载过程被放置在虚拟机外部,以便应用自行决定如何去获取类。实现此动作的代码块称为 类加载器。

  • 启动类加载器(Bootstrap ClassLoader): 加载java_home/lib目录、被-Xbootclasspath指定的目录、并且被虚拟机识别的类库到虚拟机内存。与下面所述的加载器不同,其无法被Java直接引用,对于HotSpot,其使用C++实现。
  • 扩展类记载器(Extension ClassLoader): 由sun.misc.Launcher$ExtClassLoader实现,负责加载java_home/lib/ext目录、被java.ext.dirs系统变量指定的目录中的类库。
  • 应用程序加载器(Application ClassLoader): 由sun.misc.Launcher$AppClassLoader实现,是ClassLoader.getSystemClassLoader()方法的返回值。

双亲委派模型

JDK1.2中被引入。

按上所述顺序形成父子类加载器(不以继承(Inheritance)关系而以组合(Composition)方式实现来复用毒加载器的代码),当子类收到类加载请求,则先委派给父加载器去完成(如果类还未被加载的话),当父加载器抛出ClassNotFoundException后,再调用自己的findClass()方法进行加载。其主要代码集中在java.lang.ClassLoader的loadClass()方法中。此模型使得不管哪个加载器要加载特定的类,最终均会使用同一加载器完成,即为同一个类,实现统一性(不同加载器加载的同一Class属于不同类)。

被破坏的双亲委派模型:

  1. 例如JNDI、JDBC、JCE、JAXB、JBI等(代码由启动类加载器加载)涉及SPI(Service Provider Interface, 接力提供者)的加载动作, 需要调用独立厂商实现并部署在ClassPath下的接口提供者代码(启动类加载器并不认识)。为此新增线程上下文类加载器(通过java.lang.Thread.setContextClassLoader()设置,若创建线程时为设置,则从父加载器继承一个,若全局范围均未设置,也默认为应用类加载器),使父加载器可以请求子加载器完成加载操作。
  2. OSGi环境下,为实现模块化热部署,类加载进一步发展为网状结构。

虚拟机字节码执行引擎

每个方法从调用开始到完成均对应着一个栈帧从入栈到出栈的过程。

编译程序代码时,栈帧中需要多大的局部变量和多深的操作数栈都已经确定。一个栈帧分配多少内存不受运行期变量数据影响。

运行时栈帧结构:

  • 局部变量表: 一组变量值存储空间,以变量槽(Slot)为最小范围,单位大小可简单理解为32位长度的内存空间。对于64位数据类型,以高位在前为其分配两个Slot。虚拟机使用Slot的索引值(0值开头)使用局部变量表。如果是实例方法(Not Static Method),则局部变量表中的第0位索引的Slot默认是用于传递方法所述对象实例的引用,也即this关键字所访问的的参数。Slot可被重用,若字节码PC计数器已超过某变量运用域,则变量对应的Slot可交由其他变量使用。不同于类变量,局部变量无“准备阶段”,即不会存在默认值(Boolean类型默认为false是不存在的)。
  • 操作数栈(操作栈): 其中内容可为任意Java数据类型。方法开始执行时,栈为空,当做诸如算法等操作时,字节码指令会向栈中提取和写入内容。原则上两个栈帧相互独立,但作为优化,会有部分重合,使方法调用无须额外的参数复制。解释执行引擎即基于栈的执行引擎,栈即指操作数栈
以下三项可统称为栈帧信息
  • 动态连接: 各栈帧均包含指向运行时常量池中该帧所属方法的引用以支持动态链接。
  • 方法返回地址: 调用者的PC计数器可以作为返回地址。当有返回值时,会将其压入调用者的操作数栈中。
  • 附加信息: 虚拟机允许添加规范中为涉及的信息,如调试信息。

方法调用

虚拟机编译不包含连接过程,一切方法调用在Class文件中都是符号引用,非实际内存入口,使Java拥有强大的扩展能力,但方法调用更为复杂。

  • 解析: 每个目标调用方法在Class文件里都是一个常量池中的符号引用。对于静态方法私有方法,实例构造器,父类方法,final修饰的方法五类(invokestatic,invokespecial, invokevirtual, invokeinterface字节码指令中的前两个所调用的方法及final修饰的方法),在类加载的时候就会把符号引用解析为直接引用,称为解析调用(对应于分派调用,这些方法称为非虚方法)。
  • 分派:

静态分派:

static abstract class Human{}
static class Women extends Human{}
static class Man extends Human{}

Human为静态类型(外观类型),Women和Man为实际类型。虚拟机(编译器)在重载时通过静态类型判断,其在编译期是可知的。当以以上三个类为参数不同点进行重载时,编译器会编译期生成invokevirtual指令,调用Human参数的重载方法。使用静态类型定位调用方法版本的分派动作称为静态分派。当类线性实现接口时,针对接口变量的重载将成回溯形形式定位调用方法版本,当某处同时实现两个接口且两个接口均存在重载方法时会拒绝编译。此时调用重载方法时需要显式类型转换。

动态分派: 当调用Women重写于Human的方法时,执行的invokevirtual指令会先找到操作数栈第一个元素指向的对象实例类型(也即实际类型)。如果其中找到所需方法则进行权限检验(未通过则抛java.lang.IIlegalAccessError),否则按继承关系向上搜索与验证。若始终找不到,抛java.lang.AbstractMethodError。invokevirtual指令把常量池中的类方法符号引用解析到不同的直接引用上,此过程即为方法重写的本质。在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

  • 单分派和多分派: 静态多分派,动态单分派。
father.say(_360)
song.say(qq)

编译阶段,静态分派使用静态类型进行定位,最终生成Father.say(_360)和Father.say(qq)两条invokevirtual指令。其根据两个宗量(目标方法所有者和方法参数称为宗量)进行选择,故称为多分派
运行阶段,确定两条指令的确切目标是Father还是Son,参数将不再成为选择因素,故一个宗量进行选择称为单分派

  • 动态分派的优化

动态分派非常频繁且需要运行时在类的方法元数据中搜索合适的目标方法,出于性能考虑,会有一些优化手段。比如使用虚方法表(virtual method table,接口方法表与此类似)存储各个方法的实际入口,父子类未重写的方法入口地址将相同。

基于栈的字节码解释执行引擎

基于栈的指令集和基于寄存器的指令集

基于栈的解释器的执行过程

类加载及执行子系统的案例与实战

程序编译与代码优化

  1. 前段编译器: .java转换成.class的过程,Sun的Javac,Eclipse JDT中的增量式编译器(ECJ)。javac使用语法糖来改善程序员的编码风格和效率。
  2. 运行期编译器(JIT编译器): 字节码转换成机器码,HotSpot VM的C1, C2编译器。优化的主要阶段,使非javac编译的Class文件也可以享受编译器优化带来的好处。
  3. 静态提前编译器(AOT编译器): .java直接转换成机器码,GNU Complier for the Java(GCJ),Excelsior JET。

Javac 编译器

  • 解析与充填符号表: 词法分析,将源码中字符流转换成标记(Token)集合,标记指一个关键词如int。语法分析将根据Token序列构造抽象语法树(AST)。
  • 注解处理器
  • 语义分析与字节码生成: 对AST上正确的源码进行上下文有关性审查(如类型审查)。标注检查,数据及控制流分析,解语法糖,字节码生成。

语法糖

泛型与类型擦出

Java与C#不同,使用伪泛型,在编译生成的字节码文件中,泛型被替换成了原生类型,并在相应的地方进行了强制类型转换。如New Map<String, String>实际上是New Map,.get的时候强制类型转换为String。故使用泛型的Map作为不同之处进行重载时,无法被编译。

自动装箱,拆箱与循环遍历

==操作在没有遇到算数运算的情况下不会拆箱,equal方法不会处理数据转型关系。

条件编译

编译器会将分支中不成立的代码删除(如: if False)。其也在解语法糖阶段完成。

晚期(运行期)优化

即时编译器JWT(Just InTime Compiler),无特殊说明均指HotSpot虚拟机的相应模块。

解释器和编译器

解释器使程序快速启动和执行,省去编译时间。随着时间的推移,越来越多代码被编译成本地代码之后,可获得更高的执行效率。
HotSpot内置两个即时编译器,Client Complier(C1编译器)和Server Complier(C2编译器)。默认采用一个解释器和其中一个编译器直接配合使用。具体可设置。
分层编译: 第0层,程序解释执行,解释器不开启性能监控功能。可触发第1层编译。第1层,也称C1编译,将字节码编译成本地代码,进行简单优化,可加入性能监控逻辑。第2层,也称C2编译,于2启动了一些编译耗时较长的优化。甚至会根据性能监控信息进行一些不可靠的激进优化。
实施分层编译后,C1与C2同时工作,许多代码会被多次编译,使用C1获得更好的编译速度,C2获得更好的编译质量。

编译对象与触发条件

判断一段代码是不是热点代码,需不需要触发即时编译,此行为称为热点探测。包含以下两种方法:

  1. 基于采样: 周期性检查栈顶。认为经常出现在栈顶的方法是热点方法。简单高效但不精确如遇到线程阻塞时会出现严重误判。
  2. 基于计数器: 为每个方法(甚至是代码块)建立计数器,计数超过阀值(-XX:CompileThreshhold, Client 模式下默认值1500,Server 10000)则认为是热点。HotSpot采用此方法。调用方法时,检查是否有JIT编译后的版本,无也加计数器,若达阀值则提交代码编译请求而不等待。调用计数器存在热度衰减,半衰周期后将计数值减半,此动作是垃圾回收时顺便进行的,可使用-XX:UseCounterDecay关闭,-XX:CounterHalfLifeTime设置半衰周期,单位s。回边计数器用于统计一个方法中循环代码的执行次数。在字节码中遇到控制流向后跳转的指令就称为回边。同样可设定,超过阀值提交一个OSR编译请求,但不存在热度衰减。

编译过程

-XX:BackgroundComplilation可禁止后台编译。
C1: 第一阶段,将平台独立的

编译优化技术

方法内联

对于虚方法(实例方法默认是虚方法),无法直接使用方法内联,优化器会做其他处理以保证可以内联,可加入final修饰已略微提高响应。

Java与C++编译器对比

也即即时编译和静态编译的对比。

Java内存模型与线程

处理器可能会对输入的代码进行乱序执行,最后会将乱序执行的结果重组,保证最终结果一致。
Java虚拟机的即时编译器中也有类似的指令重排序优化。
C/C++等语言直接是用物理硬件(操作系统的内存模型),因此会由于不同平台内存模型的差异,在不同平台并发时可能会出现问题。

主内存和工作内存

需虚拟机拥有主内存,各线程拥有自己的工作内存。工作内存中存放的是需要使用的变量的主内存的副本拷贝,其对线程的操作均在工作内存完成,后进行回写。

volatile型变量

轻量级的同步机制,被其修饰的变量在被线程修改时会立即对其他线程可见,即修改会立即同步到主内存,读取是会立即从主内存刷新。此处仅仅是可见,依然会出现读取数值后对数值进行修改所带来的并发结果被覆盖的情况。若是直接赋值-修改的值不依赖当前值则可达到线程安全。
同时,可禁止指令重排序,即针对此变量的操作,其之前的代码一定会在此操作之前完成,其之后的也在其之后执行。但不保证其之前和之后的代码不会发生指令重排序。
典型应用场景为,使用一个参数辨识系统启动完成,在启动后将该参数赋True值,但由于指令重排序,其有可能不会等到系统启动完成就被赋值。

其它实现可见性机制: synchronized: 每次unlock之前,必须把变量同步回主内存。final: 一旦在构造器中被初始化,一般情况下其他线程就能看到final字段的值。

synchronized基于一个变量同一时刻只能被一个线程lock。

先行发生原则

Java与线程

  • 使用内核实现: 程序一般不会直接使用内核线程(Kernel Thread, KLT),而是去使用内核的一个高级接口,轻量级进程(Light Weight Process, LWP)。这种轻量级进程与内核线程1:1的关系称为一对一的线程模型。
  • 使用用户线程(User Thread, UT)实现: 广义上讲,非内核线程即为用户线程。狭义上,其完全建立在用户空间的线程库上。这种进程与用户线程之间1:n的关系称为一对多的线程模型。
  • 混合实现: 用户线程与轻量级进程的数量比不定,称为M:N的线程模型。
  • 线程模型基于操作系统的原生线程模型实现。Windows and Linux采用一对一。线程模型。

Java线程调度

协同式( operative Threads-Scheduling)线程调用: 线程自行控制其执行时间。不稳定,容易存在坚持不让的情况。
抢占式(Preemptive ThreadsScheduling) 线程调用: 执行时间由系统分配,Java采用此方式

Java中的线程优先级是不稳定的,因为其实际是映射到系统的原生线程实现,其有自己的优先级机制。

进程状态转换

新建。
运行。
无限期等待: 需要主动唤醒。如没有设置timeout的Object.wait和Thread.join,LockSupport.park()。
限期等待: 一段时间后自动唤醒,如设置了timeout的Object.wait和Thread.join,LockSupport.parkNanos, LockSupport.parkUntil。
阻塞: 等待某个事件。
结束。

线程安全与锁

线程安全级别:

  1. 不可变: 被final修饰变量,在构造函数赋值结束后,在未发生this指针逃逸时此变量将永远是线程安全的。
  2. 绝对线程安全: Vector的所有方法均被synchronized修饰,但并不代表并发的读写是安全的,只能说并发的读和并发的写是安全的。
  3. 相对线程安全: Vector,HashTable, Collections的synchronizedCollection方法包装的集合等。
  4. 线程兼容: ArrayList,HashMap等可在调用端实现线程安全的。
  5. 线程对立: 无法在多线程中使用代码。

线程安全的实现方法

  1. 互斥同步: synchronized 修饰符(编译之后会在同步块的前后调用moniterenter和monitorexit两个字节码指令已锁定和解锁制定的对象,不指定则有默认规则)等互斥机制,如临界区,互斥量,信号量。由于Java线程是映射到系统的原生线程上,阻塞和唤醒一条线程均需要操作系统从用户太转换为核心态,其需要大量的处理器时间。所以对于代码简单的同步块,如被synchronized的setter和getter,其转换时间可能比代码执行时间长,故synchronized 的是一个重量级操作,无非必要无需使用。java.util.concurrent.ReentrantLock于其: 等待中断,即等太久可选择放弃而执行任务。可选择性开启公平锁,多个等候时按时间顺序获得锁。绑定多个条件。出于性能考虑,二者优先推荐synchronized。互斥同步可认为是悲观锁。
  2. 非阻塞同步: 乐观锁机制,java.util.current.AtomicInteger等原子类,其会先进行操作,若最后发现有资源争用,产生了冲突,则进行补救,如再次尝试直至成功。
  3. 无同步方案: 可重入性,如果一个方法,其结果时可预测的,输入同一个值均能得到相同结果。可重入的代码均是线程安全的,反之不成立。线程本地存储,如果一个变量是线程共享的,可使用volatile修饰。线程独占数据可使用java.lang.ThreadLocal存储。

锁优化

  • 自旋锁与自适应自旋

    • 线程请求锁后,执行一个忙循环(自旋),当锁占用时间很短时,请求锁的线程无需进行阻塞/唤醒,效率有很大提高。当自旋次数超过默认值10时,则进入阻塞。自适应循环则是动态决定自旋次数甚至不进行自旋
  • 锁消除

    • 对于编译器和程序员添加的锁,若在即时编译时被认为是针对线程私有,不存在逃逸的变量或代码块加锁,则会被消除。
  • 锁粗化

    • 如果虚拟机探测到一串零碎的操作都对同一对象加锁,则会把加锁单位扩大到整个操作序列的外部。
  • 轻量级锁

    • 1.6后加入了新型锁。

Eriasuitor
21 声望1 粉丝