开智小站-面试强化系列(六)深入剖析JVM
JVM是Java的基石,不知道你信不信,反正我是信了
1. JIT和解析器
JIT:批量字节码的编译,先编译,再执行
解析器:逐行解析,边解析,边执行
Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。
运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。
JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。在Java平台的结构中, 可以看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件交互的关键。
在Java编程语言和环境中,即时编译器(JIT compiler,just-in-timecompiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器(processor)的指令的程序
早在Java1.0版本的时候,Sun公司发布了一款名为Sun Classic VM的Java虚拟机,它同时也是世界上第一款商用Java虚拟机,在当时这款虚拟机内部只提供解释器,用今天的眼光来看待必然是效率低下的,因为如果Java虚拟机只能够在运行时对代码采用逐行解释执行,程序的运行性能可想而知。但是如今的HotSpot VM中不仅内置有解释器,还内置有先进的JIT(Just In Time Compiler)编译器,在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短。在此大家需要注意,无论是采用解释器进行解释执行,还是采用即时编译器进行编译执行,最终字节码都需要被转换为对应平台的本地机器指令。或许有些开发人员会感觉到诧异,既然HotSpot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRockit VM内部就不包含解释器,字节码全部都依靠即时编译器编译后执行,尽管程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。
既然HotSpot VM中采用了即时编译器,那么这就意味着将字节码编译为本地机器指令是一件运行时任务。在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
-client:指定Java虚拟机运行在Client模式下,并使用C1编译器;
-server:指定Java虚拟机运行在Server模式下,并使用C2编译器。
除了可以显式指定Java虚拟机在运行时到底使用哪一种即时编译器外,默认情况下HotSpot VM则会根据操作系统版本与物理机器的硬件性能自动选择运行在哪一种模式下,以及采用哪一种即时编译器。简单来说,C1编译器会对字节码进行简单和可靠的优化,以达到更快的编译速度;而C2编译器会启动一些编译耗时更长的优化,以获取更好的编译质量。不过在Java7版本之后,一旦开发人员在程序中显式指定命令“-server”时,缺省将会开启分层编译(Tiered Compilation)策略,由C1编译器和C2编译器相互协作共同来执行编译任务。不过在早期版本中,开发人员则只能够通过命令“-XX:+TieredCompilation”手动开启分层编译策略。
之前笔者曾经提及过,缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:
-Xint:完全采用解释器模式执行程序;
-Xcomp:完全采用即时编译器模式执行程序;
-Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
在此大家需要注意,如果Java虚拟机在运行时完全采用解释器执行,那么即时编译器将会停止所有的工作,字节码将完全依靠解释器逐行解释执行。反之如果Java虚拟机在运行时完全采用即时编译器执行,但解释器仍然会在即时编译器无法进行的特殊情况下介入执行,以确保程序能够最终顺利执行。
由于即时编译器将本地机器指令的编译推迟到了运行时,自此Java程序的运行性能已经达到了可以和C/C++程序一较高下的地步。这主要是因为JIT编译器可以针对那些频繁被调用的“热点代码”做出深度优化,而静态编译器的代码优化则无法完全推断出运行时热点,因此通过JIT编译器编译的本地机器指令比直接生成的本地机器指令拥有更高的执行效率也就理所当然了。比如使用Python实现的PyPy执行器,比使用C实现的CPython解释器更加灵活,更重要的是,在程序的运行性能上进行比较,PyPy将近是CPython解释器执行效率的1至5倍,这就是对JIT技术魅力的一个有力证明。并且Java技术自身的诸多优势同样也是C/C++无法比拟的,所谓各有所长就是这个道理。在此大家需要注意,世界上永远没有最好的编程语言,只有最适用于具体应用场景的编程语言。
解释器
JVM可以加载字节码即.class文件,然后边翻译边执行,因而被称为解释型编程语言(但是解释的过程就是编译一条机器码执行一条,且JVM中存在即时编译器编译热点代码,所以也被称为半解释半执行的编程语言)
2、即时编译(Jit)
JVM中还存在着即时编译器优化代码执行,HotSpot中的即时编译器分为client模式与server模式,又称为c1、c2编译器(jdk1.7默认server模式),他会检测代码中的热点代码(即多次调用的方法或循环的代码块),这些代码如果每次都通过解释器解释执行无疑大大降低了运行效率,因此Jit编译器将他们编译成本地代码,则下次调用时就不需要解释器再次解释执行。
Jit编译器检测热点代码:
1、方法计数器:记录方法调用的次数 2、汇编计数器:记录代码块循环次数 当计数器数值大于默认阈值或指定阈值时,方法或代码块会被编译成本地代码。 Java代码编译过程图:
2.JVM基本概念
3.Java程序运行过程
我们都知道Java源文件,通过编译器,能够生产相应的.Class文件,也就是字节码文件,而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码 。
也就是如下:
① Java源文件—->编译器—->字节码文件
② 字节码文件—->JVM—->机器码
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
4.三种JVM
① Sun公司的HotSpot;
② BEA公司的JRockit;
③ IBM公司的J9 JVM;
在JDK1.7及其以前我们所使用的都是Sun公司的HotSpot,但由于Sun公司和BEA公司都被oracle收购,jdk1.8将采用Sun公司的HotSpot和BEA公司的JRockit两个JVM中精华形成jdk1.8的JVM。
事实上
java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的java虚拟机进行解释,最后再转换为不同平台的机器码,最终得到执行。这样我们是不是可以推演,如果要在mac系统上运行,是不是只需要安装mac java虚拟机就行了。那么了解了这个基本原理后,我们尝试去做更深的研究,一个普通的java程序它的执行流程到底是怎样的呢?例如我们写了一段这样的代码:
public class HelloWorld {
public static void main(String[] args) {
System.out.print("Hello world");
}
}
这段程序从编译到运行,最终打印出“Hello world”中间经过了哪些步骤呢?我们直接上图:
5. JVM内存结构
所有的Java开发人员可能会遇到这样的困惑?我该为堆内存设置多大空间呢?OutOfMemoryError的异常到底涉及到运行时数据的哪块区域?该怎么解决呢?其实如果你经常解决服务器性能问题,那么这些问题就会变的非常常见,了解JVM内存也是为了服务器出现性能问题的时候可以快速的了解那块的内存区域出现问题,以便于快速的解决生产故障。Java内存结构和Java内存模型不是一回事,Java内存结构和Java运行时数据区是一回事
我们先来看下运行时数据区的图解,如下:
再来看看堆区的详细图解,如下:
PermGen:从JDK1.8后,永久代放到了方法区
来张总图吧,这张图绘制了半小时:
6. 对象对应的内存区域
还记得前面介绍内存管理时,JVM对内存的划分吗?
我们将上面三种对象对应到内存区域当中,就是夭折对象和老不死对象都在JAVA堆,而不灭对象在方法区。
对于JAVA堆,JVM规范要求必须实现GC,因而对于夭折对象和老不死对象来说,死几乎是必然的结局,但也只是几乎,还是难免会有一些对象会一直存活到应用结束。然而JVM规范对方法区的GC并不做要求,所以假设一个JVM实现没有对方法区实现GC,那么不灭对象就是真的不灭对象了。
由于不灭对象的生命周期过长,因此分代搜集算法就是针对的JAVA堆而设计的,也就是针对夭折对象和老不死对象。
7. JAVA堆的对象回收(夭折对象和老不死对象)
有了以上分析,我们来看看分代搜集算法如何处理JAVA堆的内存回收的,也就是夭折对象与老不死对象的回收。
夭折对象:这类对象朝生夕灭,存活时间短,还记得复制算法的使用要求吗?那就是对象存活率不能太高,因此夭折对象是最适合使用复制算法的。
小疑问:50%内存的浪费怎么办?
答疑:因为夭折对象一般存活率较低,因此可以不使用50%的内存作为空闲,一般的,复制算法:使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的活动区间与另外80%中存活的对象转移到10%的空闲区间,接下来,将之前90%的内存全部释放,以此类推。
GC流程:
8. 常见的垃圾收集器
https://blog.csdn.net/qq_41701956/article/details/81664921
垃圾收集机制是Java的招牌能力,极大地提高了开发效率。垃圾收集器(GC,Garbage Collector)是和具体JVM实现紧密相关的,不同厂商(IBM、Oracle),不同版本的JVM,提供的选择也不同。接下来,我来谈谈最主流的Oracle JDK。收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。
说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。
Young generation:年轻代
Tenured generation:老年代
8.1Serial 收集器
这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。
8.2ParNew 收集器
可以认为是 Serial 收集器的多线程版本。
8.3Parallel Scavenge 收集器
这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。
CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。
作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。
8.4Serial Old 收集器
收集器的老年代版本,单线程,使用 标记 —— 整理。
8.5Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 —— 整理
8.6CMS 收集器
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。
运作步骤:
初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
并发标记(CMS concurrent mark):进行 GC Roots Tracing
重新标记(CMS remark):修正并发标记期间的变动部分
并发清除(CMS concurrent sweep)
缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除 算法带来的空间碎片
8.7G1 收集器
面向服务端的垃圾回收器。是Oracle JDK 9以后的默认GC选项
优点:并行与并发、分代收集、空间整合、可预测停顿。
运作步骤:
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)
9.垃圾回收算法
我认为总体上有个了解,理解相应的原理和优缺点,就已经足够了
9.1复制(Copying)算法
我前面讲到的新生代GC,基本都是基于复制算法,过程就如专栏上一讲所介绍的,将活着的对象复制到to区域,拷贝过程中将对象顺序放置,就可以避免内存碎片化。
这么做的代价是,既然要进行复制,既要提前预留内存空间,有一定的浪费;另外,对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,这个开销也不小,不管是内存占用或者时间开销。
9.2标记-清除(Mark-Sweep)算法
首先进行标记工作,标识出所有要回收的对象,然后进行清除。这么做除了标记、清除过程效率有限,另外就是不可避免的出现碎片化问题,这就导致其不适合特别大的堆;否则,一旦出现Full GC,暂停时间可能根本无法接受。
9.3标记-整理(Mark-Compact)
类似于标记-清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。
10.类的生命周期
10.1.加载
将.class文件从磁盘读取到内存
10.2.连接
10.2.1.验证
验证字节码的正确性
10.2.2 准备
给类的静态变量分配内存,并赋予默认值
10.2.3解析
类装载器装入类所引用的其他所有类
10.3.初始化
为类的静态变量赋值正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的默认值,此处赋予的才是程序编写者为变量分配的真正的初始值,执行静态代码块。
10.4.使用
10.5.卸载
11.类加载器的种类
启动类加载器(Bootstrap ClassLoader)
负责加载JRE的核心类库,如JRE目标下的rt.jar,charsets.jar等。
扩展类加载器(Extension ClassLoader)
负责加载jre扩展目录ext中的jar
系统类加载器(Application ClassLoader)
负责加载classpath路径下的类包
用户自定义加载器(User ClassLoader)
负责加载用户自定义路径下的类包
类加载器的关系:向上委托的关系
12.类加载机制
全盘负责委托机制
当一个ClassLoader加载一个类的时候,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入。
例如,系统类加载器AppClassLoader加载入口类(含有main方法的类)时,会把main方法所依赖的类及引用的类也载入,依此类推。“全盘负责”机制也可称为当前类加载器负责机制。显然,入口类所依赖的类及引用的类的当前类加载器就是入口类的类加载器。
双亲委派机制(模型)parent delegate model
指先委托父加载器寻找目标类,在找不到的情况下在自己的路径中查找并载入目标类
双亲委派模式的优点
沙箱安全机制:比如自己写的String.class类不会被加载,这样可以防止核心库被随意篡改
避免类的重复加载:当父ClassLoader已经加载了该类的时候,就不需要子ClassLoader再加载一次
如何打破双亲委派机制
Tomcat就打破了双亲委派机制,
SPI Service Provider interface 服务提供者接口
感觉自己的语言描述不是很精准,就不再自己创造了,在网上拷贝了一段描述,讲明了什么是SPI技术,为什么要用SPI,用SPI有什么好处。如下: SPI的全名为Service Provider Interface.大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java spi机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,java spi的具体约定为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader
Java提供接口,由厂商去实现
13. JVM性能调优监控命令
垃圾回收对堆空间做回收,栈空间内存随着线程的消亡而消亡,并不受jvm垃圾回收的管辖!
小白补充课:我得知道现在我的系统中运行着哪些java进程
jps:通过jps查询进程的id pid
jinfo:查看正在运行的java程序的扩展参数
查看JVM的参数
jinfo -flags 11740
查看java系统属性
jinfo -sysprops 11740
等同于System.getProperties()
Jstat
jstat命令可以查看对内存各部分的使用量,以及加载类的数量。命令格式:
jstat [-命令选项][Vmid][间隔时间/毫秒][查询次数]
类加载统计
Loaded:加载class的数量
Bytes:所占用空间大小
Unloaded:未加载 数量
Bytes:未加载占用空间
Time:时间
垃圾回收统计
堆内存统计
新生代垃圾回收统计
新生代内存统计
Jmap
可以用来查看内存信息
堆的对象统计
jmap -histo 11740 > hello.txt
14.调优
JVM调优主要就是调整下面两个指标
停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。-XX:MaxGCPauseMillis
吞吐量:垃圾收集的时间和总时间的占比:1/(1+n),吞吐量为1-1/(1+n)。-XX:GCTimeRatio=n
堆区调优:-Xmx: 堆内存最大值 物理内存的1/4
-Xms: 堆内存最小值 物理内存的1/64
14.1MAT(Memory Analyzer) 内存分析工具
1. 安装一个插件
你从Eclipse商店中安装
中间有两道confirm之后。提示你要重启Eclipse。重启后
检测是否安装成功
2. 安装一个插件,单独安装
3. 写一个程序。。。。。。让JVM爆掉。
对当前类,加上一个参数化设置
-Xmx20m -Xms10m -XX:+HeapDumpOnOutOfMemoryError
堆大小最大值20m,我写的程序是不是超过20m就可以了。
//我New一次 T1 ,就向JVM索要1MB的内存
byte[] bytes=new byte[1024*1024];
public static void main(String[] args) {
List<T1> list=new ArrayList<T1>();
for(int i=0;i<20;i++){
list.add(new T1());
}
}
Pidxxxxxxxxxx:默认生成到项目根目录:需要手动刷新一下,完成后再来点开查看。你只有安装了MAT插件,才能直接打开。
14.2GC调优步骤
package com.openmind.day06;
import java.util.ArrayList;
import java.util.List;
/**
* @Classname T3
* @Description 微冷的雨训练营 www.cnblogs.com/weilengdeyu
* @Date 2019/9/2 17:24
* @Created by Happy-微冷的雨
*/
public class T3 {
//01.静态内部类
static class OOMObject{
//字节数组 IO/socket
public byte[] placeholder=new byte[64*1024];//64k
//bit 和byte 1byte=8bit
//bit 二进制位
}
public static void fillHeap(int num) throws InterruptedException {
List<OOMObject> list=new ArrayList<OOMObject>();
for (int i=0;i<num;i++){
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws InterruptedException {
//Fill Heap
fillHeap(2000);
}
}
参数如下:
-XX:+PrintGCDetails -XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:c:/log/gc-2.log
https://gceasy.io
1.打印GC日志
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:c:/log/gc.log
Tomcat可以直接加载JAVA_OPTS变量
2.分析日志得到关键性指标
3.分析GC原因,优化JVM参数
Parallel Scavenge收集器(默认)
分析日志:
第一次调优,设置Metaspace大小:增大元空间大小 ()-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M
第二次调优,增大年轻代动态扩容增量(默认是20%),可以减少YGC:-XX:YoungGenerationSizeIncrement=30
合并调优
-XX:+PrintGCDetails -XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M -XX:YoungGenerationSizeIncrement=30 -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:c:/log/gc-2.log
比较以下几次调优效果:
吞吐量 最大停顿 平均停顿 YGC FGC
99.98% 70.0 ms 1.13 ms 237 3
96.711% 60.0 ms 0.958 ms 238 2
调整前,如下图:
调整后:
GC日志参数分析
14.3参数控制各区域内存大小
控制参数
· -Xms设置堆的最小空间大小。
· -Xmx设置堆的最大空间大小。
· -XX:NewSize设置新生代最小空间大小。
· -XX:MaxNewSize设置新生代最大空间大小。
· -XX:PermSize设置永久代最小空间大小。
· -XX:MaxPermSize设置永久代最大空间大小。
· -Xss设置每个线程的堆栈大小。
可以关注俺