JVM总结

作者: 萌萌哒的鸡蛋 | 来源:发表于2020-03-23 11:34 被阅读0次

    一.Java虚拟机

    1.JVM是Java的基石,JVM是JRE的一部分

    2.JRE是Java Runtime Enviroment是指Java的运行环境,是面向Java程序的使用者,一般JDK中包含JRE。

    3.JDK是面向开发人员使用的SDK。

    三种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代码源文件(.java文件)经过java的编译器编译成了java字节码文件(.class文件),然后被装入内存中,然后被java的解释器和jit(即时编译)编译成机器码,然后被运行。

            运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。

            早在Java1.0版本的时候,Sun公司发布了一款名为Sun Classic VM的Java虚拟机,它同时也是世界上第一款商用Java虚拟机,在当时这款虚拟机内部只提供解释器,用今天的眼光来看待必然是效率低下的,因为如果Java虚拟机只能够在运行时对代码采用逐行解释执行,程序的运行性能可想而知。但是如今的HotSpot VM中不仅内置有解释器,还内置有先进的JIT(Just In Time Compiler)编译器,在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短。在此大家需要注意,无论是采用解释器进行解释执行,还是采用即时编译器进行编译执行,最终字节码都需要被转换为对应平台的本地机器指令。或许有些开发人员会感觉到诧异,既然HotSpot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRockit VM内部就不包含解释器,字节码全部都依靠即时编译器编译后执行,尽管程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。

    1、解释器

    JVM可以加载字节码即.class文件,然后边翻译边执行,因而被称为解释型编程语言(但是解释的过程就是编译一条机器码执行一条,且JVM中存在即时编译器编译热点代码,所以也被称为半解释半执行的编程语言)

    2、即时编译(Jit)

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

    Jit编译器检测热点代码:

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

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

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

    java代码的编译过程
    编译成字节码后执行

    java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的java虚拟机进行解释,最后再转换为不同平台的机器码,最终得到执行。

    二、JVM的内存结构

    控制参数

    · -Xms设置堆的最小空间大小。

    · -Xmx设置堆的最大空间大小。

    · -XX:NewSize设置新生代最小空间大小。

    · -XX:MaxNewSize设置新生代最大空间大小。

    · -XX:PermSize设置永久代最小空间大小。

    · -XX:MaxPermSize设置永久代最大空间大小。

    · -Xss设置每个线程的堆栈大小。

    JVM的内存结构(Java的运行时数据区)有五个主要结构:

    线程独享:

    程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;

    Java 虚拟机栈(Java Virtual Machine Stacks):里面的栈帧存储着:用于存储局部变量表、操作数栈、动态链接、方法出口等信息;

    本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;

    线程共享:

    Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存,

    方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

    JVM内存结构

    1)堆:

    堆是占用内存最大的一块,储存所有的对象实例,它有着自己的

    复制算法:

    使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的活动区间与另外80%中存活的对象转移到10%的空闲区间,接下来,将之前90%的内存全部释放,以此类推。 


    堆结构

    年轻代:

    1:eden、servicorFrom 复制到 ServicorTo,年龄+1

    首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);

    2:清空 eden、servicorFrom

    然后,清空 Eden 和 ServicorFrom 中的对象;

    3:ServicorTo 和 ServicorFrom 互换

    最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom

    区。

    老年代

    放置长生命周期的对象,通常都是从Survivor区域拷贝过来的对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上;如果对象较大,JVM会试图直接分配在Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。

    永久代

    这部分就是早期Hotspot JVM的方法区实现方式了,储存Java类元数据、常量池、Intern字符串缓存,在JDK 8之后就不存在永久代这块儿了。

    2)本地方法栈

    结构图

    ①栈里面主要有很多歌线帧,每一个线帧中又有自己的:

    局部变量表、操作栈、动态链接和返回地址。

    ②若单个线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError(栈溢出错误)。

    OutOfMemoryError指的是当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时抛出的异常。

    JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前线程虚拟机栈的内存空间耗尽,典型如一个无结束条件的递归函数调用。

    ③java虚拟机栈也是线程私有的,虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。

    局部变量表:存放了编译期可知的各种基本数据类型、对象引用和returnAddress(指向了一条字节码指令的地址)。

    操作栈:存放了用于计算和操作的数值和符号,将他们放置在一个栈中(入栈),然后计算的时候将他们取出(弹栈),然后交给CPU计算。

    动态链接:

    返回地址:存储返回值。

    三、常见的垃圾收集器

    1.Serial 收集器

    这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。

    2.ParNew 收集器

    可以认为是 Serial 收集器的多线程版本。

    3.Parallel Scavenge 收集器

    这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。

    CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。

    作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。

    4.CMS 收集器

    CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。

    运作步骤:

    初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象

    并发标记(CMS concurrent mark):进行 GC Roots Tracing

    重新标记(CMS remark):修正并发标记期间的变动部分

    并发清除(CMS concurrent sweep)

    5.G1 收集器

    面向服务端的垃圾回收器。是Oracle JDK 9以后的默认GC选项

    优点:并行与并发、分代收集、空间整合、可预测停顿。

    运作步骤:

    初始标记(Initial Marking)

    并发标记(Concurrent Marking)

    最终标记(Final Marking)

    筛选回收(Live Data Counting and Evacuation)

    四、垃圾回收算法

    1.复制(Copying)算法

    我前面讲到的新生代GC,基本都是基于复制算法,过程就如专栏上一讲所介绍的,将活着的对象复制到to区域,拷贝过程中将对象顺序放置,就可以避免内存碎片化。

    这么做的代价是,既然要进行复制,既要提前预留内存空间,有一定的浪费;另外,对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,这个开销也不小,不管是内存占用或者时间开销。

    2.标记-清除(Mark-Sweep)算法

    首先进行标记工作,标识出所有要回收的对象,然后进行清除。这么做除了标记、清除过程效率有限,另外就是不可避免的出现碎片化问题,这就导致其不适合特别大的堆;否则,一旦出现Full GC,暂停时间可能根本无法接受。

    3.标记-整理(Mark-Compact)

    类似于标记-清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。

    五、JVM调优

    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)类加载机制

    1.全盘负责委托机制

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

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

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

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

    双亲委派模式的优点:

    沙箱安全机制:比如自己写的String.class类不会被加载,这样可以防止核心库被随意篡改

    避免类的重复加载:当父ClassLoader已经加载了该类的时候,就不需要子ClassLoader再加载一次

    如何打破双亲委派机制

    Tomcat就打破了双亲委派机制,SPI Service Provider interface 服务提供者接口

    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提供接口,由厂商去实现。

    4)JVM调优

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

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

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

    可调优策略:

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

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

    相关文章

      网友评论

          本文标题:JVM总结

          本文链接:https://www.haomeiwen.com/subject/lrkhyctx.html