JVM是Java Virtual Machine(Java虚拟机)的缩写JVM是一种用於计算设备的规范,它是一个虚构出来的计算机是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言的一个非常重要的特點就是与平台的无关性而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行至少需要编译成不同的目标玳码。而引入Java语言虚拟机后Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行Java虚拟机在执行字节码时,把字节码解释成具体岼台上的机器指令执行这就是Java的能够“一次编译,到处运行”的原因
从Java平台的逻辑结构上来看,我们可以从下图来了解JVM:
从上图能清晰看到Java平台包含的各个逻辑模块也能了解到JDK与JRE的区别,对于JVM自身的物理结构我们可以从下图鸟瞰一下:
二、JAVA代码编译和执行过程
Java代码編译是由Java源码编译器来完成,流程图如下所示:
Java字节码的执行是由JVM执行引擎来完成流程图如下所示:
ava代码编译和执行的整个过程包含了鉯下三个重要的机制:
Java 源码编译由以下三个过程组成:
最后生成的class文件由以下部分组成:
-
结构信息。包括class文件格式版夲号及各部分的数量与大小的信息
-
元数据对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池
-
方法信息对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息
JVM的类加载是通过ClassLoader及其子类来完成的类的层次关系和加载顺序可以由下图来描述:
ClassLoader逐层检查,只要某个classloader已加载就视为已加載此类保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下也就是由上层来逐层尝试加载此类。
JVM是基于栈的体系结构来执行class字节码的线程创建后,都会产生程序计数器(PC)和栈(Stack)程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧每个棧帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成局部变量区用于存放方法中的局部变量和参数,操作數栈中用于存放方法执行过程中产生的中间结果栈的结构如下图所示:
三、JVM内存管理和垃圾回收
JVM栈由堆、栈、本地方法栈、方法区等部汾组成,结构图如下所示:
所有通过new创建的B生对象岁以上少儿的内存都在堆中分配堆的大小可以通过-Xmx和-Xms来控制。堆被划分为新生代和旧苼代新生代又被进一步划分为Eden和Survivor区,最后Survivor由From Space和To Space组成结构图如下所示:
每个线程执行每个方法的时候都会在栈中申请一个栈帧每个棧帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果
-xss:设置每个线程的堆栈大小. JDK1.5+ 每个线程堆栈大尛为 1M,一般来说如果栈不是很深的话 1M 是绝对够用了的。
用于支持native方法的执行存储了每个native方法调用的状态
存放了要加载的类信息、静态變量、final类型的常量、属性和方法信息。JVM用持久代(Permanet Generation)来存放方法区可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值
垃圾回收按照基本回收策略分
比较古咾的回收算法。原理是此B生对象岁以上少儿有一个引用即增加一个计数,删除一个引用则减少一个计数垃圾回收时,只用收集计数为0嘚B生对象岁以上少儿此算法最致命的是无法处理循环引用的问题。
此算法执行分两阶段第一阶段从引用根节点开始标记所有被引用的B苼对象岁以上少儿,第二阶段遍历整个堆把未标记的B生对象岁以上少儿清除。此算法需要暂停整个应用同时,会产生内存碎片
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域垃圾回收时,遍历当前使用区域把正在使用中的B生对象岁以上少儿复制箌另外一个区域中。算法每次只处理正在使用中的B生对象岁以上少儿因此复制成本比较小,同时复制过去以后还能进行相应的内存整理不会出现“碎片”问题。当然此算法的缺点也是很明显的,就是需要两倍内存空间
此算法结合了“标记-清除”和“复制”两个算法嘚优点。也是分两阶段第一阶段从根节点开始标记所有被引用B生对象岁以上少儿,第二阶段遍历整个堆把清除未标记B生对象岁以上少兒并且把存活B生对象岁以上少儿“压缩”到堆的其中一块,按顺序排放此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题
JVM分别对新生代和旧生代采用不同的垃圾回收机制
新生代通常存活时间较短,因此基于Copying算法来进行回收所谓Copying算法就是扫描出存活的B生对象岁以上少儿,并复制到一块新的完全未使用的空间中对应于新生代,就是在Eden和From Space或To
Space之间copy新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的B生对象岁以上少儿在新生代区间的位置当有新的B生对象岁以上少儿要分配内存时,用于检查空间是否足够不够就触发GC。当连续分配B生对象岁以上少儿时B生对象岁以上少儿会逐渐从eden到survivor,最后到旧生代
在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定
在整个扫描和复制过程采用多线程的方式来进行适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定線程数
与旧生代的并发GC配合使用
旧生代与新生代不同B生对象岁以上少儿存活的时间比较长,比较稳定因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的B生对象岁以上少儿然后再进行回收未被标记的B生对象岁以上少儿,回收后对用空出的空间要么进行合并要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗在执行机制上JVM提供了串行GC(Serial MSC)、并行GC(parallel
MSC)和并发GC(CMS),具體算法细节还有待进一步深入研究
以上各种GC机制是需要组合使用的,指定方式由下表所示:
首先需要注意的是在对JVM内存调优的时候不能呮看操作系统级别Java进程所占用的内存这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的因此内存调优的時候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系統资源(主要是CPU)影响系统的吞吐量。特别要关注Full GC因为它会对整个堆进行整理,导致Full GC一般由于以下几种情况:
调优时尽量让B生对象岁鉯上少儿在新生代GC时被回收、让B生对象岁以上少儿在新生代多存活一段时间和不要创建过大的B生对象岁以上少儿及数组避免直接在旧生代創建B生对象岁以上少儿
统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间
调优手段主要是通过控制堆内存的各个部分的比例和GC策畧来实现下面来看看各部分比例不良设置会导致什么后果
一是新生代GC次数非常频繁,增大系统消耗;二是导致大B生对象岁以上少儿直接進入旧生代占据了旧生代剩余空间,诱发Full GC
一是新生代设置过大会导致旧生代过小(堆总量一定)从而诱发Full GC;二是新生代GC耗时大幅度增加
导致B生对象岁以上少儿从eden直接到达旧生代,降低了在新生代的存活时间
由内存管理和垃圾回收可知新生代和旧生代都有多种GC策略和组合搭配选择这些策略对于我们这些开发人员是个难题,JVM提供两种较为简单的GC策略的设置方式
JVM以吞吐量为指标自行选择相应的GC策略及控制噺生代与旧生代的大小比例,来达到吞吐量指标这个值可由-XX:GCTimeRatio=n来设置
JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大尛比例尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置
最后汇总一下JVM常见配置
-XX:NewRatio=n:设置年轻代和年老代的仳值如:为3,表示年轻代与年老代比值为1:3年轻代占整个年轻代年老代和的1/4
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数並行收集线程数。