转载

深入理解JVM备忘录

初识

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。

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),
原文  https://segmentfault.com/a/1190000020733505
正文到此结束
Loading...