本帖最后由 时间留下最真 于 2019-1-17 15:06 编辑
一、什么是JVM JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。 二、JVM的体系结构
1. 类加载器子系统主要用于定位类定义的二进制信息,然后将这些信息解析并加载至虚拟机,转化为虚拟机内部的类型信息的数据结构。类加载器子系统还承担着安全性的责任,并且是JVM的动态链接和动态加载的基础。将二进制信息=>类型信息的数据结构,中间需要经过很多步骤。首先类加载器是JVM安全沙箱的第一道防线,能够防止非信任类破坏虚拟机。每一个被加载的class文件需要经过四次校验才能被加载。校验通过后,类加载器的命名空间和运行时包的特性能够防止非信任类伪装成信任类来破坏虚拟机。类加载器在方法区构造具有这个类的信息的数据结构后,会在堆上创建一个Class对象作为访问这个数据结构的接口。同时,类加载还需要初始化类的静态数据,也就是调用类的<clinit>方法。以上就是一个类的加载、链接及初始化的过程。
2. 运行时数据区是JVM运行时的内存空间的组织,逻辑上又划分为多个区,这些区的生命周期和它是否线程共享有关,它们分别是:
堆:用于存放对象或数组实例,也就是运行期间new出来的对象。堆的生命周期与JVM相同,并且在线程之间共享访问。由于多线程并发访问,所以需要考虑线程安全的问题,有两种方法。第一种是,加锁进行互斥访问。第二种是线程本地分配缓冲(Thread Local Allocate Buffer, TLAB),在线程创建时预先给每个线程分配一块区域,这块区域是线程私有的,对其他线程是不可见,也就不会被共享。JVM规范规定在申请不到足够的内存时,堆会抛出OutOfMemoryException。
方法区:存放类型信息和运行时常量池(Runtime Constant Pool)。每个被类加载器加载的类都会在方法区中形成一个与子对应的类型信息的数据结构,包括:这个类的类名、直接超类、实现的接口列表、字段列表、方法列表等。运行时常量池是class文件中的常量池列表(Constant Pool List)在运行时的一种体现,其中存储各种基本数据类型及String类型的常量以及其他类、方法、字段的符号引用。方法区的生命周期与JVM相同,被多个线程共享,所以要考虑并发访问的安全性的问题。JVM规范规定在需要的内存得不到满足的情况下,方法区会抛出OutOfMemoryException。
PC(Program Counter):线程私有的,生命周期与线程相同,是对CPU中PC的一种模拟。如果线程正在执行的是Java方法,则该线程的PC中存放的下一条字节码指令的地址。在进行Java方法的调用和返回时,需要更新PC以保存当前方法(Current Method)正在执行的字节码指令的地址。PC是JVM规范中唯一没有规定会抛出异常的存储区。
JVM栈:线程私有,生命周期与线程相同,是对传统语言(比如C)中的方法调用栈的一种模拟。JVM栈中存放栈帧(Frame)用于进行方法调用和返回、存储局部变量以及计算的中间结果。JVM规范规定栈可以抛出两种异常:(1)StackOverflowException,在栈的深度大于某个规定值的情况下抛出。(2)OutOfMemoryException,在为新栈帧分配内存或者是为线程分配栈的内存时,申请不到足够的内存的情况下抛出。
JVM栈中存放的是栈帧,每个栈帧对应着一次方法调用。每一时刻,JVM线程只能执行一个方法(Current Method),该方法的栈帧是JVM栈的栈顶的元素(叫做当前栈帧,Current Frame),当调用一个方法时,会初始化一个栈帧压入JVM栈;当方法调用返回或者抛出异常没有被处理的情况下,JVM栈会弹出该方法对应的栈帧。每一个栈帧中存放局部变量表(Local Variable Table)、操作数栈(Oprand Stack)以及其他栈帧信息。栈帧的大小在编译时就确定了,编译器会把局部变量表和操作数栈的大小记录在class文件中method_info的属性表中。局部变量表类似于数组存放局部变量和方法参数。由于JVM采用的是基于栈的指令集体系结构,而不是基于寄存器,所以JVM上的所有计算都是在操作数栈上进行的(比如,算术运算、方法调用、内存访问等)。
本地方法栈:用于支持本地方法调用,抛出的异常与JVM栈相同。
3. 执行引擎用于执行JVM字节码指令,主要由两种实现方式:(1)将输入的字节码指令在加载时或执行时翻译成另外一种虚拟机指令;(2)将输入的字节码指令在加载时或执行时翻译成宿主主机本地CPU的指令集。这两种方式对应着字节码的解释执行和即时编译。比如在HotSpot VM中执行引擎的实现是一种解释-编译的层次结构:
(1)解释执行:解释执行字节码,并以方法为单位收集“热点(HotSpot)代码”的信息,将“热点代码”执行C0编译。
(2)C0编译:将收集的“热点代码”编译成本地代码,并进行一些简单的优化。继续收集运行时信息,将一些频繁执行的本地代码进行C1编译。
(3)C1编译:将C0阶段的本地代码,进行一些比较激进的优化。如果某些优化导致本地代码执行失败,此时JVM会退化到解释执行字节码阶段。
4. 自动内存管理用于管理运行时数据区的分配和释放。和C和C++相比,Java不需要程序员主动的管理内存(在new出对象后,不需要显示的delete),这样JVM就需要承担内存管理这个任务。内存管理的重点主要是在申请内存(new对象、类加载和初始化、启动线程时初始化栈等)得不到满足时,JVM可以自动回收那些不再存活的对象所占用的内存,也就是经常听到的垃圾收集。在回收过程中还要保证处理内存空间的碎片,以提高空间利用率。回收过程主要有两个关键点,标记存活对象和回收内存的算法。
标记存活对象主要有引用计算和根搜索法两种。
(1)引用计数,是一种很普遍的方法,在python、lua等一些脚本语言中都是使用这种算法。每个对象持有一个计数器,标记这个对象被引用的次数。进行垃圾收集时,那些引用计数为0的对象就是“死”对象,需要被收集。引用计数的一个缺点就是它没有办法处理循环引用的情况(A->B, B->A)。
(2)根搜索,HotSpot虚拟机采用这种算法标记存活对象。把方法区、JVM栈中的所有的引用组成的集合作为搜索的根,从这个集合开始遍历直到结束。其中被遍历到的对象是存活对象;那些没有被遍历到的对象需要被垃圾收集。这样可以有效的避免循环引用的情况。
回收内存的算法主要有:
(1)复制算法,将内存分成两个部分,每一时刻只是用其中的一个。进行回收时,将所有存活的对象依次复制到另一个部分(依次复制避免了内存碎片的产生),接下来只用这一个部分。复制算法需要在两个内存区域来回复制,有一定的复制开销和空间开销(每一时刻只使用一个区域),但是可以很好的解决内存碎片的问题,适用于对象频繁创建并且生命周期短的情况。
(2)标记清扫,先进行存活对象标记,回收时将“死”对象占用的内存直接释放掉,会产生大量的内存碎片。
(3)标记整理,标记阶段与标记清扫算法一样,回收阶段释放“死”对象的内存后,还需要进行对象的移动使得所有对象依次在内存中排列,避免了内存碎片的产生。标记整理与复制算法相反,适用于对象创建不频繁,生命周期长得情况。
(4)按代收集,将内存按照对象生命周期的不同划分为多个部分,每个部分采用不同的收集算法。目前,大部分商业虚拟机都是采用这种算法。比如,在HotSpot中,内存被划分为:新生代(New)、老年代(Old)和永久代(Perm)。新生代采用复制算法,老年代和永久代采用标记整理算法。内存分配、回收的策略是,对象首先在新生代分配,如果新生代内存不满足要求,则触发一次新生代内存的垃圾收集(Young GC,或者是Minor GC)。Young GC会导致部分新生代的对象被移动至老年代,一部分是因为新生代内存不足以放下所有的对象;另一部分是因为这些对象的年龄(每个对象都保存着这个对象被垃圾收集的次数,表示它的年龄。存储在对象头的age属性中)大到足以晋升到老年代。当新生代的对象进入老年代,而老年代的内存不满足要求时,则会触发一次整个新生代和老年代的垃圾收集(Full GC, 或者是Major GC)。
三、JVM常见配置
堆设置 -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:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename 并行收集器设置 -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。 -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) 并发收集器设置 -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。 -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
|