2016-04-16 23:10:50
在这里,我们进一步认识JAVA及JAVA虚拟机,包括它的体系结构和垃圾回收机制等,并通过虚拟机监控工具进行简单的性能调优。
一、 JVM相关概念
什么是JVM
JVM能运行哪些编程语言
JVM运行流程
JVM生命周期
二、 JVM体系结构
类加载器
执行引擎
运行时数据区
本地库接口
三、 JVM内存参数调整及监控
JVM之内存调整
JVM监控工具之Jconsole
JVM监控工具之JProfile
四、JVM实战
JVM垃圾回收
JVM性能调优--提高Eclipse的运行效率
一、 JVM相关概念
1.1 什么是JAVA虚拟机
JVM是JAVA虚拟机( JAVA Virtual Machine)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 JAVA虚拟机有自己完善的虚拟硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。 JVM屏蔽了与具体操作系统平台相关的信息,使得JAVA程序只需生成在JAVA虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行.
1.2 Jvm能运行哪些编程语言
JVM可以运行任何能编译成符合JVM字节码规范的语言。 JAVA 和其他JVM语言都是要经过编译成字节码之后运行的。
一次编写,到处运行。经过多年的发展JVM已经成为一个多元化的平台,越来越多的语言可以运行在JVM上。
1.3 Jvm运行流程
1.4 Jvm的生命周期
(1)JVM实例的诞生
当启动一个JAVA程序时,一个JAVA实例就产生了,任何一个拥有public static void main (String[] args)函数的class都可以作为JVM实例运行的起点。
(2)JVM实例的运行
main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部又有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,JAVA程序也可以标明自己创建的线程是守护线程。
(3)JVM实例的消亡
当程序中的所有非守护线程都终止时(此时守护线程也没什么好提供服务的了),JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()退出。
在java.lang.System的源代码中:
public static void exit(int status) { Runtime.getRuntime().exit(status); }
status=0是正常退出程序,而1或其他非0值表示非正常退出程序。
二、 JVM体系结构
JVM体系结构主要包含两个子系统和两个组件:
Class Loader(类加载器)子系统, Execution Engine(执行引擎)子系统
Runtim Data Area(运行时数据区域) 组件, Native
Interface(本地接口) 组件
2.1 JVM类加载器
(一)Class Loader 类加载器
类加载器负责加载 JAVA 类的字节代码到 JAVA 虚拟机中,可以根据指定的类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area中的method area(方法区域)。 JAVA程序员可以extendsjava.lang.ClassLoader类来写自己的Class loader。
双亲委派模型工作过程:
如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
优点: java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在tools.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类, java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。
站在JVM的角度讲,主要有两种类型加载器:启动类加载器和所有其它的类加载器。启动类加载器是JVM实现的一部分,使用C++语言实现,其它类加载器都由java语言实现 ,独立于虚拟机外部,并且全部继承抽象类java.lang.ClassLoader。
(1) Bootstrap ClassLoader 启动类加载器
这是JVM的根ClassLoader,它是用C++实现的, JVM启动时初始化此ClassLoader,并由此ClassLoader完成$JAVA_HOME$中jrelib t.jar( Sun JDK的实现)中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。启动类加载器无法被JAVA程序直接引用 。
(2) Extension ClassLoader 扩展类加载器
扩展类加载器负责加载$JAVA_HOME$libext目录中或者java.ext.dirs系统变量所指定的所有类库,开发者可以直接使用扩展类加载器。
(3)Application ClassLoader 应用程序类加载器
JVM用此classloader来加载用户类路径 (Classpath)上所指定的类库,包含指定的jar包以及目录,该加载器有时也称为系统类加载器。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
(4) User-Defined ClassLoader 用户自定义类加载器
User-DefinedClassLoader是Java开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可用于加载非Classpath中的jar以及目录。
(二)类加载过程:
1. 加载
加载过程负责找到二进制字节码并加载至JVM中, JVM通过类名、类所在的包名通过ClassLoader来完成类的加载。
2. 连接
链接过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口、类。
(1)验证:确保被导入类的正确性
(2)准备:为类变量分配内存,并将其初始化为默认值
(3)解析:把类中的符号引用转换为直接引用
3.初始化
初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化,在四种情况下初始化过程会被触发执行:调用了new;反射调用了类中的方法;子
类调用了初始化; JVM启动过程中指定的初始化类。
2.2 Execution Engine 执行引擎
执行引擎是JVM最核心的组成部分之一,其主要是执行class中的指令,任何JVM实现的核心是Execution engine 。执行引擎可以把JAVA字节码转为机器能识别的字节码,并调用机器的指令进行计算等,不同JVM的执行效率很大程度决定于他们各自实现的Execution engine的好坏。“虚拟机”的执行引擎与 “物理机”的执行引擎是比较类似的,这两种机器都有执行代码能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎是自己实现的,因此虚拟机可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令。
2.3 JAVA Native Interface(JNI)本地接口
Java本地接口( Java Native Interface, JNI)是一个标准的 Java API,它支持将 Java 代码与使用其他编程语言编写的代码相集成,例如可以调用Native语言函数CC++等。 JNI是java与其它编程语言交互的接口。
2.4 Runtime Date Area 运行时数据区
这个组件就是JVM的内存区域,这是了解JVM内存模型的重要部分。
Runtime data area 主要包括五个部分:
Heap (堆)
Method Area(方法区域)
VM Stack(虚拟机栈),
Native method stack(本地方法栈) ( 在Sun的HotSpot虚拟机中VM Stack和Native method stack是合并到一起的)
Program Counter(程序计数器)
Heap 和Method Area是被所有线程共享使用的;而vm stack, Program counter 和Native method stack是以线程为粒度的,每个线程独自拥有。
(一)程序计数器Program Counter Register
程序计数器是是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。JAVA虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程”私有的内存。
(二)虚拟机栈 VM stack
与程序计数据器一样, JAVA虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是JAVA方法执行的内存模型:每个方法被执行时都会同时创建一个栈帧( Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入到出栈的过程。
(三)本地方法栈 Native Method stack
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行JAVA方法服务的,而本地方法栈则是为虚拟机使用到的本地方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。例如Sun HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。
(四)方法区 Method area
方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息等。当开发人员在程序中通过Class对象中的getName等方法来获取信息时,这些数据都来源于方法区域。
方法区域也是全局共享的,因此会涉及到多线程访问的同步问题,方法区在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出JAVA.lang.OutOfMemoryError:PermGen full的错误信息。
在Sun JVM中这块区域对应的为Permanet Generation,又称为持久代, Permanet Generation实际上并不等同于方法区,只不过是Hotspot JVM用Permanet Generation来实现方法区而已,有些虚拟机也没有PermSpace而是用其他机制来实现方法区。
运行时常量池( Runtime Constant Pool)
运行时常量池是方法区(永久代)的一部分。 Class文件中除了有类的版本,字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性, JAVA语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是Sring类的intern()方法。运行时常量池既然是方法区的一部分,自然也会受到方法区内存的限制,当常量池无池再申请到内存时会抛出OutOfMenmoryError异常。
(五)堆空间(Heap)
堆空间是JAVA对象生死存亡的地区, JAVA对象的出生,成长,死亡都在这个区域完成。 JAVA程序在运行时创建的所有类实例或数组都放在堆中。
每一个JAVA程序独占一个JVM实例, 一个JVM实例只存在一个堆空间,因些每个JAVA程序都有它自己的堆空间,它们不会彼此干扰。同一个JAVA程序的多个线程都共享着同一个堆空间,所以就需要考虑多线程访问对象(堆数据)的同步问题。
Sun的HotSpot虚拟机对于堆内存共划分为二大部分:
年轻代/新生代( Young Generation)、老年代( Old Generation)。
1. Young(年轻代)
jvm规范中的 Heap的一部份, 年轻代又分三个区:一个Eden区,两个Survivor区 .
(1)伊甸园( Eden space) :
JAVA堆空间中的大部分对象在此出生,该区的名字因此而得名。也即是说当你的JAVA程序运行时,需要创建新的对象时, JVM都将在该区为你创建一个指定的对象
供程序使用。
(2)幸存者0区( Survivor 0 space)和幸存者1区( Survivor1 space) :
当伊甸园的空间用完时,程序又需要创建对象;此时JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的还存活的对象移动到幸存者0区或1区。幸存者两个区就是用于存放伊甸园垃圾回收时所幸存下来的JAVA对象。Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且, Survivor区总有一个是空的。(具体的回收GC下面介绍)
2. Tenured(老年代)
在年轻代中经历了多次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。另外一些大对象也会直接进入老年代,可以通过设置jvm参数来指定多大对象直接进入老年代(参数为-XX:PretenureSizeThreshold=1024,单位为字节)