- Xm
- 一.概述
- 2.Java程序的执行流程
- 二.Java内存区域与内存溢出异常
- 三.垃圾收集
- 四.JVM监控与故障处理工具
- 六.Class文件结构
- 七.虚拟机类加载机制
- 八.字节码的执行
- 九.Java语法糖
Xm
-Xmx3550m -Xms3550m -Xmn2g -Xms1024m -Xmx1024m -XX:PermSize=64M -XX:MaxNewSize=256m -XX:MaxPermSize=128m
一.概述
HotSpot VM是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。以下部分的讲解为说明的情况下都是以JDK1.6为基础,JVM思路是以HotSpot为基础。
注意:虽然Android也使用Java语言,但是其虚拟机是谷歌公司的Dalvik,成为DVM。安卓的SDK和DVM实现思路与下文中区别很大,请不要混淆。
1.Java程序的组成
自定义类库:编译后的class文件集合
JDK或JRE类库:基础的class文件的集合
JVM虚拟机:一般是C++编写的,用于将字节码文件解释成特定平台的机器码的程序,此程序在不同平台的动态链接库是不同的(windows下是jvm.dll,linux下是libjvm.so)
其中JDK还包含一些工具,如编译器和JavaDoc等
理解:Java程序运行过程中,JVM规范的思想一部分是由JVM实现,还有一部分由JDK的类库所实现,至于程序的实际逻辑则是由开发人员编写的类决定。 #
2.Java程序的执行流程
二.Java内存区域与内存溢出异常
1.JVM规范的内存模型
Java虚拟机规范有以下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。
理解:这里是JVM规范的内存模型,不代表实际虚拟机的实现模型,比如方法区和堆是逻辑上隔离的,但是随着HotSpot升级,方法区和堆的关系发生了变化,详见2.2。
程序计数器
当前线程所执行的字节码的行号指示器,每个线程有一个,互不影响。如果执行的是Native方法,则计数器是空(Undefined)。唯一不会抛内存异常的区域。
方法区
线程共享的内存区域,存储加载的类信息、常量、静态变量和编译后的代码数据。注意这里的变量是基本数据类型或者是对象的引用。
运行时常量池
这是方法区的一部分,主要是用于存放编译生成的符号引用,一般也会存放直接引用。
理解:类、方法、字段等都是引用,直接引用可以理解为确定的指向某块数据,但是符号引用一个标志,间接指向的数据甚至可能不存在或者不存在,所有符号引用使用时必须要转变成直接引用。
理解:规范只说明了运行时常量池,实现时常量池分为多种,除了运行时常量池还有字符串常量池和Class常量池。
Java虚拟机栈
线程私有的与线程生命周期相同,每个方法执行时会有一个栈帧,每个栈帧包括以下部分:
-
局部变量表:编译时就可以确定大小,存放的是方法参数和方法内部定义的变量,包括基本数据类型、对象引用和返回地址。
-
操作数栈:“基于栈的执行引擎”指的就是操作数栈,方法执行会中,会有加操作、赋值操作等,都会对操作数栈进行入栈和出栈。
-
动态连接:**理解:动态连接指向的是栈帧所属的方法,在类加载的过程中(详见7.1)部分符号引用解析成了直接引用,叫做静态解析,但是还有部分符号引用需要在运行时转成直接引用,此时确定的对应的直接引用就是动态连接。这样,虽然常量池的符号引用相同,但是栈帧中的动态连接不同就可以实现多态。
- 方法返回地址:对于方法,正常返回和异常抛出都需要回到方法被调用的位置。
线程请求的栈深度过大会抛出StackOverflowError,如果扩展栈空间内存不足会OutOfMemoryError。
本地方法栈
为虚拟机使用本地方法服务。
Java堆
所有线程共享,存放几乎所有对象实例,是垃圾回收的主要区域。
直接内存
实际不属于虚拟机的运行数据区,但是收到机器内存大小限制,主要是在NIO中使用,需要用代码主动回收。
2.HotSpot内存模型
从物理上划分,内存分为JVM进程内存和进程之外的系统内存,其中JVM进程内存分为堆内存(Heap)和本地内存(Native Memory),其中堆内存是JVM管理和回收的主要部分。本地内存一部分用于线程栈,还有一块是CodeCache代码缓存,用来存放JIT编译后的代码,JIT是用来把字节码转化为程序指令。CodeCache逻辑上属于方法区,但是物理上是是独立的。直接内存也是通过本地内存向系统申请的。
(1)JDK1.6版本的内存模型:
HotSpot方法区也成为永久代,此时永久代存放的是:类的元数据信息、运行时常量池、静态字段、Class对象实例或者说Class常量池、字符串常量池。而且物理上永久代依然是堆空间的一部分。
(2)jdk1.7的内存模型
静态字段、Class对象实例和字符串常量池移动到堆中。
(3)JDK1.8的内存模型
删除了永久代,用元空间代替:
MetaSpace存储类的元数据,MetaSpace直接申请在本地内存中,这样类的元数据分配只受本地内存大小的限制,但是依然可以用JVM参数指定。
3.对象的创建
Object object = new Object();
假设该语句出现在方法体中,object会作为引用类型(reference)的数据保存在Java栈的局部变量表中,而会在Java堆中保存该实例化对象,Java堆中还保存对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等),这些类的数据则保存在方法区中。
引用的主流的访问方式有两种:使用句柄池和直接使用指针。
句柄访问方式在对象移动时只会改变句柄的数据指针,而引用本身不需要修改。使用直接指针访问方式是速度快。目前HotSpot虚拟机采用的便是直接指针。
4.内存溢出异常
三.垃圾收集
1对象引用
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。但在JDK1.2之后,Java对引用的概念进行了扩充,将其分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,引用强度依次减弱。
2.对象存活的判断
引用计数算法
对象有引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器都为0的对象就是不可能再被使用的。 实现简单高效,但是很难解决对象之间的相互循环引用问题。
可达性分析算法
通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。在Java语言里,可作为GC Roots的兑现包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
本地方法栈中JNI(Native方法)的引用对象。
3.垃圾收集算法
判定除了垃圾对象之后,便可以进行垃圾回收了。下面介绍一些垃圾收集算法,由于垃圾收集算法的实现涉及大量的程序细节,因此这里主要是阐明各算法的实现思想,而不去细论算法的具体实现。
标记—清除算法
标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。标记—清除算法的执行情况如下图所示:
该算法有如下缺点:
标记和清除过程的效率都不高。
标记清除后会产生大量不连续的内存碎片。
复制算法
复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它讲课用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。
复制算法有如下优点:
每次只对一块内存进行回收,运行高效。
只需移动栈顶指针,按顺序分配内存即可,实现简单。
内存回收时不用考虑内存碎片的出现。
它的缺点是:可一次性分配的最大内存缩小了一半。
复制算法的执行情况如下图所示:
回收前状态:
回收后状态:
标记—整理算法
复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。标记—整理算法的回收情况如下所示: 回收前状态:
回收后状态:
分代收集
都采用分代收集,它根据对象的存活周期的不同将内存划分为几块,一般是分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
4.垃圾回收分析
在用代码分析之前,我们对内存的分配策略明确以下三点:
对象优先在Eden分配。
大对象直接进入老年代。
长期存活的对象将进入老年代。
对垃圾回收策略说明以下两点:
新生代GC(Minor GC):发生在新生代的垃圾收集动作,因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):发生在老年代的GC,出现了Major GC,经常会伴随至少一次Minor GC。由于老年代中的对象生命周期比较长,因此Major GC并不频繁,一般都是等待老年代满了后才进行Full GC,而且其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。
5.简单性能调优
-
在机器内存足够大的情况下,分配更多的堆内存来提高速度,但是需要控制FullGC的频率。控制Full GC频率的关键是保证应用中绝大多数对象的生存周期不应太长,尤其不能产生批量的、生命周期长的大对象,这样才能保证老年代的稳定。
-
分配超大堆时,如果用到了NIO机制分配使用了很多的Direct Memory,则有可能导致Direct Memory的OutOfMemoryError异常,这时可以通过-XX:MaxDirectMemorySize参数调整Direct Memory的大小。
-
线程栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或OutOfMemoryError(横向无法分配,即无法建立新的线程)。
-
Socket缓冲区:每个Socket连接都有Receive和Send两个缓冲区,分别占用大约37KB和25KB的内存。如果无法分配,可能会抛出IOException:Too many open files异常。
-
JNI代码:如果代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中。
-
虚拟机和GC:虚拟机和GC的代码执行也要消耗一定的内存。
6.垃圾收集器
JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:
-
Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它只用一个CPU/一个线程去完成GC, 且在进行垃圾收集时必须暂停其他所有的工作线程(“Stop The World” -后面简称STW),使用的是复制算法。
-
Serial Old是Serial收集器的老年代版本,同样是单线程收集器,使用标记整理算法。作为CMS收集器的后备预案。
-
ParNew收集器是Serial的多线程版本,许多运行在Server模式下的虚拟机中首选的新生代收集器,包括Serial可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等,是CMS默认的新生代收集器。
-
Parallel Scavenge收集器也是新生代收集器,使用复制算法又是并行的多线程收集器,它的目标是达到一个可控制的运行用户代码跟(运行用户代码+垃圾收集时间)的百分比值。
-
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。
-
Concurrent Mark Sweep (CMS)收集器是一种以获得最短回收停顿时间为目标的收集器,基于标记清除算法。过程如下:初始标记,并发标记,重新标记,并发清除,优点是并发收集,低停顿,缺点是对CPU资源非常敏感,无法处理浮动垃圾,收集结束会产生大量空间碎片。
-
G1收集器是基于标记整理算法实现的,不会产生空间碎片,可以精确地控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First)。
四.JVM监控与故障处理工具
jps:jvm process status tool,显示指定系统内所有的hotspot虚拟机进程
jstat:jvm statistics monitoring tool,用于收集hotspot虚拟机各方面的运行数据
jinfo:configuration info for java,显示虚拟机配置信息
jmap:memory map for java,生成虚拟机的内存转储快照(heapdump文件)
jhat:jvm heap dump browser,用于分析heapmap文件,它会建立一个http/html服务器让用户可以在浏览器上查看分析结果
jstack:stack trace for java ,显示虚拟机的线程快照
六.Class文件结构
1.平台无关
别的语言也可以变成Class文件,只要Class符合结构,就可以在Java中运行。Java语言的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更强大,而且这也正是在类加载时要进行安全验证的原因。
2.类文件结构
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。
magicNumber与版本
开头四个字节唯一作用是判断该文件是否为一个能被虚拟机接受的Class文件。紧接着magic的4个字节存储的是Class文件的次版本号和主版本号
constant_pool常量池
存放字面量和符号引用。字面量接近于Java的常量,如文本字符串、被声明为final的常量值等。而符号引用总结起来则包括了下面三类常量:
类和接口的全限定名(即带有包名的Class名,如:org.lxh.test.TestClass)
字段的名称和描述符(private、static等描述符)
方法的名称和描述符(private、static等描述符)
JVM在加载Class文件时才会进行动态连接,也就是说,Class文件中的符号引用需要在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。
符号引用以一组符号来描述所引用的目标,直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
access_flag访问标志
类或接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,abstract类型,如果是类的话,是否声明为final,等等。
this_class、super_class、interfaces 类索引 父类索引 接口索引集合
类索引和父类索引是类描述符常量,通过该常量中的索引值找到全限定名字符串。而接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口的索引集合中。
fields字段表
描包括了类级变量或实例级变量,但不包括在方法内声明的变量。字段的名字、数据类型、修饰符等都是无法固定的,只能引用常量池中的常量来描述。
字段表集合中不会列出从父类或接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段。比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
methods方法表
结构与属性表的结构相同,方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里。如果父类方法在子类中没有被覆写,方法表集合中就不会出现来自父类的方法信息。但同样,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“
attributes属性表
Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
七.虚拟机类加载机制
1.类加载过程
图中的顺序是过程开始的顺序,其中加载、验证、准备、初始化的先后顺序是确定的,解析有可能是在初始化之后开始,这样可以实现动态绑定或晚期绑定。初始化之后就是加载完成,使用和卸载不属于加载过程。
绑定是将类与对应的方法关联起来,静态绑定是编译时关联(final,static,private,构造方法),动态绑定是运行时决定类关联的方法,几乎所有方法都是动态绑定。
理解:这里区分绑定和连接,其实是两个维度的东西。绑定说的编译与运行(包括类加载),针对的是方法与类的关联;连接指的是类加载和类运行,针对的是符号引用到直接引用的转换。静态绑定是编译时就确定方法属于某个类,但是依然需要解析过程将这个关系加载到内存,这个过程是静态解析。而动态连接是运行时将符号引用转为直接引用,也就是多态的实现原理。
总而言之,静态绑定的是不可能重写的方法,动态绑定的是可能重写的方法,静态绑定存到类信息中,动态绑定的方法名和地址存到方法表中。所有静态绑定的方法和大部分动态绑定的方法都是依赖静态解析加载到内存的,还有部动态绑定的方法是多态的,依赖的是动态连接在运行时实现的。这一块可以结合内存模型与类的存储结构
只有当类被主动引用时,会触发‘初始化’(包含前面的过程),其它被动引用不会初始化。
主动引用包括:实例化对象、操作静态字段、调用静态方法、使用反射操作类、初始化子类会初始化父类、JDK1.7的MethodHandle。
被动引用例子有:通过子类引用静态字段,但是字段属于父类,则子类不会初始化。实例化一个类的数组对象,不会触发该类的初始化,虚拟机会造一个类[Lxxx.xxx.xxx.xx。使用了类的常量(final),该类不会被初始化,因为编译阶段有常量传播优化
加载
加载就是把class文件数据加载到内存,具体包括通过类全名“找”二进制字节流,字节流的静态存储转换为方法区的数据,生成Class对象。
二进制来源不仅是文件,也可以是网络或者动态生成。
类加载器(ClassLoader)
虽然是叫做“类加载器”,但是只能修改类的“加载”过程(ClassLoader中的defineClass方法是final的且最后调用的native方法,也就是后面的过程无法修改)。
对于任意一个类,只有类本身和类加载器相同,才是同一个类。
从JVM角度看,有两种类加载器:启动类加载器(HotSpot是C++实现)和其它类加载器(Java实现)。
从Java开发看,有三类启动类加载器(如rt.jar,所有的java.开头的类),扩展类加载器sun.misc.Launcher$ExtClassLoader(\jre\lib\ext目录,如javax.开头的类),应用程序类加载器sun.misc.Launcher$AppClassLoader(用户类路径)。
除此之外用户可以自定义类加载器,以此实现在代码运行时去载入新的类,但是类加载器有双亲委派模型约束:类加载器会先尝试使用父类去加载类,如果没有才亲自加载,这样所有的类都会先由启动类加载器加载。
验证
文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
元数据验证:对类的元数据信息进行语义校验。
字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
准备
为类变量(static)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配,但是分配的是初始值(0或null),只有当final修饰的时候才会设置为真正的值。
解析
解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。
初始化
初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源。初始化阶段是执行类构造器(不是用来构造实例的构造方法)
1. 静态语句是按顺序进行的,而且静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中仅可以赋值。
static {
i = 1;//可以通过
System.out.print(i);//编译器会报错
}
static int i = 0;
去掉错误语句,i的值最后为0。
2. 虚拟机会保证在子类的
3. 如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成
4. 接口中不能使用静态语句块,但仍然有类变量初始化的赋值操作,但是接口不需要先执行父接口的
5. 虚拟机会保证一个类的
八.字节码的执行
1.方法解析
Class存储的都是符号引用,而不是方法在实际运行时入口地址。类运行期间才能确定某些目标方法的直接引用,称为动态连接,也有一部分方法的符号引用在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。
在Java语言中,静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,因此它们都适合在类加载阶段进行解析。
Java虚拟机里共提供了四条方法调用字节指令,分别是:
invokestatic:调用静态方法。
invokespecial:调用实例构造器
invokevirtual:调用所有的虚方法。
invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
invokestatic和invokespecial指令调用的方法,有静态方法、私有方法、实例构造器和父类方法四类,称为非虚方法(还包括final方法),与之相反,其他方法就称为虚方法(final方法除外)。
解析调用一定是个静态过程,在编译期间就完全确定。
2.静态分派
所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派,静态分派的最典型应用就是多态性中的方法重载。静态分派发生在编译阶段,因此确定静态分配的动作实际上不是由虚拟机来执行的。编译器(不是虚拟机,因为如果是根据静态类型做出的判断,那么在编译期就确定了)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。
Human man = new Man();
我们把上面代码中的“Human”称为变量的静态类型,后面的“Man”称为变量的实际类型。
3.动态分派
根据变量的实际类型来分派方法的执行版本的。而实际类型的确定需要在程序运行时才能确定下来,这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
4.单分派和多分派
方法的接受者(亦即方法的调用者)与方法的参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。
一是方法的接受者(即调用者)的静态类型是Father还是Child,二是方法参数类型是Eat还是Drink。因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。
唯一可以影响到虚拟机选择的因素只有此方法的接受者的实际类型是Father还是Child。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。
根据以上论证,我们可以总结如下:目前的Java语言(JDK1.6)是一门静态多分派、动态单分派的语言。
九.Java语法糖
Java中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。虚拟机并不支持这些语法,它们在编译阶段就被还原回了简单的基础语法结构,这个过程成为解语法糖。
Java语言在JDK1.5之后引入的泛型实际上只在程序源码中存在,在编译后的字节码文件中,就已经被替换为了原来的原生类型,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList
自动拆装箱、变长参数等语法糖也都是在编译阶段就把它们该语法糖结构还原为了原生的语法结构,因此在Class文件中也只存在其对应的原生类型。