认识JVM虚拟机
Posted 悦码
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了认识JVM虚拟机相关的知识,希望对你有一定的参考价值。
JVM是什么
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
Java虚拟机的结构
这张图参考了网上广为流传的JVM构成图。
运行时数据区
程序执行期间使用各种运行时数据区域。其中一些数据区域是在Java虚拟机启动时创建的,仅在Java虚拟机退出时才被销毁,例如方法区,堆区数据区域。其他区域为线程私有,线程退出时每个数据区域被销毁,例如PC程序计数器,虚拟机栈,本地方法栈。
PC程序计数器
字节码解释器工作时,就是通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支,跳转,循环,异常处理等基础功能都需要这个计数器来完成。
Java是一门支持多线程的语言,所谓多线程实则是通过CPU的时间片轮转调度算法来实现的,在一个时间片内,处理器只会执行一个线程的指令。
为了保证线程切换过程时可以恢复到原来的执行位置,每条线程都会有一个独立的程序计数器(即线程私有),各个线程之间互相不影响,独立存储
程序计数器是唯一一个在Java虚拟机规范没有规定任何内存溢出情况的区域。
Java虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型,每个方法被执行的同时会创建一个栈帧,用于存储方法中的局部变量表,操作数栈,动态连接,方法的出口等信息,每个方法从调用知道执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
此区域会出现的两种异常StackOverflowError异常:线程请求的栈太深大于虚拟机所允许的深度。
OOM异常当扩展时无法申请足够的内存时抛出。
局部变量表
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量的大小。
本地方法栈
与Java虚拟机栈作用类似,他们区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中使用到的语言、方式和数据结构并无强制规定,因此具体的虚拟机可实现它。有的虚拟机直接把本地方法栈和虚拟机栈合二为一。
堆
每个线程所共享的的内存区域,类的实例和数组在堆中分配内存,堆在虚拟机启动时创建。
堆中没有内存完成实例分配,并且堆也无法再扩展时,也会抛出OOM异常。
方法区
存储已被虚拟机加载的类信息、常量、静态变量、即是编译器编译后的代码等数据。
垃圾收集行为在这个区域比较少出现,这个区域的内存回收目标是针对常量池的回收和对类型的卸载
运行时常量池
用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。
双亲委派模型
如果一个类加载器收到了类加载请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,只有当父类加载器无法完成加载请求,子类加载器才会尝试加载类。
类加载器
启动类加载器:Bootstrap ClassLoader,是虚拟机的一部分,用来加载核心类库。Java_HOME/lib/目录中的,或者被-Xbootclasspath参数所指定的路径中并且被虚拟机识别的类库
扩展类加载器:Extension ClassLoader,负责加载Java的扩展库。加载/lib/ext目录或者Java.ext.dirs系统变量指定的路径中的所有类库。
应用程序类加载器:Application ClassLoader,负责加载用户类路径classpath上的指定类库,我们可以直接使用这个类加载器。一般情况我们没有自定义类加载器默认就是用这个加载器。
类的生命周期
类被加载到虚拟机内存中开始,到卸载出内存为止,他的整个生命周期其实包括7个阶段。
加载,验证,准备,解析,初始化,使用,卸载。
其中加载,验证,准备,初始化和解析这五个阶段是顺序进行的,而解析就不一定了。
类的加载过程
就是类生命周期的前五个阶段
加载虚拟机需要完成一下3件事情
(1)通过一个类的权限定名来获取此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络,动态生成,数据库等)。
(2)这个字节流所代表的静态存储结构转化为方法区运行时数据结构。
(3)在内存中(对于HotSpot虚拟机而言就是方法区)生成一个能代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证大致会需要完成4个阶段的检验动作。
(1)文件格式验证:验证字节流是否符合Class文件格式的规范(版本号是否在虚拟机处理范围之内,常量池中的常量是否有不被支持的类型)
(2)元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求(例如:这个类是否有父类,除了java.lang.Object之外);
(3)字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的;
(4)符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响。如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备准备阶段是正式为类变量(static成员变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义如下:
解析
解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。解析动作主要针对类或者接口,字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用。
初始化
初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全有虚拟机主导和控制,到了初始化阶段,才真正开始执行类中定义的Java程序代码(字节码)。
在准备阶段,变量已经赋值过一次系统要求的初始值(零值);而在初始化阶段,则根据程序指定的主观计划去初始化类变量和其他资源,或者更直接地说:初始化阶段是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。
对象的创建
使用Class的newInstance方法 调用了构造方法
使用Constructor类的newInstance方法 调用了构造方法
JVM垃圾回收机制
在Java中,程序员不需要显示的去释放一个对象的内存,而是由虚拟机自动执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到回收的集合中,进行回收
垃圾回收算法
引用计数法
可以看到循环引用下JVM回收不了对象。
可达性分析法 GC Root
(1)虚拟机栈(栈帧中的本地变量表)中引用的对象;
(2)方法区中类静态属性引用的对象;
(3)方法区中常量引用的对象;
(4)本地栈中引用的对象。
java中的引用
引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。
强引用
类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用
软引用用来描述一些还有用,但并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。
如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用
被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。
回收策略/算法
标记-清除
先标记,再清除,清除之后会有很多空间碎片,如果后面需要分配大的对象,就会导致没有连续的内存可供使用。
标记-整理
先标记,然后清除整理,就解决了空间碎片的问题。然后又出现了一个新的问题,每次都得移动对象。
复制算法
把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活的对象复制到另一个区域中,最后将当前使用的区域的可回收的对象进行回收。
分代收集
根据对象的存活周期将内存划分为几块。一般是把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。
垃圾收集器
Java中根据不同的场景提供了三个种类收集器,分别是串行、吞吐量优先和用户响应时间优先
并行:指多条垃圾收集器线程一起工作,但此时用户线程仍然处于等待状态
并发:指用户线程和垃圾收集器线程同时执行(不一定是并行的,可能会交替执行)。用户线程继续运行,而垃圾收集器运行于另一个CPU上。
CMS(并发GC)收集器
整个收集过程大致分为4个步骤
(1)初始标记
(2)并发标记
(3)重新标记
(4)并发清除
其中初始标记、重新标记这两个步骤需要停顿其他用户线程。CMS由于使用的是标记-清除算法,所以还是会产生大量碎片,空间碎片太多时,将会给对象的分配带来很多麻烦,比如大对象的分配,内存空间找不到连续的空间来分配不得不提前出发一次Full GC,为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection参数。
用于再Full GC之后增加一个碎片整理过程,还可以通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
G1收集器(用户响应时间优先)
(1)并发与并行:
(2)分代收集
(3)空间整合
(4)可预测停顿
G1同样存在年代的概念,内部类似棋盘状的一个个region组成。
region介绍
大小一致,数值是在1M到32M字节之间的一个2的幂止树,JVM会尽量划分2048个左右、同等大小的region。这个数字可以手动调整,G1也会根据堆大小自动进行调整。
G1实现中,一部分region是作为Eden,一部分作为Survivor,一部分为Old,G1会将超过region50%大小的对象归类为Humongous对象,并放置在相应的region。逻辑上,Humongous region算是老年代的一部分,因为复制这样的大对象是很昂贵的操作,并不适合新生代GC的复制算法。
G1的缺点。
region 大小和大对象很难保证一致,这会导致空间的浪费;特别大的对象是可能占用超过一个 region 的。并且,region 太小不合适,会令你在分配大对象时更难找到连续空间,这是一个长久存在的情况。
GC 算法的角度,G1 选择的是复合算法,可以简化理解为:
在新生代,G1 采用的仍然是并行的复制算法,所以同样会发生 Stop-The-World的暂停。新生代的清理会带上old区已标记好的region。
在老年代,大部分情况下都是并发标记,而整理(Compact)则是和新生代GC时捎带进行,并且不是整体性的整理,而是增量进行的,也就是原本新生代的区域中对象在足够old时,该区域可以直接成为老生代。
Humongous 对象的分配和回收
Humongous region 作为老年代的一部分,通常认为它会在并发标记结束后才进行回收,但是在新版 G1 中,Humongous 对象回收采取了更加激进的策略。我们知道 G1 记录了老年代 region 间对象引用,Humongous 对象数量有限,所以能够快速的知道是否有老年代对象引用它。如果没有,能够阻止它被回收的唯一可能,就是新生代是否有对象引用了它,但这个信息是可以在 Young GC 时就知道的,所以完全可以在 Young GC 中就进行 Humongous 对象的回收,不用像其他老年代对象那样,等待并发标记结束。
虚拟机性能监控
虚拟机进程状况工具-jps:可以列出正在运行的虚拟机进程。
显示虚拟机执行主类名以及这些进程的本地虚拟机唯一ID。
虚拟机统计信息监控工具-jstat:用于监视虚拟机各种运行状态信息的命令工具。
显示虚拟机中的类装载、内存、垃圾收集、JIT编译等运行数据
Java内存映像工具-jmap:用于生存堆转储快照
还可以查询finalize执行队列,Java堆和永久代的详细信息。
如空间使用率,当前使用的收集器
Java堆栈跟踪工具-jstack:生存虚拟机当前时刻的线程快照
Java监视与管理工具-JConsole:可视化监控、管理工具
可以看见JVM中全部信息,包括内存、线程、类、VM摘要和GC信息
JVM调优思路
查看堆信息 判断是否堆空间太小
查看Full GC情况 判断是否内存泄漏
使用合理的垃圾收集器
堆空间的最小内存和最大内存设置为一样 会存在扩容和缩容的情况
JVM常用参数举例
内存设置
GC设置
调试参数
编辑整理 丨付君华
以上是关于认识JVM虚拟机的主要内容,如果未能解决你的问题,请参考以下文章