认识JVM虚拟机

JVM是什么

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

Java虚拟机的结构

在这里插入图片描述
这张图参考了网上广为流传的JVM构成图。

运行时数据区

程序执行期间使用各种运行时数据区域。其中一些数据区域是在Java虚拟机启动时创建的,仅在Java虚拟机退出时才被销毁,例如方法区堆区数据区域。其他区域为线程私有,线程退出时每个数据区域被销毁,例如PC程序计数器虚拟机栈本地方法栈

PC程序计数器

每个CPU都有一个PC寄存器即程序计数器,PC程序计数器记录当前线程所执行的字节码指令的地址,通常也叫做行号指示器。
字节码解释器工作时,就是通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支,跳转,循环,异常处理等基础功能都需要这个计数器来完成。
Java是一门支持多线程的语言,所谓多线程实则是通过CPU的时间片轮转调度算法来实现的,在一个时间片内,处理器只会执行一个线程的指令。
为了保证线程切换过程时可以恢复到原来的执行位置,每条线程都会有一个独立的程序计数器(即线程私有),各个线程之间互相不影响,独立存储
程序计数器是唯一一个在Java虚拟机规范没有规定任何内存溢出情况的区域。

Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型,方法的执行的同时会创建一个栈帧,用于存储方法中的局部变量表,操作数栈,动态连接,方法的出口等信息,每个方法从调用知道执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

每个线程所共享的的内存区域,类的实例和数组在堆中分配内存,堆在虚拟机启动时创建。

方法区

存储每个类的结构,例如运行时常量池,静态字段和方法数据,以及方法和构造函数的代码,包括用于类和实例化以及接口初始化的特殊方法,运行时常量池也在方法区分配内存

运行时常量池

用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区。

本地方法栈

与Java虚拟机栈作用类似,他们区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中使用到的语言、方式和数据结构并无强制规定,因此具体的虚拟机可实现它。有的虚拟机直接把本地方法栈和虚拟机栈合二为一。

双亲委派模型

如果一个类加载器收到了类加载请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,只有当父类加载器无法完成加载请求,子类加载器才会尝试加载类。

在这里插入图片描述

类加载器

启动类加载器: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成员变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义如下

那么,变量在准备阶段过后的值还是0,而不是111.因为这时候没有开始执行任何Java方法,而把value赋值为
111的putstatic指令是程序被编译后,存放于类构造器方法()之中,所以把value赋值为111的动作将在初始化阶段才会执行。
public static int value=111;
特殊情况:当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定值,所以标注为final之后,value在准备阶段初始值为111而不是0
public static final int value=111;

解析
解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。解析动作主要针对类或者接口,字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用。
初始化
初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全有虚拟机主导和控制,到了初始化阶段,才真正开始执行类中定义的Java程序代码(字节码)。
准备阶段,变量已经赋值过一次系统要求的初始值(零值);而在初始化阶段,则根据程序指定的主观计划去初始化类变量和其他资源,或者更直接地说:初始化阶段是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。

int a=0;
static {
a是对象创建之后才分配内存在堆上。
a;//Non-static field 'a' cannot be referenced from a static context
}

在这里插入图片描述

对象的创建

使用new 调用了构造方法
使用Class的newinstance方法 调用了构造方法
使用Constructor类的newInstance方法 调用了构造方法
使用clone方法 克隆一个对象(浅克隆,深克隆)
使用反序列化 ?

JVM垃圾回收机制

在Java中,程序员不需要显示的去释放一个对象的内存,而是由虚拟机自动执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到回收的集合中,进行回收

垃圾回收算法

引用计数法
在这里插入图片描述
可以看到循环引用下JVM回收不了对象。
可达性分析法 GC Root
(1)虚拟机栈中引用的对象(栈帧中的本地变量表);
(2)方法区中类静态属性引用的对象;
(3)方法区中常量引用的对象;
(4)本地栈中引用的对象。在这里插入图片描述

回收策略/算法

标记-清除
先标记,再清除,清除之后会有很多空间碎片,如果后面需要分配大的对象,就会导致没有连续的内存可供使用
标记-整理
先标记,然后清除整理,就解决了空间碎片的问题。然后又出现了一个新的问题,每次都得移动对象。
复制算法
把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活的对象复制到另一个区域中,最后将当前使用的区域的可回收的对象进行回收。

分代收集
根据对象的存活周期将内存划分为几块。一般包括年轻代。老年代和永久代,

垃圾收集器

Java中根据不同的场景提供了三个种类收集器,分别是串行、吞吐量优先和用户响应时间优先
在这里插入图片描述

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常用参数举例
内存设置

参数 含义
-Xms1024m 初始堆大小
-Xmx1024m 最大堆大小
-Xmn500m 新生代大小
-Xss1024K 单个线程栈
-XX:PermSize=200m 永久代 before Java8
-XX:MaxPermSize=300m 永久代 berfore Java8
-XX:MetaSpaceSize 元数据 after 8
-XX:MaxMetaSpaceSize 元数据 after 8
-XX:NewRatio=4 Olden区与Young区比例 4:1
-XX:SurvivorRatio=8 Eden区与S区比例 8:1:1
-XX:LargePageSizeInBytes 内存页大小 JVM优化之调成大内存分页

GC设置

新生代(别名) 老年代 JVM参数
Serial (DefNew) Serial Old(PSOldGen) -XX:+UseSerialGC
Parallel Scavenge (PSYoungGen) Serial Old(PSOldGen) -XX:+UseParallelGC
Parallel Scavenge (PSYoungGen) Parallel Old (ParOldGen) -XX:+UseParallelOldGC
ParNew (ParNew) Serial Old(PSOldGen) -XX:-UseParNewGC
ParNew (ParNew) CMS+Serial Old(PSOldGen) -XX:+UseConcMarkSweepGC
G1 G1 -XX:+UseG1GC

调试参数

参数 含义 说明
-XX:+PrintGCDetails GC日志
-XX:+PrintGCApplicationStoppedTime GC停顿时间
-verbose:gc/class/jni 查看gc、类加载、本地方法调用
-XX:+PrintHeapAtGC 打印GC触发时的堆栈
-Xloggc:log/gc.log 输出gc log
-XX:+HeapDumpOnOutOfMemoryError 内存溢出时产生堆dump
-XX:+HeapDumpOnCtrlBreak Crtl+Break产生dump

作者:qq_45366515

相关推荐

【SAP】用户维护-加解锁和密码初始化

【SAP】用户维护-加解锁和密码初始化

在这里插入图片描述

讲给女朋友听的java多线程(2万字深入理解多线程,有实例代码辅助理解)

当阿里面试官问我:Java创建线程有几种方式?我就知道问题没那么简单

在这里插入图片描述

【每日爬虫】:利用线程池爬取2万张装修效果图