一、什么是JVM
JVM是Javavirtualmachine(Java虚拟机)的缩写,JVM是计算设备的规范,是通过在实际计算机上模拟各种计算机功能实现的虚构计算机
Java语言的一个非常重要的特征是与平台的无关性。 要做到这一点,使用Java虚拟机很重要。 要在不同的平台上运行高级语言(如),必须至少将其编译为不同的目标代码。 部署Java语言虚拟机后,在不同的平台上运行Java语言时无需重新编译。 Java语言使用Java虚拟机屏蔽有关特定平台的信息。 这样,Java语言编译器只需生成在Java虚拟机上运行的目标代码(字节码),就可以在各种平台上运行,而无需修改。 运行字节码时,Java虚拟机会将字节码解释为特定平台上的机器指令来运行。 这就是Java“一次编译并到处运行”的原因。
通过查看Java平台的逻辑结构,可以从下图中了解JVM。
从上图可以清楚地看到Java平台中包含的每个逻辑模块,还可以看到JDK和JRE之间的区别。 从下图俯瞰一下JVM自身的物理结构。
二、JAVA代码的编译和执行过程
Java代码的编译由Java源编译器执行,如下一个流程图所示。
JVA字节码的执行由JVM执行引擎完成,流程图如下所示。
编译和运行ava代码的整个过程包含三个重要机制:
Java源代码的编译机制
类加载机制
类执行机制
Java源代码的编译机制
编译Java源代码由三个进程组成:
分析和输入符号表
注释处理
语义分析和class文件的生成
流程图如下。
最后生成的class文件由以下部分组成:
结构信息。 包含class文件格式的版本号以及每个节的数量和大小信息
元数据。 与Java源代码中的声明和常量相对应的信息。 包括类/继承的超类/实现的接口的声明信息、域和方法的声明信息以及常量池
方法信息。 与Java源代码中的语句和表达式相对应的信息。 包括字节代码、异常处理器表、评估堆栈和局部变量区域大小、评估堆栈类型记录、调试符号信息
类加载机制
JVM的类加载由ClassLoader及其子类完成,可以在下图中说明类的层次关系和加载顺序。
1 ) bootstrap类加载器
负责加载$JAVA_HOME中的jre/lib/rt.jar中的所有class,并由c而不是ClassLoader子类实现
2 )扩展类加载器
负责加载java平台中的扩展功能的jar包,包括在$JAVA_HOME中由jre/lib/*.jar或-Djava.ext.dirs指定的目录中找到的jar包
3 ) app类加载器
负责描述在classpath中指定的jar包和目录中的class
4 )自定义类加载器
这是一个ClassLoader,APP应用程序(如tomcat和jboss )根据自己的需求定制。 jboss首先检查在加载ClassLoader时是否按照j2ee规范加载了类。 检查顺序是按层次从Custom ClassLoader到BootStrap ClassLoader进行检查,加载顺序为自上而下,即从上层逐层尝试这样的加载。
类执行机制
JVM在基于堆栈的体系结构中执行class字节码。 创建线程后,将生成程序计数器[PC]和堆栈,其中包含要执行的下一个指令在方法中的偏移。 堆栈包含与每个方法的调用相对应的堆栈帧,堆栈帧包含用于存储方法中局部变量和参数的局部变量区域,以及用于存储方法执行期间生成的中间结果的操作数堆栈区域。 的结构如下图所示。
三. JVM内存管理和垃圾回收
JVM内存配置
JVM堆栈由堆、堆栈、本地方法堆栈和方法区域等部分组成,结构图如下:
1 )堆
在new中创建的所有对象的内存都分配给堆,并且可以通过-Xmx和-Xms控制堆的大小。 堆分为亮哆啦A梦代和老代,亮哆啦A梦代又分为djm和Survivor两部分,最后Survivor由From Space和To Space组成,结构图如下:
明亮的哆啦A梦一代。 所有新对象都以明亮的哆啦A梦替代内存分配,当djm空间不足时,将生存对象移动到Survivor。
开朗的机器猫代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制djm和Survivor的比例旧生代。用于存放开朗的机器猫代中经过多次垃圾回收仍然存活的对象
持久带(Permanent Space)实现方法区,主要存放所有已加载的类信息,方法信息,常量池等等。可通过-XX:PermSize和-XX:MaxPermSize来指定持久带初始化值和最大值。Permanent Space并不等同于方法区,只不过是Hotspot JVM用Permanent Space来实现方法区而已,有些虚拟机没有Permanent Space而用其他机制来实现方法区。
-Xmx:最大堆内存,如:-Xmx512m
-Xms:初始时堆内存,如:-Xms256m
-XX:MaxNewSize:最大年轻区内存
-XX:NewSize:初始时年轻区内存.通常为 Xmx 的 1/3 或 1/4。开朗的机器猫代 = djm + 2 个 Survivor 空间。实际可用空间为 = djm + 1 个 Survivor,即 90%
-XX:MaxPermSize:最大持久带内存
-XX:PermSize:初始时持久带内存
-XX:+PrintGCDetails。打印 GC 信息
-XX:NewRatio 开朗的机器猫代与老年代的比例,如 –XX:NewRatio=2,则开朗的机器猫代占整个堆空间的1/3,老年代占2/3
-XX:SurvivorRatio 开朗的机器猫代中 djm 与 Survivor 的比值。默认值为 8。即 djm 占开朗的机器猫代空间的 8/10,另外两个 Survivor 各占 1/10
2)栈
每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。
-xss:设置每个线程的堆栈大小. JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。
3)本地方法栈
用于支持native方法的执行,存储了每个native方法调用的状态
4)方法区
存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(Permanet Generation)来存放方法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值
垃圾回收按照基本回收策略分
引用计数(Reference Counting):
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
标记-清除(lqdbg-Sweep):
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
复制(Copying):
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
标记-整理(lqdbg-Compact):
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
JVM分别对开朗的机器猫代和旧生代采用不同的垃圾回收机制
开朗的机器猫代的GC:
开朗的机器猫代通常存活时间较短,因此基于Copying算法来进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于开朗的机器猫代,就是在djm和From Space或To Space之间copy。开朗的机器猫代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在开朗的机器猫代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到survivor,最后到旧生代。
在执行机制上JVM提供了串行GC(Serial GC)、并行回收GC(Parallel Scavenge)和并行GC(ParNew)
1)串行GC
在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、开朗的机器猫代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定
2)并行回收GC
在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数
3)并行GC
与旧生代的并发GC配合使用
旧生代的GC:
旧生代与开朗的机器猫代不同,对象存活的时间比较长,比较稳定,因此采用标记(lqdbg)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行GC(Serial MSC)、并行GC(parallel MSC)和并发GC(CMS),具体算法细节还有待进一步深入研究。
以上各种GC机制是需要组合使用的,指定方式由下表所示:
指定方式
开朗的机器猫代GC方式
旧生代GC方式
-XX:+UseSerialGC
串行GC
串行GC
-XX:+UseParallelGC
并行回收GC
并行GC
-XX:+UseConelqdbgSweepGC
并行GC
并发GC
-XX:+UseParNewGC
并行GC
串行GC
-XX:+UseParallelOldGC
并行回收GC
并行GC
-XX:+ UseConelqdbgSweepGC
-XX:+UseParNewGC
串行GC
并发GC
不支持的组合
1、-XX:+UseParNewGC -XX:+UseParallelOldGC
2、-XX:+UseParNewGC -XX:+UseSerialGC
四、JVM内存调优
首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。特别要关注Full GC,因为它会对整个堆进行整理,导致Full GC一般由于以下几种情况:
旧生代空间不足
调优时尽量让对象在开朗的机器猫代GC时被回收、让对象在开朗的机器猫代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象
Pemanet Generation空间不足
增大Perm Gen空间,避免太多静态对象
统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间
控制好开朗的机器猫代和旧生代的比例
System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠JVM自身的机制
调优手段主要是通过控制堆内存的各个部分的比例和GC策略来实现,下面来看看各部分比例不良设置会导致什么后果
1)开朗的机器猫代设置过小
一是开朗的机器猫代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC
2)开朗的机器猫代设置过大
一是开朗的机器猫代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是开朗的机器猫代GC耗时大幅度增加
一般说来开朗的机器猫代占整个堆1/3比较合适
3)Survivor设置过小
导致对象从eden直接到达旧生代,降低了在开朗的机器猫代的存活时间
4)Survivor设置过大
导致eden过小,增加了GC频率
另外,通过-XX:MaxTenuringThreshold=n来控制开朗的机器猫代存活时间,尽量让对象在开朗的机器猫代被回收
由内存管理和垃圾回收可知开朗的机器猫代和旧生代都有多种GC策略和组合搭配,选择这些策略对于我们这些开发人员是个难题,JVM提供两种较为简单的GC策略的设置方式
1)吞吐量优先
JVM以吞吐量为指标,自行选择相应的GC策略及控制开朗的机器猫代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置
2)暂停时间优先
JVM以暂停时间为指标,自行选择相应的GC策略及控制开朗的机器猫代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置
最后汇总一下JVM常见配置
堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中djm区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示djm:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConclqdbgSweepGC:设置并发收集器
垃圾回收统计信息
-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数。并行收集线程数。