JVM有关的知识点(一)
1. 你了解JAVA虚拟机吗?能解释一下底层的模块吗?
JVM在整个jdk(java 运行环境)中处于最底层,负责与操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也就虚拟计算机. 操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境.
(1)创建JVM装载环境和配置
(2)装载JVM.dll
(3)初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例
(4) 调用JNIEnv实例装载并处理class类。
主要由【程序计数器,虚拟机栈,本地方法栈,堆,方法区】五个主要模块构成
程序计数器
主要是当前线程执行字节码的行号指示器,是线程私有的,如果执行的是java方法,则计数器记录的是正在执行虚拟机字节码的指令地址;如果执行的是native方法,则这个计数器为空。
此内存区域是整个jvm唯一没有规定内存溢出【OutOfMemoryError】的区域。
虚拟机栈(又称java栈)
虚拟机栈其实就是栈内存主要指代的地方,这一部分内存用来存储基础数据类型的对象,以及为对内存中动态变量的引用提供一个地址,也就是java的指针。
这一部分也是【线程私有】,每个方法在执行的同时创建一个栈帧,实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有局部变量表、操作数栈、动态链接、方法出口信息
随着方法执行的结束,这个栈帧也在jvm中进行了入栈与出栈的过程。
这一块可以抛出两个异常:
【stackOverFlowError】:线程请求的栈的深度大于虚拟机预设的深度
【OutOfMemoryError】:jvm无法扩展足够内存
主要分三个区域:局部变量区、运行环境区、操作数区。
-
局部变量区:
每个Java方法使用一个固定大小的局部变量集。它们按照与vars寄存器的字偏移量来寻址。局部变量都是32位的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈的指令,也提供了把操作数栈中的值写入局部变量的指令。
-
运行环境区
在运行环境中包含的信息用于动态链接,正常的方法返回以及异常捕捉。
-
操作数区
机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存器或非通用寄存器的机器(如Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd指令将两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相加,并把结果压回到操作数栈中。
每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了long和double型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个int类型的数,如果把它们当作是一个long类型的数则是非法的。在Sun的虚拟机实现中,这个限制由字节码验证器强制实行。但是,有少数操作(操作符dupe和swap),用于对运行时数据区进行操作时是不考虑类型的。
本地方法栈:
与虚拟机栈很相似,但是虚拟机栈主要为java方法(也就是字节码)提供内存服务;
而本地方法栈则为虚拟机使用到的 Native 方法服务。
因为native的原因,本地方法栈对栈中方法的语言,使用方式,数据结构内都没有要求,比较自由,在 HotSpot 虚拟机中将虚拟机栈与本地方法栈和二为一。
抛出异常与虚拟机栈相同。
堆:
占用虚拟机内存最大的一块,被所有【线程共享】因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的,启动虚拟机就会创建这一块内存区域。这一块内存区域的唯一作用就是存放对象实例。
java堆是jvm垃圾管理收集的主要区域,因此很多时候也被称为GC堆
【OutOfMemoryError】:jvm无法扩展足够内存
方法区:
【线程共享】区域,此点与堆相同,但是方法区主要存储的是已经被jvm加载的类的信息,常量,静态变量等。jvm规范是把方法区归为堆的一个逻辑部分,
但是却仍然有区别:根据分代收集算法方法区算是永久代。
【OutOfMemoryError】:jvm无法扩展足够内存
运行时常量池:
运行时常量池是方法区的一部分。Class文件中存在类的版本,属性,字段,方法,接口等以及运行时常量池(用于存放编译期生成的各种字面量和符号引用),主要存储的是类加载编译时期生成的各种字面变量和符号引用。
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
Java虚拟机从启动到结束的生命周期,当Java虚拟机启动后,在如下几种情况下,Java虚拟机将结束生命周期:
(1)执行了System.exit()方法
(2)程序正常执行结束
(3)程序在执行过程中遇到了异常或错误而异常终止
(4)由于操作系统出现错误而导致Java虚拟机进程终止
2. JRE/JDK/JVM是什么关系
JRE(JavaRuntimeEnvironment,Java运行环境) 也就是Java平台。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。
JDK(Java Development Kit) 是程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
JVM(JavaVirtualMachine,Java虚拟机) 是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
3. JVM原理
JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。
java程序运行的一个全过程图例:
java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。
4. JVM内存模型及调优
4.1 JVM内存模型
JDK1.7内存区域划分
JDK1.8内存区域划分
上面提到并介绍过的不在讲,重点讲解直接内存和元数据区
-
直接内存
直接内存并不是JVM运行时数据区的一部分, 但也会被频繁的使用: 在JDK 1.4引入的NIO提供了基于Channel与Buffer的IO方式, 它可以使用Native函数库直接分配堆外内存, 然后使用DirectByteBuffer对象作为这块内存的引用进行操作, 这样就避免了在Java堆和Native堆中来回复制数据, 因此在一些场景中可以显著提高性能.
本机直接内存的分配不会受到Java堆大小的限制(即不会遵守-Xms、-Xmx等设置), 但既然是内存, 则肯定还是会受到本机总内存大小及处理器寻址空间的限制, 因此动态扩展时也会出现OutOfMemoryError异常. -
元数据区域
元数据区域取代了1.7版本及以前的永久代。元数据和永久代本质上都时方法区的实现。
参数设置:-XX:MetaspaceSize=18m
-XX:MaxMetaspaceSize=60m
4.2. 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来设置
调优步骤:
- 监控GC的状态
使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。
举一个例子: 系统崩溃前的一些现象:
- 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
- FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
- 年老代的内存越来越大并且每次FullGC后年老代没有内存被释放
之后系统会无法响应新的请求,逐渐到OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。
- 生成堆的dump文件
通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。
-
分析dump文件
几种工具打开该文件:
- Visual VM
- IBM HeapAnalyzer
- JDK 自带的Hprof工具
- Mat(Eclipse专门的静态内存分析工具)推荐使用
备注:文件太大,建议使用Eclipse专门的静态内存分析工具Mat打开分析
- 分析结果,判断是否需要优化
如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。
注:如果满足下面的指标,则一般不需要进行GC:
- Minor GC执行时间不到50ms;
- Minor GC执行不频繁,约10秒一次;
- Full GC执行时间不到1s;
- Full GC执行频率不算频繁,不低于10分钟1次;
- 调整GC类型和内存分配
如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择
- 不断的分析和调整
通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。
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数。并行收集线程数。
参考 & 拓展
深入理解Java虚拟机 https://book.douban.com/subject/24722612/
JVM内幕:Java虚拟机详解 (荐) http://www.importnew.com/17770.html
JAVA的内存模型及结构 http://ifeve.com/under-the-hood-runtime-data-areas-javas-memory-model/
JVM内存模型与调优 https://blog.csdn.net/qq_39712188/article/details/84840613