一.JVM学习
1.1JVM运行机制的最重要的三点:加载(类加载器,classloader) 、内存管理(包含GC)、执行。
如果再加上JDK所作的把java文件编译为二进制class文件的步骤,就组成了Java代码的执行机制三部曲:
编译–>加载–>执行
2.1 Java编译机制
Java编译机制不属于JVM,但是JVM运行class文件,首先需要JDK把java源码编译成class文件。
JVM规范规定了class文件的格式,但并未规定java源码如何编译成class文件,以及如何执行class文件。
各厂商的JDK编译器各不相同,比如Sun JDK中的编译器是javac.exe。
Java 源码编译由以下三个过程组成:
- 分析和输入到符号表
- 注解处理
- 语义分析和生成class文件
2.1.1 分析和输入到符号表(Parse and Enter):
Parse过程主要做词法分析和语法分析。
Enter过程是将符号输入到符号表。
2.1.2 注解处理(Annotation Processing):
根据注解产生一些新的代码,或进行一些特殊检查。
2.1.3 语义分析和生成class文件(Analyse and Generate):
语义分析包括声明检查、类型检查、语句到达检查、exception检查、变量赋值检查、解除语法、泛型转换等。
采用后续遍历语法树生成class文件。
class文件包含了如下信息:
结构信息:包括格式版本号及各部分的数量与大小的信息。
元数据:主要是声明和常量的信息。
方法信息:主要是语句和表达式的信息。
2.2 类加载机制
类加载机制是指class文件加载到JVM,并形成class对象的机制。类的加载又分为三个步骤:
装载–>链接–>初始化
2.2.1 装载(Load)
装载过程负责找到二进制代码并加载到JVM中。
对于接口或非数组型的类,由ClassLoader直接加载;
对于数组型的类,数组类由JVM直接创建,而数组中的元素类型还是由ClassLoader加载。
2.2.2 链接(Link)
链接过程又分为:校验,准备,和解析。
校验格式遵循JVM规范。
准备过程会初始化类中的静态变量,并将其值赋为默认值。(特别注意这一点:静态变量初始化是在Initialize阶段之前完成的)
最后对类中所有的属性、方法进行验证。
装载和链接过程完成后,二进制字节码就转换成了class对象。此时还未初始化。
2.2.3 初始化(Initialize)
初始化即执行类的静态初始化代码、静态属性初始化、实例初始块代码、构造方法等。(但静态初始化和实例/构造方法初始化不是同时进行的,一个是类加载完后进行,一个是实例化的时候进行)
初始化是在初次主动使用对象前执行,在以下4种情况下初始化过程会被触发执行:
1) 调用了new;
2) 反射执行newInstance()方法;
3) 子类调用了初始化;
4) JVM启动过程中指定的初始化类。
关于静态变量、静态初始化块、实例初始化块、构造方法的初始化顺序,见另一文《Java类和实例初始化顺序》
JVM对类的加载,是通过ClassLoader及其子类来完成的,分为:
Bootstrap ClassLoader, Extension ClassLoader, Application ClassLoader, Custom ClassLoader。
1) Bootstrap ClassLoader:
Sun JDK采用C++实现此类,启动时会初始化此ClassLoader,并由它完成jre/lib/rt.jar包里所有class文件的加载。
如果用java代码去打印此类,只会显示null。
2) Extension ClassLoader:
用来加载扩展包。Sun JDK中,继承自java.lang.ClassLoader,全限定名为:sun.misc.Launcher.ExtClassLoader
3) Application ClassLoader:
用来加载classpath指定的jar包。Sun JDK中,继承自java.lang.ClassLoader,全限定名为:sun.misc.Launcher.ExtClassLoader
(注意:ExtClassLoader和AppClassLoader是属于同一个package的,而不是加载关系中的父子关系)
4) Custom ClassLoader:
根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。
在代码中可以通过类名.class.getClassLoader()或类名.class.getClassLoader().getParent()或继续.getParent()来得到类名信息。
Sun JDK中各ClassLoader的继承关系如下图:
其实IBM的JDK中用到的ClassLoader基本都是Sun的这一套,并在此基础上再实现了一些ClassLoader类。
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。
而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
这就是所谓的双亲委托模式(英文名为parent delegation,其实是单亲,双亲这个词容易让人误会),这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。比如java.lang.String在系统启动的时候由Bootstrap ClassLoader加载了,所以用户就不能再试图自己写一个java.lang.String来代替原有的String类了。
命名空间:
Java的命名空间其实说法并不统一。
一说Java的命名空间就是package,如同C#里的namespace。
一说Java的命名空间是由类装载器实例所装载的类组成。每个类装载器实例有自己的命名空间,命名空间由所有以此装载器实例为创始类装载器的类组成。
不管怎么说,不同命名空间的两个类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类。
隐式加载和显式加载
隐式加载:程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
显式加载:通过class.forname(),class.loadClass()等方法,显式加载需要的类。
两个异常ClassNotFoundException和NoClassDefFoundError的区别:
ClassNotFoundException: 当前ClassLoader加载类时未找到类文件。
NoClassDefFoundError: 主要原因是加载的类中引用到得其他类找不到。
2.3 类执行机制
前面说了,JVM规范并没有规定如何执行class文件。计算机不能理解高级语言(如Java,C),只能理解机器语言。Java编译器是把Java文件编译为二进制字节码。这是一种中间代码,计算机仍然不懂。这就需要JVM把它翻译成机器码。翻译有两种方式:解释和编译。那么在JVM执行类的时候,就有两种方式了:解释执行和编译执行。
解释执行:
在运行程序的时候才翻译,且每次运行都需要翻译,所以效率低。
JVM采用了invokestatic、invokevirtual、invokeinterface、invokespecial四个指令来执行不同的方法调用,基本上可以从名字看出来各自的用途,其中invokespecial是用来调用private方法和实例初始化方法。
编译执行:
将字节码编译成机器码执行。这里需要特别指出的是,Sun JVM支持在运行时编译(Just In Time, JIT编译)。Sun JVM在执行过程中对执行频率高的代码(Hotspot,热点)进行编译,对执行不频繁的代码则继续采用解释的方式,所以Sun JVM又称为Hotspot VM。
Sun JVM提供了两种编译模式: client compiler(-client)和server compiler(-server)。
- client comiler: 又称为C1,轻量级,只做少量优化,占用内存少。主要优化有:
- 方法内联
- 去虚拟化
- 冗余削除
- server compiler: 又称为C2,重量级,采用大量优化,占用内存多。逃逸分析是C2进行很多优化的基础。
主要优化有:
- 变量替换
- 栈上分配
- 同步削除
逃逸分析(Escape Analysis):是指根据运行状况来判断方法中的便利是否会被外部读取。如会被读取,则此变量是逃逸的。比如在方法中,给外部全局变量赋值,或方法有返回值,或对象引用传递等。如果没有发生逃逸,则可以对代码进行上述优化
几种情况下:
32位windows机器始终都是client模式。
当机器配置CPU超过2核且内存超过2G,默认为server模式。
可以通过增加-client或-server来强制指定。
查看本机默认编译方式:
在命令行执行”java –version”
如:
java version “1.7.0_01″
Java(TM) SE Runtime Environment (build 1.7.0_01-b08)
Java HotSpot(TM) 64-Bit Server VM (build 21.1-b02, mixed mode)
最后一行显示了其JVM默认为server模式编译。mixed mode表示解释和编译混合执行,区别于单纯的解释执行和编译执行。
==================================================================
3. JVM内存管理
3.1 内存空间划分
JVM在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域,各区域有各自不同的用途和生存期。按规范,JVM所管理的内存包括以下几个运行时数据区域:
- 程序计数器(Program Counter Register):
程序计数器是一块比较小的内存空间,它是线程私有的内存,指向当前线程所执行的字节码的位置。
程序计数器是唯一一块不会抛出OutOfMemoryError的区域。
- 虚拟机栈(VM Stacks):
即栈,或称方法栈。也是线程私有的。基本操作为压栈和出栈,单位为栈帧,顺序为先进后出。
栈帧由三部分组成:局部变量区、操作数栈、栈帧数据区。(动态链接,方法出口)
局部变量表存放了编译器可知的基本数据类型、对象引用和方法返回值等。
栈是运行时的单位。
StackOverFlowError: 当线程请求的栈深度大于虚拟机所允许的深度时,抛出StackOverFlowError。换句话说,当需要存储的数据超过了分配的栈空间时,就会抛这个错误信息。比如死循环、递归次数过多等。
OutOfMemoryError: 虚拟机栈动态扩展到无法申请到足够的内存时,就会抛OutOfMemoryError。但基本很少会碰到这个Error。
- 本地方法栈(Native Method Stacks):
本地方法栈是为虚拟机使用的本地方法服务的。而虚拟机栈则是为虚拟机执行Java方法服务的。两者很相似,HotSpot虚拟机是直接把两者合二为一的。
当然也是线程私有的。
- 堆(Heap):
一个JVM只有一个堆,所有线程共享。堆用来存放类实例和数组。这块区域是GC管理的主要区域。
堆是存储的单位。
当堆中没有足够内存来分配对象实例,且堆无法再扩展时,就会OOM。
- 方法区(Method Area):
又称为非堆(Non-Heap),也是所有线程共享的。
用于存放虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的常量和符号引用。
HotSpot虚拟机使用Perm代(永久代)来实现方法区,所以在一定概念和时间范围内可以相互通用。但以后HotSpot也将采用Native Memory来实现方法区。
3.2 对象访问
我们知道,一个对象的引用reference是分配在栈空间的局部变量表里的,指向对象的引用。但JVM规范并没有规定对象的访问方式。主流的访问方式有两种:使用句柄和直接访问。
使用句柄:Java堆中划分一块区域作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。如图:
直接访问: reference中直接存储堆中的对象地址。如图:
使用句柄的好处:当发生GC时,对象经常会发生移动,而reference中存储的句柄地址就不需要发生变化,变化的只是句柄中的对象指针。
直接访问的好处:速度!
HotSpot采用直接访问的方式。
3.3 GC
3.3.1 垃圾回收算法:
引用计数(Reference Counting)
标记-清除(Mark-Sweep)
复制(Copying)
标记-整理(Mark-Compact)
增量收集(Incremental Collecting)
分代(Generational Collecting):
3.3.2 垃圾收集器:
HotSpot JVM有如下几种垃圾收集器,这里不展开了:
Serial收集器
ParNew收集器
Parallel Scavenge收集器
Serial Old收集器
Scavenge Old收集器
CMS收集器
G1收集器
3.3.3 分代回收:
不同的对象的生命周期是不一样的,因此对不同生命周期的对象,可以采取不同的回收方式,以提高回收效率。所以分代收集算法是以其他算法为基础的。
在HotSpot JVM中将分配到的内存堆(Heap)分为两个物理区域,一个是年轻代(Young Generation),一个是年老代(Tenured Generation),另外再加上持久代(Permanent Generation),就组成了分代回收GC中的三个内存区域。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
Young(年轻代): 一般来说年轻代又分为一个Eden区和两个Survivor区。(也可分多个Survivor区) Survivor区无先后顺序,彼此对等。大部分对象在Eden区生成。
当Eden区满的时候,存活的对象就被复制到其中一个Survivor区中,我们权且称其为主Survivor区;
当此Survivor区的数据又满的时候,其中的对象就被复制到另一个Survivor区,原Survivor区被清空。那么现在,第二个Survivor区就成了主区,所以它同时又接收Eden区复制转移过来的对象。
当此Survivor区再次满的时候,复制转移了两次还存活的对象,就被复制到年老代中去了。(转移几次才复制到老年代中,可通过参数配置)
Tenured(老年代):一般来说,年老代存放的都是存活期较长的对象。
Permanent(持久代): 用于存放静态数据,如类、方法等。注意,持久代仅仅是HotSpot虚拟机目前对方法区的实现方式,其他虚拟机是没有持久代的,所以持久代不等同于方法区。
分代垃圾回收分为两种:Scaverage GC和Full GC
Scaverage GC:
当新产生对象,写入Eden区,而Eden区满了的时候,就会触发Scaverage GC。这时GC就会清理Eden区,清除非存活对象,把存活的对象写入Survivor区, 同时清理两个Survivor区。Eden区内存分配不大,且大部分对象都是在Eden区产生的,所以Scaverage GC会比较频繁。
Full GC:
Full GC会对整个堆进行整理,包括年轻代、年老代、持久代。Full GC会消耗很多内存,频繁的Full GC更会严重影响性能,所以要避免频繁的Full GC。会导致Full GC的可能原因有:
- Tenured Generation写满了。
- Perm Generation写满了。
- 显式调用System.gc()
3.3.4 常见配置汇总
- 堆设置
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -XX:NewSize=n:设置年轻代大小
- -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
- -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
- -XX:MaxPermSize=n:设置持久代大小
- 收集器设置
- -XX:+UseSerialGC:设置串行收集器
- -XX:+UseParallelGC:设置并行收集器
- -XX:+UseParalledlOldGC:设置并行年老代收集器
- -XX:+UseConcMarkSweepGC:设置并发收集器
- 垃圾回收统计信息
- -XX:+PrintGC
- -XX:+Printetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
- 并行收集器设置
- -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
- -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
- 并发收集器设置
- -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
- -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。