JVM虚拟机原理


1 序言

Java作为一种热门开发语言,在行业知名度也可谓是“家喻户晓”。在使用的道友,也是不计其数。所以,应该都听说过一句经典语句:Write once,run anywhere。而为什么java能达到如此强大的存在呢,这离不开我们今天要复习的重点-java虚拟机(JVM)。


2 JVM介绍

2.1 JVM是什么

JVM就是Java虚拟机(Java virtual machine)。JVM是JRE的一部分,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。

2.2 JVM工作流程

咱们先看两张解析图:
解析图

咱们平时编写java程序后,大体工作流程就如上图。下面我们再进一步详解,请看下图:
流程图
简单解释一下上面这个流程图:

  1. 程序运行时,java文件通过java编译器转译成class文件
  2. class文件通过类装载器以及java类库,装载到JVM中
  3. JVM通过解释器,即时编译器等将装载进来的class文件进行编译操作。如内存分配,运行处理等
  4. 最后JVM将相应操作与操作系统、硬件交互

2.3 JVM内部结构

下面说说JVM虚拟机这块的内部结构,从图入手:

内部结构图
JVM内部结构分为三部分:类加载器(加载.class文件),执行引擎(执行字节码或执行本地方法),数据区(包含PC寄存器,栈,堆,方法区以及本地方法栈)。 下面我们对前两者进行一个介绍,待会着重讲讲数据区。

2.3.1 类加载器

类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

通过流程图看看类加载器做了哪些事:
类加载器

①Bootstrap ClassLoader
负责加载$ JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
②Extension ClassLoader
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
③App ClassLoader
负责记载classpath中指定的jar包及目录中class
④Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
更具体细节可参考:https://blog.csdn.net/m0_38075425/article/details/81627349

2.3.2 执行引擎

执行引擎是 Java 虚拟机最核心的组成部分之一。“虚拟机” 是一个相对于 “物理机” 的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行哪些不被硬件直接支持的指令集格式。

所谓的「虚拟机字节码执行引擎」其实就是 JVM 根据 Class 文件中给出的字节码指令,基于栈解释器的一种执行机制。通俗点来说,也就是 JVM 解析字节码指令,输出运行结果的一个过程。

细节参考:http://www.imooc.com/article/291225

2.4 JVM的运行时数据区

先通过两张结构图,了解一下不同版本下的运行时数据区的结构!

1.8版本之后的结构图如下:

结构图

1.8版本之前的结构图如下:

结构图
JVM中,运行时数据区具体是什么?带着这个疑问,咱们通过下面这张图详细了解一下:
数据区
看似复杂,其实咱们大致先分为几个部分来理解。上图可分为两大类型:共享区域非共享区域 。图中左侧都是所有线程共享的区域,右侧为每个线程私有的区域。

2.4.1 共享区域

堆(heap)

所有类的实例就放在这个区域,为所有线程共享。可以想象你的一个系统会产生很多实例,因此Java堆的空间也是最大的。如果Java堆空间不足了,程序会抛出OutOfMemoryError异常。

方法区

各个线程共享的区域,存放类信息、常量、静态变量。

2.4.2 非共享区域

程序计数器

指向当前线程正在执行的字节码的地址 和行号(指令都是在cpu上面运行的,分配到时间片才去运行的,当多个线程的时候其他线程会被挂起,程序计数器就是记录被挂起之前的字节码执行到哪行 地址等,等到重新分配到了时间片然后再继续执行)。它的作用就是控制程序指令的执行顺序

虚拟机栈

存储当前线程运行方法所需要的数据,指令,返回的地址

每个线程创建的同时会创建一个JVM栈,JVM栈中每个栈帧存放的为当前线程中局部基本类型的变量、部分的返回结果,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。
而每个栈帧中,又包含如上图中的:局部变量表,操作数栈,动态链接,方法出口,具体作用如图所解释。

本地方法栈
  • 本地方法栈是用来存储本地方法相关的数据。本地方法就是带有native标识符修饰的方法;
  • native修饰符修饰的方法并不提供方法体,但因为其实现体是由非java代码在在外部实现的,因此不能与abstract连用;
  • 存在的意义:不方便用java语言写的代码,使用更为专业的语言写更合适;甚至有些JVM的实现就是用c编写的,所以只能使用c来写

2.4.3 JMM(java内存模型)

通过一张图来认识JMM的内部结构:
JMM内部结构

  • JMM主要分为新生代 ( Young ) 与老年代 ( Old ) ,二者比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小,老年代 ( Old ) = 2/3 的堆空间大小。
  • 其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to以示区分。 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
  • JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。新生代是 GC 收集垃圾的频繁区域。
  • 当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳 ( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
  • From Survivor区域与To Survivor区域是交替切换空间,在同一时间内两者中只有一个不为空。

永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。 我们知道在HotSpot虚拟机中存在三种垃圾回收现象,minor GC、major GC和full GC。对新生代进行垃圾回收叫做minor GC,对老年代进行垃圾回收叫做major GC,同时对新生代、老年代和永久代进行垃圾回收叫做full GC。 许多major GC是由minor GC触发的,所以很难将这两种垃圾回收区分开。major GC和full GC通常是等价的,收集整个GC堆。

在1.8之后已经取消了永久代,改为元空间,类的元信息被存储在元空间中。 元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。这项改造也是有必要的,永久代的调优是很困难的,虽然可以设置永久代的大小,但是很难确定一个合适的大小,因为其中的影响因素很多,比如类数量的多少、常量数量的多少等。永久代中的元数据的位置也会随着一次full GC发生移动,比较消耗虚拟机性能。同时,HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。将元数据从永久代剥离出来,不仅实现了对元空间的无缝管理,还可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。

3.JVM的GC


上面简单提到过,分别有三种回收现象:minor GC、major GC和full GC。

3.1 如何确定某个对象(垃圾)是可回收

3.1.1 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。

这种方式的问题是无法解决循环引用的问题,当两个对象循环引用时,就算把两个对象都设置为null,因为他们的引用计数都不为0,这就会使他们永远不会被清除。

3.1.2 根搜索算法(可达性分析/可达算法)

为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

比较常见的将对象视为可回收对象的原因:

  • 显式地将对象的唯一强引用指向新的对象。
  • 显式地将对象的唯一强引用赋值为Null。
  • 局部引用所指向的对象(如,方法内对象)。
  • 只有弱引用与其关联的对象。

3.2 几种典型的垃圾回收算法

3.2.1 标记-清除算法(Mark-Sweep)

标记清楚算法

最基础的垃圾回收算法,分为“标注”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
标记过程:为了能够区分对象是live的,可以为每个对象添加一个marked字段,该字段在对象创建的时候,默认值是false。
清除过程:去遍历堆中所有对象,并找出未被mark的对象,进行回收。与此同时,那些被mark过的对象的marked字段的值会被重新设置为false,以便下次的垃圾回收。
缺点:效率低,空间问题(产生大量不连续的内存碎片),后续可能发生大对象不能找到可利用空间的问题。

3.2.2 复制算法(Copying)

复制算法

为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为大小相等的两块,每次只使用其中一块。当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存空间一次清理掉。这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。

3.2.3 标记-整理算法

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。具体流程见下图:
标记-整理算法

3.2.4 分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

分代收集算法

4 JVM参数调优

  • -Xms20M 表示设置堆容量的最小值为20M,必须以M为单位
  • -Xmx20M 表示设置堆容量的最大值为20M,必须以M为单位。将-Xmx和-Xms设置为一样可以避免堆自动扩展。大的项目-Xmx和-Xms一般都要设置到10G、20G甚至还要高
  • -verbose:gc 表示输出虚拟机中GC的详细情况
  • -Xss128k 表示可以设置虚拟机栈的大小为128k
  • -Xoss128k 表示设置本地方法栈的大小为128k。不过HotSpot并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说这个参数是无效的
  • -XX:PermSize=10M 表示JVM初始分配的永久代的容量,必须以M为单位
  • -XX:MaxPermSize=10M 表示JVM允许分配的永久代的最大容量,必须以M为单位,大部分情况下这个参数默认为64M
  • -Xnoclassgc 表示关闭JVM对类的垃圾回收
  • -XX:+TraceClassLoading 表示查看类的加载信息
  • -XX:+TraceClassUnLoading 表示查看类的卸载信息
  • -XX:NewRatio=4 表示设置年轻代:老年代的大小比值为1:4,这意味着年轻代占整个堆的1/5
  • -XX:SurvivorRatio=8 表示设置2个Survivor区:1个Eden区的大小比值为2:8,这意味着Survivor区占整个年轻代的1/5,这个参数默认为8
  • -Xmn20M 表示设置年轻代的大小为20M
  • -XX:+HeapDumpOnOutOfMemoryError 表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照
  • -XX:+PrintGCDetails 表示在控制台上打印出GC具体细节
  • -XX:+PrintGC 表示在控制台上打印出GC信息
  • -XX:MaxTenuringThreshold=1 表示对象年龄大于1,自动进入老年代

5 总结

java虚拟机的内容,本章就到此告一段落。后续有时间,会相继补充未完善的地方。但看完本章后,相信大家也能对JVM有了一定的认知与理解,不过,路漫漫,其修远兮!继续加油吧!

本博客皆为学习、分享、探讨为本,欢迎各位朋友评论、点赞、收藏、关注,一起加油!
关注

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值