1、java虚拟机的生命周期:java
Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就中止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。 Java虚拟机老是开始于一个main()方法,这个方法必须是公有、返回void、直接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包换main()方法的类名。 Main()方法是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其余的线程都由他来启动。Java中的线程分为两种:守护线程 (daemon)和普通线程(non-daemon)。守护线程是Java虚拟机本身使用的线程,好比负责垃圾收集的线程就是一个守护线程。固然,你也可 以把本身的程序设置为守护线程。包含Main()方法的初始线程不是守护线程。 只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会中止。若是有足够的权限,你能够调用exit()方法终止程序。程序员
2、java虚拟机的体系结构:算法
在Java虚拟机的规范中定义了一系列的子系统、内存区域、数据类型和使用指南。这些组件构成了Java虚拟机的内部结构,他们不只仅为Java虚拟机的实现提供了清晰的内部结构,更是严格规定了Java虚拟机实现的外部行为。 每个Java虚拟机都由一个类加载器子系统(class loader subsystem),负责加载程序中的类型(类和接口),并赋予惟一的名字。每个Java虚拟机都有一个执行引擎(execution engine)负责执行被加载类中包含的指令。 程序的执行须要必定的内存空间,如字节码、被加载类的其余额外信息、程序中的对象、方法的参数、返回值、本地变量、处理的中间变量等等。Java虚拟机将 这些信息通通保存在数据区(data areas)中。虽然每一个Java虚拟机的实现中都包含数据区,可是Java虚拟机规范对数据区的规定却很是的抽象。许多结构上的细节部分都留给了 Java虚拟机实现者本身发挥。不一样Java虚拟机实现上的内存结构千差万别。一部分实现可能占用不少内存,而其余如下可能只占用不多的内存;一些实现可 能会使用虚拟内存,而其余的则不使用。这种比较精炼的Java虚拟机内存规约,可使得Java虚拟机能够在普遍的平台上被实现。 数据区中的一部分是整个程序共有,其余部分被单独的线程控制。每个Java虚拟机都包含方法区(method area)和堆(heap),他们都被整个程序共享。Java虚拟机加载并解析一个类之后,将从类文件中解析出来的信息保存与方法区中。程序执行时建立的 对象都保存在堆中。 当一个线程被建立时,会被分配只属于他本身的PC寄存器“pc register”(程序计数器)和Java堆栈(Java stack)。当线程不掉用本地方法时,PC寄存器中保存线程执行的下一条指令。Java堆栈保存了一个线程调用方法时的状态,包括本地变量、调用方法的 参数、返回值、处理的中间变量。调用本地方法时的状态保存在本地方法堆栈中(native method stacks),可能再寄存器或者其余非平台独立的内存中。 Java堆栈有堆栈块(stack frames (or frames))组成。堆栈块包含Java方法调用的状态。当一个线程调用一个方法时,Java虚拟机会将一个新的块压到Java堆栈中,当这个方法运行结束时,Java虚拟机会将对应的块弹出并抛弃。 Java虚拟机不使用寄存器保存计算的中间结果,而是用Java堆栈在存放中间结果。这是的Java虚拟机的指令更紧凑,也更容易在一个没有寄存器的设备上实现Java虚拟机。 图中的Java堆栈中向下增加的,PC寄存器中线程三为灰色,是由于它正在执行本地方法,他的下一条执行指令不保存在PC寄存器中。
3、类加载器子系统:编程
Java虚拟机中的类加载器分为两种:原始类加载器(primordial class loader)和类加载器对象(class loader objects)。原始类加载器是Java虚拟机实现的一部分,类加载器对象是运行中的程序的一部分。不一样类加载器加载的类被不一样的命名空间所分割。 类加载器调用了许多Java虚拟机中其余的部分和java.lang包中的不少类。好比,类加载对象就是java.lang.ClassLoader子类 的实例,ClassLoader类中的方法能够访问虚拟机中的类加载机制;每个被Java虚拟机加载的类都会被表示为一个 java.lang.Class类的实例。像其余对象同样,类加载器对象和Class对象都保存在堆中,被加载的信息被保存在方法区中。 一、加载、链接、初始化(Loading, Linking and Initialization) 类加载子系统不只仅负责定位并加载类文件,他按照如下严格的步骤做了不少其余的事情:(具体的信息参见第七章的“类的生命周期”) 1)、加载:寻找并导入指定类型(类和接口)的二进制信息 2)、链接:进行验证、准备和解析 ①验证:确保导入类型的正确性 ②准备:为类型分配内存并初始化为默认值 ③解析:将字符引用解析为直接饮用 3)、初始化:调用Java代码,初始化类变量为合适的值 二、原始类加载器(The Primordial Class Loader) 每一个Java虚拟机都必须实现一个原始类加载器,他可以加载那些遵照类文件格式而且被信任的类。可是,Java虚拟机的规范并无定义如何加载类,这由 Java虚拟机实现者本身决定。对于给定类型名的类型,原始莱加载器必须找到那个类型名加“.class”的文件并加载入虚拟机中。 三、类加载器对象 虽然类加载器对象是Java程序的一部分,可是ClassLoader类中的三个方法能够访问Java虚拟机中的类加载子系统。 1)、protected final Class defineClass(…):使用这个方法能够出入一个字节数组,定义一个新的类型。 2)、protected Class findSystemClass(String name):加载指定的类,若是已经加载,就直接返回。 3)、protected final void resolveClass(Class c):defineClass()方法只是加载一个类,这个方法负责后续的动态链接和初始化。 具体的信息,参见第八章“链接模型”( The Linking Model)。 四、命名空间 当多个类加载器加载了同一个类时,为了保证他们名字的惟一性,须要在类名前加上加载该类的类加载器的标识。具体的信息,参见第八章“链接模型”( The Linking Model)。
4、方法区:数组
在Java虚拟机中,被加载类型的信息都保存在方法区中。这写信息在内存中的组织形式由虚拟机的实现者定义,好比,虚拟机工做在一个“little- endian”的处理器上,他就能够将信息保存为“little-endian”格式的,虽然在Java类文件中他们是以“big-endian”格式保 存的。设计者能够用最适合并地机器的表示格式来存储数据,以保证程序可以以最快的速度执行。可是,在一个只有很小内存的设备上,虚拟机的实现者就不会占用 很大的内存。 程序中的全部线程共享一个方法区,因此访问方法区信息的方法必须是线程安全的。若是你有两个线程都去加载一个叫Lava的类,那只能由一个线程被允许去加载这个类,另外一个必须等待。 在程序运行时,方法区的大小是可变的,程序在运行时能够扩展。有些Java虚拟机的实现也能够经过参数也订制方法区的初始大小,最小值和最大值。 方法区也能够被垃圾收集。由于程序中的内由类加载器动态加载,全部类可能变成没有被引用(unreferenced)的状态。当类变成这种状态时,他就可 能被垃圾收集掉。没有加载的类包括两种状态,一种是真正的没有加载,另外一个种是“unreferenced”的状态。详细信息参见第七章的类的生命周期 (The Lifetime of a Class)。 一、类型信息(Type Information) 每个被加载的类型,在Java虚拟机中都会在方法区中保存以下信息: 1)、类型的全名(The fully qualified name of the type) 2)、类型的父类型的全名(除非没有父类型,或者弗雷形式java.lang.Object)(The fully qualified name of the typeís direct superclass) 3)、给类型是一个类仍是接口(class or an interface)(Whether or not the type is a class ) 4)、类型的修饰符(public,private,protected,static,final,volatile,transient等)(The typeís modifiers) 5)、全部父接口全名的列表(An ordered list of the fully qualified names of any direct superinterfaces) 类型全名保存的数据结构由虚拟机实现者定义。除此以外,Java虚拟机还要为每一个类型保存以下信息: 1)、类型的常量池(The constant pool for the type) 2)、类型字段的信息(Field information) 3)、类型方法的信息(Method information) 4)、全部的静态类变量(很是量)信息(All class (static) variables declared in the type, except constants) 5)、一个指向类加载器的引用(A reference to class ClassLoader) 6)、一个指向Class类的引用(A reference to class Class) 1)、类型的常量池(The constant pool for the type) 常量池中保存中全部类型是用的有序的常量集合,包含直接常量(literals)如字符串、整数、浮点数的常量,和对类型、字段、方法的符号引用。常量池 中每个保存的常量都有一个索引,就像数组中的字段同样。由于常量池中保存中全部类型使用到的类型、字段、方法的字符引用,因此它也是动态链接的主要对 象。详细信息参见第六章“The Java Class File”。 2)、类型字段的信息(Field information) 字段名、字段类型、字段的修饰符(public,private,protected,static,final,volatile,transient等)、字段在类中定义的顺序。 3)、类型方法的信息(Method information) 方法名、方法的返回值类型(或者是void)、方法参数的个数、类型和他们的顺序、字段的修饰符(public,private,protected,static,final,volatile,transient等)、方法在类中定义的顺序 若是不是抽象和本地本法还须要保存 方法的字节码、方法的操做数堆栈的大小和本地变量区的大小(稍候有详细信息)、异常列表(详细信息参见第十七章“Exceptions”。) 4)、类(静态)变量(Class Variables) 类变量被全部类的实例共享,即便不经过类的实例也能够访问。这些变量绑定在类上(而不是类的实例上),因此他们是类的逻辑数据的一部分。在Java虚拟机使用这个类以前就须要为类变量(non-final)分配内存 常量(final)的处理方式于这种类变量(non-final)不同。每个类型在用到一个常量的时候,都会复制一份到本身的常量池中。常量也像类变 量同样保存在方法区中,只不过他保存在常量池中。(多是,类变量被全部实例共享,而常量池是每一个实例独有的)。Non-final类变量保存为定义他的 类型数据(data for the type that declares them)的一部分,而final常量保存为使用他的类型数据(data for any type that uses them)的一部分。详情参见第六章“The Java Class FileThe Java Class File” 5)、指向类加载器的引用(A reference to class ClassLoader) 每个被Java虚拟机加载的类型,虚拟机必须保存这个类型是否由原始类加载器或者类加载器加载。那些被类加载器加载的类型必须保存一个指向类加载器的引 用。当类加载器动态链接时,会使用这条信息。当一个类引用另外一个类时,虚拟机必须保存那个被引用的类型是被同一个类加载器加载的,这也是虚拟机维护不一样命 名空间的过程。详情参见第八章“The Linking Model” 6)、指向Class类的引用(A reference to class Class) Java虚拟机为每个加载的类型建立一个java.lang.Class类的实例。你也能够经过Class类的方法: public static Class forName(String className)来查找或者加载一个类,并取得相应的Class类的实例。经过这个Class类的实例,咱们能够访问Java虚拟机方法区中的信息。具体参照Class类的JavaDoc。 二、方法列表(Method Tables) 为了更有效的访问全部保存在方法区中的数据,这些数据的存储结构必须通过仔细的设计。全部方法区中,除了保存了上边的那些原始信息外,还有一个为了加快存 取速度而设计的数据结构,好比方法列表。每个被加载的非抽象类,Java虚拟机都会为他们产生一个方法列表,这个列表中保存了这个类可能调用的全部实例 方法的引用,报错那些父类中调用的方法。详情参见第八章“The Linking Model”
5、堆:tomcat
当Java程序建立一个类的实例或者数组时,都在堆中为新的对象分配内存。虚拟机中只有一个堆,全部的线程都共享他。 一、垃圾收集(Garbage Collection) 垃圾收集是释放没有被引用的对象的主要方法。它也可能会为了减小堆的碎片,而移动对象。在Java虚拟机的规范中没有严格定义垃圾收集,只是定义一个Java虚拟机的实现必须经过某种方式管理本身的堆。详情参见第九章“Garbage Collection”。 二、对象存储结构(Object Representation) Java虚拟机的规范中没有定义对象怎样在堆中存储。每个对象主要存储的是他的类和父类中定义的对象变量。对于给定的对象的引用,虚拟机必须嫩耨很快的 定位到这个对象的数据。另为,必须提供一种经过对象的引用方法对象数据的方法,好比方法区中的对象的引用,因此一个对象保存的数据中每每含有一个某种形式 指向方法区的指针。 一个可能的堆的设计是将堆分为两个部分:引用池和对象池。一个对象的引用就是指向引用池的本地指针。每个引用池中的条目都包含两个部分:指向对象池中对 象数据的指针和方法区中对象类数据的指针。这种设计可以方便Java虚拟机堆碎片的整理。当虚拟机在对象池中移动一个对象的时候,只须要修改对应引用池中 的指针地址。可是每次访问对象的数据都须要处理两次指针。下图演示了这种堆的设计。在第九章的“垃圾收集”中的HeapOfFish Applet演示了这种设计。 另外一种堆的设计是:一个对象的引用就是一个指向一堆数据和指向相应对象的偏移指针。这种设计方便了对象的访问,但是对象的移动要变的异常复杂。下图演示了这种设计 当程序试图将一个对象转换为另外一种类型时,虚拟机须要判断这种转换是不是这个对象的类型,或者是他的父类型。当程序适用instanceof语句的时候也 会作相似的事情。当程序调用一个对象的方法时,虚拟机须要进行动态绑定,他必须判断调用哪个类型的方法。这也须要作上面的判断。 不管虚拟机实现者使用哪种设计,他均可能为每个对象保存一个相似方法列表的信息。由于他能够提高对象方法调用的速度,对提高虚拟机的性能很是重要,但 是虚拟机的规范中比没有要求必须实现相似的数据结构。下图描述了这种结构。图中显示了一个对象引用相关联的全部的数据结构,包括: 1)、一个指向类型数据的指针 2)、一个对象的方法列表。方法列表是一个指向全部可能被调用对象方法的指针数组。方法数据包括三个部分:操做码堆栈的大小和方法堆栈的本地变量区;方法的字节码;异常列表。 每个Java虚拟机中的对象必须关联一个用于同步多线程的lock(mutex)。同一时刻,只能有一个对象拥有这个对象的锁。当一个拥有这个这个对象 的锁,他就能够屡次申请这个锁,可是也必须释放相应次数的锁才能真正释放这个对象锁。不少对象在整个生命周期中都不会被锁,因此这个信息只有在须要时才需 要添加。不少Java虚拟机的实现都没有在对象的数据中包含“锁定数据”,只是在须要时才生成相应的数据。除了实现对象的锁定,每个对象还逻辑关联到一 个“wait set”的实现。锁定帮组线程独立处理共享的数据,不须要妨碍其余的线程。“wait set”帮组线程协做完成同一个目标。“wait set”每每经过Object类的wait()和notify()方法来实现。 垃圾收集也须要堆中的对象是否被关联的信息。Java虚拟机规范中指出垃圾收集一个运行一个对象的finalizer方法一次,可是允许 finalizer方法从新引用这个对象,当这个对象再次不被引用时,就不须要再次调用finalize方法。因此虚拟机也须要保存finalize方法 是否运行过的信息。更多信息参见第九章的“垃圾收集” 三、数组的保存(Array Representation) 在Java 中,数组是一种彻底意义上的对象,他和对象同样保存在堆中、有一个指向Class类实例的引用。全部同一维度和类型的数组拥有一样的Class,数组的长 度不作考虑。对应Class的名字表示为维度和类型。好比一个整型数据的Class为“[I”,字节型三维数组Class名为“[[[B”,两维对象数据 Class名为“[[Ljava.lang.Object”。 数组必须在堆中保存数组的长度,数组的数据和一些对象数组类型数据的引用。经过一个数组引用的,虚拟机应该可以取得一个数组的长度,经过索引可以访问特定 的数据,可以调用Object定义的方法。Object是全部数据类的直接父类。更多信息参见第六章“类文件”。
6、基本结构:安全
从Java平台的逻辑结构上来看,咱们能够从下图来了解JVM:数据结构
从上图能清晰看到Java平台包含的各个逻辑模块,也能了解到JDK与JRE的区别。多线程
JVM自身的物理结构jvm
此图看出jvm内存结构
JVM内存结构主要包括两个子系统和两个组件。两个子系统分别是Classloader子系统和Executionengine(执行引擎)子系统;两个组件分别是Runtimedataarea(运行时数据区域)组件和Nativeinterface(本地接口)组件。
Classloader子系统的做用:
根据给定的全限定名类名(如java.lang.Object)来装载class文件的内容到Runtimedataarea中的methodarea(方法区域)。Java程序员能够extendsjava.lang.ClassLoader类来写本身的Classloader。
Executionengine子系统的做用:
执行classes中的指令。任何JVMspecification实现(JDK)的核心都是Executionengine,不一样的JDK例如Sun的JDK和IBM的JDK好坏主要就取决于他们各自实现的Executionengine的好坏。
Nativeinterface组件:
与nativelibraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的而且再也不受虚拟机限制的世界,因此也很容易出现JVM没法控制的nativeheapOutOfMemory。
RuntimeDataArea组件:
这就是咱们常说的JVM的内存了。它主要分为五个部分——
一、Heap(堆):一个Java虚拟实例中只存在一个堆空间
二、MethodArea(方法区域):被装载的class的信息存储在Methodarea的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,而后读入这个class文件内容并把它传输到虚拟机中。
三、JavaStack(java的栈):虚拟机只会直接对Javastack执行两种操做:以帧为单位的压栈或出栈
四、ProgramCounter(程序计数器):每个线程都有它本身的PC寄存器,也是该线程启动时建立的。PC寄存器的内容老是指向下一条将被执行指令的饿地址,这里的地址能够是一个本地指针,也能够是在方法区中相对应于该方法起始指令的偏移量。
五、Nativemethodstack(本地方法栈):保存native方法进入区域的地址
对于JVM的学习,在我看来这么几个部分最重要:
Java代码编译是由Java源码编译器来完成,流程图以下所示:
Java字节码的执行是由JVM执行引擎来完成,流程图以下所示:
Java代码编译和执行的整个过程包含了如下三个重要的机制:
Java 源码编译由如下三个过程组成:(javac –verbose 输出有关编译器正在执行的操做的消息)
最后生成的class文件由如下部分组成:
、
JVM的类加载是经过ClassLoader及其子类来完成的,类的层次关系和加载顺序能够由下图来描述:
1)Bootstrap ClassLoader /启动类加载器
$JAVA_HOME中jre/lib/rt.jar里全部的class,由C++实现,不是ClassLoader子类
2)Extension ClassLoader/扩展类加载器
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
3)App ClassLoader/ 系统类加载器
负责记载classpath中指定的jar包及目录中class
4)Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)
属于应用程序根据自身须要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader
加载过程当中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只全部ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
在这里,须要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,若是父类加载器能够完成类加载任务,就成功返回;只有父类加载器没法完成此加载任务时,才本身去加载。
JVM是基于栈的体系结构来执行class字节码的。线程建立后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每一个栈帧对应着每一个方法的每次调用,而栈帧又是有局部变量区和操做数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操做数栈中用于存放方法执行过程当中产生的中间结果。
JVM栈由堆、栈、本地方法栈、方法区等部分组成,结构图以下所示:
Sun的JVMGenerationalCollecting(垃圾回收)原理是这样的:把对象分为年青代(Young)、年老代(Tenured)、持久代(Perm),对不一样生命周期的对象使用不一样的算法。(基于对对象生命周期分析)
1.Young(年轻代)
年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的而且此时还存活的对象,将被复制年老区(Tenured。须要注意,Survivor的两个区是对称的,没前后关系,因此同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。并且,Survivor区总有一个是空的。
2.Tenured(年老代)
年老代存放从年轻代存活的对象。通常来讲年老代存放的都是生命期较长的对象。
3.Perm(持久代)
用于存放静态文件,现在Java类、方法等。持久代对垃圾回收没有显著影响,可是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候须要设置一个比较大的持久代空间来存放这些运行过程当中新增的类。持久代大小经过-XX:MaxPermSize=进行设置。
举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,若是是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部份内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部份内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,通过屡次回收之后若是from区内存也分配完毕,就会也发生内存回收而后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收而后把幸存的对象拷贝至年老区。
一般咱们说的JVM内存回收老是在指堆内存回收,确实只有堆中的内容是动态申请分配的,因此以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是以前提到的MethodArea,不属于Heap。
一、手动将生成的无用对象,中间对象置为null,加快内存回收。
二、对象池技术若是生成的对象是可重用的对象,只是其中的属性不一样时,能够考虑采用对象池来较少对象的生成。若是有空闲的对象就从对象池中取出使用,没有再生成新的对象,大大提升了对象的复用率。
三、JVM调优经过配置JVM的参数来提升垃圾回收的速度,若是在没有出现内存泄露且上面两种办法都不能保证JVM内存回收时,能够考虑采用JVM调优的方式来解决,不过必定要通过实体机的长期测试,由于不一样的参数可能引发不一样的效果。如-Xnoclassgc参数等。