JVM

徘徊边缘 提交于 2019-11-29 05:01:23

一.JVM简介

  java虚拟机就是可运行 java代码的假象计算机,他有一套字节码指令集,一组寄存器(存储指令,数据和地址的一个部件),一个栈,一个垃圾回收(GC),堆和一个存储方法域。
他是运行在操作系统上的,和硬件没有直接的交互。

二.三种JVM

  Sun hotspot
  BEA JRockit
  IBM J9 JVM
  在JDK及以前都是用的sun公司的hotspot 但由于sun和bea都被甲骨文收购了,所以jdk1.8采用的是两个JVM中的精华集成了1.8JVM


四.JVM的运行过程

  Java之所以能跨平台(一次编译,到处执行),就是因为根据操作平台的不同,java提供了对应的虚拟机进行解释,转换为不同平台的机器码,最终得到执行。
个普通的java程序它的执行流程

 

 

 https://www.cnblogs.com/leefreeman/p/7344460.html

JVM 加载 class 文件的原理机制

 

 

 JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,后面我们会讲到类的加载机制。

五.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 CompilerServer 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中存在即时编译器编译热点代码,所以也被称为半解释半执行的编程语言)

即时编译(Jit)

JVM中还存在着即时编译器优化代码执行,HotSpot中的即时编译器分为client模式与server模式,又称为c1、c2编译器(jdk1.7默认server模式),他会检测代码中的热点代码(即多次调用的方法或循环的代码块),这些代码如果每次都通过解释器解释执行无疑大大降低了运行效率,因此Jit编译器将他们编译成本地代码,则下次调用时就不需要解释器再次解释执行。

Jit编译器检测热点代码:

1、方法计数器:记录方法调用的次数 

2、回边计数器:记录代码块循环次数 

当计数器数值大于默认阈值或指定阈值时,方法或代码块会被编译成本地代码。 

Java代码编译过程图: 


五.JVM内存结构(Java运行时数据区)

首先我们要知道java内存结构和内存模型说的不是一个东东!切记!

内存模型:JMM(java memory model) https://www.jianshu.com/p/76959115d486

1.大致分类:
  如果从线程的角度来讲,他可以分为线程私有(虚拟机栈,本地方法栈,程序计数器)的线程共享(堆,方法区)的。
  还可以从另一个角度分为 指令数据
  数据在java虚拟机划分,是因为根据这些区域所负责功能不同而分配对应的区域
2.一一解读:
方法区:类的信息,常量,静态变量
:夭折对象和老不死对象都在JAVA堆,而不灭对象在方法区。

程序计数器:用来标记线程执行到的位置。()


栈是先进后出的(手枪压子弹)
虚拟机栈保存Java方法
     有N个栈帧--结构:局部变量表 :参数和方法体 int a=5;int b = 10;
               操作数栈         int r =5*1;将5和1和运算符*入栈(操作数栈)进去。方法的执行就是弹栈的过程。弹到cpu,交给cpu执行。(cpu才是负责计算的东西,而内存只是个高速缓冲区而已)
              动态链接方法: 方法地址。(return的地址)
              返回地址

本地方法栈:保存Native方法
    也是N个栈帧和虚拟机栈大同
    (Thread类有个start方法,他调度了一个start0方法,我们发现start0方法就是用Native修饰的方法,其实start0让底层用c++代码调度了run方法,只不过给程序员屏蔽了,naive在哪呢,其实就在底层操作系统中,dll动态链接库,用c和c++写的)

一张图搞定所有!

 

 

 

 

 

堆的内存划分:

分代回收机制(主要真对堆来说):

六.垃圾回收

1.四种垃圾收集器
  Serial收集器:单线程
  ParNew:多线程
  CMS:保证最短的停顿时间
  GI:JDK9包括之后默认的垃圾收集器


2.垃圾回收算法
  青年代:复制算法 占内存
  老年代:标记清除 将回收的对象标记,然后清除,他还会将断续的数据组合到内存一侧,这样提高了性能


七.类加载机制

七.类加载机制

1.类的生命周期

  1.加载   将.class文件从磁盘中读取到内存

  2. 连接 (验证:验证字节码的正确性 准备:给类的静态变量分配到内存,并赋予默认值  解析:类装载器装入所引用的其他所有类)

  3.初始化  为类的静态变量赋值正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的默认值,此处赋予的才是程序编写者为变量分配真正的初始值,执行静态代码块。

  4.使用

  5.卸载

  

 

2.类加载器的种类

启动类加载器(Bootstrap ClassLoader)

负责加载JRE的核心类库,如JRE目标下的rt.jar,charsets.jar等。

扩展类加载器(Extension ClassLoader)

负责加载jre扩展目录ext中的jar

系统类加载器(Application ClassLoader)

负责加载classpath路径下的类包

用户自定义加载器(User ClassLoader)

负责加载用户自定义路径下的类包

类加载器的关系:向上委托的关系

3.类加载机制

 
全盘负责委托机制

当一个ClassLoader加载一个类的时候,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入。

例如,系统类加载器AppClassLoader加载入口类(含有main方法的类)时,会把main方法所依赖的类及引用的类也载入,依此类推。“全盘负责”机制也可称为当前类加载器负责机制。显然,入口类所依赖的类及引用的类的当前类加载器就是入口类的类加载器。

说白了,没有推诿责任的嫌疑,寻址链条不会再父的,或者父亲的父亲的类加载器。

双亲委派机制(模型)parent delegate model

指先委托父加载器寻找目标类,在找不到的情况下在自己的路径中查找并载入目标类

https://blog.csdn.net/u010312474/article/details/91046318

双亲委派模式的优点

沙箱安全机制:比如自己写的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提供接口,由厂商去实现

八.JVM性能调优

1.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

https://blog.csdn.net/u012550080/article/details/81605189

2.调优

JVM调优主要就是调整下面两个指标

停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。-XX:MaxGCPauseMillis

吞吐量:垃圾收集的时间和总时间的占比:1/(1+n),吞吐量为1-1/(1+n)。-XX:GCTimeRatio=n

 

3..GC调优步骤

1.打印GC日志

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/log/gc.log

Tomcat可以直接加载JAVA_OPTS变量

2.分析日志得到关键性指标

3.分析GC原因,优化JVM参数

 

Parallel Scavenge收集器(默认)

分析日志:

第一次调优,设置Metaspace大小:增大元空间大小 ()-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M

第二次调优,增大年轻代动态扩容增量(默认是20%),可以减少YGC: -XX:YoungGenerationSizeIncrement=30

比较以下几次调优效果:

吞吐量 最大停顿 平均停顿 YGC FGC

99.98% 70.0 ms 1.13 ms 237 3

96.711% 60.0 ms 0.958 ms 238 2

 

 

https://blog.csdn.net/qq_28165595/article/details/82431226

https://www.cnblogs.com/zjxiang/p/9218211.html

 

使用G1收集器收集日志

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+UseG1GC -Xloggc:./log/gc-g1.log

 

https://blog.csdn.net/u013812939/article/details/48782343

也可设置内存溢出的时候自动导出dump文件(内存很大的时候,可能会导不出来)

  1. -XX:+HeapDumpOnOutOfMemoryError
  1. -XX:HeapDumpPath=输出路径

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+HeapDumpOnOutofMemoryError -XX:HeapDumpPath=d:\oomdump.dump

https://gceasy.io

4.Cmder右键菜单设置

Cmder.exe /REGISTER ALL

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!