JVM 详解

jvm是什么

使用过 Java 的同学都知道,Java 代码可以在服务端(Linux 系统)运行,也可以在 Windows 系统运行。我们编写的一份java代码可以在不同的系统中运行。与其他语言不同,Java 语言并不直接将代码编译成与系统有关的机器码,而是编译成一种特定的语言规范,这种语言规范我们称之为字节码。无论 Java 程序要在 Windows 系统,还是 Mac OSX 系统,抑或是 Linux 系统,它首先都得编译成字节码文件,之后才能运行。但即使编译成字节码文件了,各个系统还是无法明白字节码文件的内容,这时候就需要 Java 虚拟机的帮助了。Java 虚拟机会解析字节码文件的内容,并将其翻译为各操作系统能理解的机器码。JVM是java实现“一次编写,到处运行”的关键。

jvm的角色
java代码从开发到运行

下面的图讲述了从java源代码到机器码的过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xH7lpGOj-1627907292562)(http://image.huawei.com/tiny-lts/v1/images/0d21ae3bb77c019df059cb46db1b6b65_574x645.png@900-0-90-f.png)]

源码到字节码

java compiler(javac)将java源代码编译为字节码,.java文件在这个阶段被编译为.class文件,如果用纯文本编辑器打开.class文件会发现其实这个文件是一连串的16进制数据流。javac的处理过程分为四个阶段
1、词法、语法分析。编译器对源码字符进行扫描,生成抽象语法树,这个阶段java编译器会搞懂代码到底想要做什么;
2、填充符号表。java代码中类似会互相引用的,在编译阶段无法确定具体的地址,要使用符号来替代,在这个节点就是做类似的事情,等到类加载阶段,再讲符号替换成具体的内存地址;
3、注解处理,对注解进行解析,根据注解的作用将其还原成具体的指令集;
4、分析与字节码生成,javac编译器根据前面几个阶段分析的结果生成字节码,并输出位class文件。
javac一般称为前端编译器,因其发生在整个编译的前期。

字节码到机器码

当源代码转化为字节码之后,运行程序有两种选择:
1、java解释器执行字节码,启动速度快,运行速度慢,运行是需要将字节码转化为机器码;
2、JIT编译器转化为本地机器代码,启动速度慢,运行速度快,jit首次编译后将字节码对应的机器码保存下来,下次可以直接使用。
在实际中,为了运行速度及效率,我们通常采用两者相结合的方式进行java代码的编译执行;HotSpot 虚拟机内置了两个即时编译器,分别称为 Client Compiler(C1编译器) 和Server Compiler (C2编译器)。C1 编译模式会将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。而 C2 编译模式,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
在 JVM 中有三个非常重要的编译器,它们分别是:前端编译器、JIT 编译器、AOT 编译器。前端编译器,最常见的就是我们的 javac 编译器,其将 Java 源代码编译为 Java 字节码文件。JIT 即时编译器,最常见的是 HotSpot 虚拟机中的 Client Compiler 和 Server Compiler,其将 Java 字节码编译为本地机器代码。而 AOT 编译器则能将源代码直接编译为本地机器码。

jvm内存结构

Java 虚拟机的内存结构可以分为公有和私有两部分。公有指的是所有线程都共享的部分,指的是 Java 堆、方法区、常量池。私有指的是每个线程的私有数据,包括:PC寄存器、Java 虚拟机栈、本地方法栈。

共有部分

Java 堆指的是从 JVM 划分出来的一块区域,这块区域专门用于 Java 实例对象的内存分配,几乎所有实例对象都在会这里进行内存的分配。
方法区指的是存储 Java 类字节码数据的一块区域,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造方法等。可以看到常量池其实是存放在方法区中的。方法区在不同版本的虚拟机有不同的表现形式,例如在 1.7 版本的 HotSpot 虚拟机中,方法区被称为永久代(Permanent Space),而在 JDK 1.8 中则被称之为 MetaSpace。
Java 堆根据对象存活时间的不同,Java 堆还被分为年轻代、老年代两个区域,年轻代还被进一步划分为 Eden 区、From Survivor 0、To Survivor 1 区。当有对象需要分配时,一个对象永远优先被分配在年轻代的 Eden 区,等到 Eden 区域内存不够时,Java 虚拟机会启动垃圾回收。此时 Eden 区中没有被引用的对象的内存就会被回收,而一些存活时间较长的对象则会进入到老年代。在 JVM 中有一个名为 -XX:MaxTenuringThreshold 的参数专门用来设置晋升到老年代所需要经历的 GC 次数,即在年轻代的对象经过了指定次数的 GC 后,将在下次 GC 时进入老年代。Eden:from :to = 8:1:1。根据 IBM 公司对对象存活时间的统计,他们发现 80% 的对象存活时间都很短。于是他们将 Eden 区设置为年轻代的 80%,这样可以减少内存空间的浪费,提高内存空间利用率。

私有部分

Java 堆以及方法区的数据是共享的,但是有一些部分则是线程私有的。线程私有部分可以分为:PC 寄存器、Java 虚拟机栈、本地方法栈三大部分。

java类加载机制
加载

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口,把代码数据加载到内存中。

验证

当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。JVM规范校验。JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。代码逻辑校验。JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。

准备

JVM 便会开始为类变量分配内存并初始化,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。被 final 修饰的类变量在准备阶段就会被赋予想要的值。

解析

当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

初始化

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化。

使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。

卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

JVM垃圾回收机制

现今的 Java 虚拟机判断垃圾对象使用的是:GC Root Tracing 算法。其大概的过程是这样:从 GC Root 出发,所有可达的对象都是存活的对象,而所有不可达的对象都是垃圾。
垃圾回收算法简单地说有三种算法:标记清除算法、复制算法、标记压缩算法。标记清除算法虽然会产生内存碎片,但是不需要移动太多对象,比较适合在存活对象比较多的情况。而复制算法虽然需要将内存空间折半,并且需要移动存活对象,但是其清理后不会有空间碎片,比较适合存活对象比较少的情况。而标记压缩算法,则是标记清除算法的优化版,减少了空间碎片。
实际的垃圾回收算法中采用了分代算法。JVM 内存的不同内存区域,采用不同的垃圾回收算法。例如对于存活对象少的新生代区域,比较适合采用复制算法。这样只需要复制少量对象,便可完成垃圾回收,并且还不会有内存碎片。而对于老年代这种存活对象多的区域,比较适合采用标记压缩算法或标记清除算法,这样不需要移动太多的内存对象。在HotSpot虚拟机中,JVM 将内存划分为一块较大的Eden空间和两块较小的Survivor空间,其大小占比是8:1:1。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Eden空间。

JVM垃圾回收类型
Minor GC

从年轻代空间回收内存被称为 Minor GC,有时候也称之为 Young GC。

  • 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以 Eden 区越小,越频繁执行 Minor GC。
  • 当年轻代中的 Eden 区分配满的时候,年轻代中的部分对象会晋升到老年代,所以 Minor GC 后老年代的占用量通常会有所升高
  • 质疑常规的认知,所有的 Minor GC 都会触发 Stop-The-World,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的,因为大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。
Major GC

从老年代空间回收内存被称为 Major GC,有时候也称之为 Old GC。Minor GC 作用于年轻代,Major GC 作用于老年代。 分配对象内存时发现内存不够,触发 Minor GC。Minor GC 会将对象移到老年代中,如果此时老年代空间不够,那么触发 Major GC。因此才会说,许多 Major GC 是由 Minor GC 引起的。

Full GC

Full GC 是清理整个堆空间 —— 包括年轻代、老年代和永久代(如果有的话)。因此 Full GC 可以说是 Minor GC 和 Major GC 的结合。
当准备要触发一次 Minor GC 时,如果发现年轻代的剩余空间比以往晋升的空间小,则不会触发 Minor GC 而是转为触发 Full GC。因为JVM此时认为:之前这么大空间的时候已经发生对象晋升了,那现在剩余空间更小了,那么很大概率上也会发生对象晋升。既然如此,那么我就直接帮你把事情给做了吧,直接来一次 Full GC,整理一下老年代和年轻代的空间。
另外,即在永久代分配空间但已经没有足够空间时,也会触发 Full GC。

Stop-The-World

是指在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程回收垃圾的时间间隔。
可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。
① 分析工作必须在一一个能确保一 致性的快照中进行
② 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
③ 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

堆栈参数配置
  • -Xms 设置堆的初始空间大小
  • -Xmx 设置堆的最大空间大小
  • 老年代的大小就等于堆大小减去年轻代大小
  • -XX:SurvivorRatio = eden/from = eden/to,设置年轻代比例关系
  • -XX:PermSize 设置永久代初始大小
  • -XX:MaxPermSize 设置永久代最大大小
  • JDK1.8 之时,永久代被移除,取而代之的是元空间(Metaspace), -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize分别代表元空间发生 GC 的初始阈值和最大大小。元空间的最大大小与物理内存一致
  • -Xss2m 设置栈空间大小,栈空间太小,会导致 StackOverFlow 异常
  • -XX:MaxDirectMemorySize 直接内存,独立于 JVM 的堆内存,设置最大直接内存。如果不设置,默认为最大堆空间,即 -Xmx
  • -XX:+PrintVMOptions 程序运行时,打印虚拟机接受到的命令行显式参数
  • -XX:+PrintCommandLineFlags 打印传递给虚拟机的显式和隐式参数。
  • -XX:+PrintFlagsFinal 打印所有的系统参数的值
  • -verbose:class 跟踪类的加载和卸载
  • -XX:+TraceClassLoading 跟踪类的加载
  • -XX:+TraceClassUnloading 跟踪类的卸载
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值