首页 > 技术文章 > JVM学习1

Gao-yubo 2021-09-13 19:17 原文

一、类加载子系统

1.1类的加载过程

流程图

1.加载阶段

通过一个类的全限定名获取定义此类的二进制字节流

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

2.链接阶段

验证 Verify

目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。

主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

如果出现不合法的字节码文件,那么将会验证不通过

准备 Prepare

类变量分配内存并且设置该类变量的默认初始值,即零值。(final修饰的类变量在此阶段会显示初始化

解析 Resolve

将常量池内的符号引用转换为直接引用的过程。

事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。

3.初始化阶段

初始化阶段就是执行类构造器法()的过程。

  • 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

  • 也就是说,当我们代码中包含static变量的时候,就会有clinit方法

  • 构造器方法中指令按语句在源文件中出现的顺序执行。

  • 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。

  • 任何一个类在声明后都有生成一个构造器,默认是空参构造器

1.2类加载器的分类

JVM支持两种类型的类加载器

  • 引导类加载器(Bootstrap ClassLoader)c/c++编写的 :所有派生于抽象类ClassLoader的类加载器
  • 自定义类加载器(User-Defined ClassLoader)。

这里的四者之间是包含关系,不是上层和下层,也不是子系统的继承关系。

Java的核心类库都是使用引导类加载器进行加载的。

1.启动类加载器

由c/c++编写

加载java核心库,用于提供jvm自身需要的类

2.扩展类加载器

java语言编写

派生于ClassLoader类(抽象类)

加载jre/lib/ext子目录下的类库,如果用户创建的jar也在此目录,则会自动由扩展类加载器加载

3.系统类加载器

java语言编写

派生于ClassLoader类(抽象类)

负责加载环境变量classpath或系统属性

该类是程序默认的类加载器,java应用的类都是由她来完成加载

4.用户自定义类加载器

1.目的:

  • ​ 隔离加载类
  • ​ 修改类加载方式
  • ​ 扩展加载源
  • ​ 防止源码泄露

2.方式:

​ 一、继承ClassLoader类

​ jdk1.2之后不去覆盖loadClass()方法,而是建议把自定义类的加载逻辑写在findClass()方法中

​ 二、继承URLClassLoader类,避免编写findClass()方法

1.3双亲委派机制

1.原理:

  1. 如果类加载器收到了类加载请求,自己不会先去加载,而是把这个请求委托给父类的加载器去执行
  2. 如果父类还存在父类加载器,则继续委托,直到启动类加载器。
  3. 如果父类可以完成加载,则加载;如不可以完成加载子加载器则尝试自己加载

1.3.1沙箱安全机制

沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。

二、运行时数据区

内存是非常重要的系统资源,是硬盘和CPU的中间仓库和桥梁

堆空间和方法区(红色)是进程的,在线程间共享

灰色的是线程私有

线程

线程使程序里的运行单元。

在Hotspot JVM中,每个线程都与操作系统的本地线程直接映射

​ 当一个java线程准备好执行以后,此时一个操作系统的本地线程也同时创建

​ java线程执行终止后,本地线程也会回收。

​ 当本地线程初始化成功后,它就会调用java线程中的run()方法。

2.1程序计数器(PC寄存器)

1.特点

  • 访问速度最快
  • 用于存储制指定下一条指令的地址,有执行引擎读取下一条指令
  • 线程私有的

2.作用:

​ 因为CPU不停地切换在各个线程间,这时候切换回来后,需要知道接着从哪开始继续执行。

2.2虚拟机栈

每个线程创建时都会创建一个虚拟机栈,内部保存一个个的栈帧,一个栈帧对应一个java方法调用。

可以使用参数-Xss 选项来设置现成的最大栈空间

1.作用:

  • 保存方法的局部变量,部分结果,并参与方法的调用和返回。

2.特点:

  1. 速度仅次于程序计数器
  2. 每个方法执行,伴随着进栈(入栈、压栈)
  3. 存在OOM,不存在垃圾回收(GC)

2.3.1栈的存储单元(栈帧)

1.特点:

一个线程中,一个时间点上,只会有一个活动的栈帧(栈顶栈帧),称为当前栈帧,对应的是当前方法,对应当前类

如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈

2.java两种返回函数的方式:(都会导致栈帧被弹出)

  1. 正常函数返回,使用return指令
  2. 抛出未捕获的异常

3.内部结构:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址
  • 一些附加信息
3.1局部变量表

定义:

一个数字数组,存储方法参数和定义在方法体内的局部变量(包括各类基本数据类型,对象引用以及returenAdress类型)

特点:

  • 是线程私有,不存在数据安全问题

  • 容量大小是在编译期确定的


slot(槽):

  • 局部变量表的基本单位

  • 32位占一个槽(int,引用数据类型等),64占两个(double,long)

  • 如果当前帧是由构造方法或非静态方法创建的,那么该对象引用this,会放在index为0的slot处,其余参数按顺序排列。

  • slot会被重复利用

3.2操作数栈

特点:

  • 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的储存空间
  • 是JVM执行引擎的一个工作区,当方法开始执行时,栈帧被创建,随之操作数栈也被创建(为空)
  • 容量大小是在编译器确定
  • 如果方法带有返回值,那么返回值也会被压入当前栈帧的操作数栈
  • 另外,我们说java虚拟机的解释引擎是基于栈的执行引擎,其中的栈值得就是操作数栈
相关技术:

栈顶缓存技术:

​ 栈式架构的虚拟机所使用的零地址指令更加紧凑,即需要更多的入栈出栈。

​ 因此,JVM设计者提出了此技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读、写次数,从而提高执行引擎的效率

3.3动态链接

定义:

指向运行时常量池的方法引用(在使用方法区内指令时的引用)

作用:

​ 为了将符号引用转换为调用方法的直接引用

方法的调用(分派)
绑定

​ 是一个字段、方法或者类在符号引用被替换为直接引用的过程,仅发生一次。

​ 分为早期绑定和晚期绑定

静态链接:

​ 如果被调用的目标方法在编译期可知,且运行期保持不变。这种情况下将调用方法的符号引用转为直接引用的过程称为静态链接。---->对应早期绑定------->早期绑定------->非虚方法

动态链接:

​ 被调用的方法,在编译期无法被确定-------->晚期绑定------->虚方法

虚方法与非虚方法:
  • invokestatic:调用静态方法(调用的为非虚方法

  • invokespecial:调用构造方法,私有及父类的方法(调用为非虚方法

  • invokevirtual:调用所有虚方法(特殊!!除去 final修饰的非虚方法,也被此字节码指令修饰)

  • invokeinterface:调用所有接口方法

  • invokedynamic:为了实现动态类型语而做的一种改进(lambda表达式)

本质:
  1. 找到操作数栈顶第一个元素所执行的对象的实际类型。记做C

  2. 如果在C中找到与常量中描述符合、简单名称都符合的方法,则进行权限校验:

    1. 通过则返回这个方法的直接引用
    2. 不通过则返回 IllegalAccessError异常
  3. 在C中没找到方法则在C的各个父类中找,找到后也校验权限

  4. 始终没有找到合适的方法,说明是接口没有重写,则抛出AbstractMethodError异常

    为了减少寻找,设计了虚方法表 记录调用该方法的类

3.4方法返回地址

存放调用该方法的pc寄存器的值(表示该方法结束,将进入调用者的下一条指令了)----->正常完成出口

而方法异常退出,返回地址要通过异常表来确定,栈帧中不会保留这部分信息。-------->异常完成出口

3.5一些附加信息

栈帧中允许携带与java虚拟机实现相关的一些附加信息。例如:对程序调试支持的信息。

局部变量在内部产生,并在内部消亡,则不存在线程安全问题。

本地方法接口的理解

  • 使用native关键字修饰的方法就是本地方法。
  • 在定义此类方法时,并不提供实现体,因为其实现体是由非java语言在外面实现的

例如:

Object的getClass;

线程里设置线程优先级的方法(因为java线程对应操作系统本地线程,需要和底层硬件有联系,所以使用非java编写的)

2.3本地方法栈

存储本地方法的栈,和虚拟机栈相似

当线程调用一个本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界。和虚拟机拥有同样的权限

​ 可以通过本地方法接口来访问虚拟机内部的运行时数据区

​ 可以直接使用本地处理器的寄存器

并不是所有的JVM都支持本地方法。

在Hotspot JVm中,直接将本地方法栈和虚拟机栈合二为一。

三、堆

3.1堆的核心概述

  • 一个JVM实例只存在一个堆空间

  • 堆区在JVM启动时,就被创建,其空间大小也确定了,是JVM管理的最大一块内存空间(可以调节-Xms:起始空间;-Xmx:最大内存)

    • 默认情况:堆初始内存大小:电脑物理内存大小/64

    ​ 最大内存大小: 电脑物理内存大小/ 4

    ​ 建议初始和最大一样

  • 堆可以在物理内存空间不连续,但在逻辑上被认为是连续的。

  • 所有线程共享Java堆,在连可以划分线程私有的缓冲区(TLAB
    TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。

3.2堆空间大小

可以调节-Xms:起始空间;-Xmx:最大内存

查看设置的参数:

​ 方式一:jps / jstat -gc 进程id

​ 方式二:-XX:+PrintGCDetails

3.3年轻代与老年代

  • 年轻代可以分为Eden空间Survivor0空间和Survivor1空间(from区、to区)
  • 几乎所有的Java对象都在Eden区中被new出来的
  • 绝大部分的java对象的销毁都在新生代
    • IBM公司专门研究表明,新生代中80%的对象都是朝生夕死

设置大小

  • 配置新生代与老年代在堆内结构的占比:-XX:NewRatio=? 表示老年代占比?,新生代占比1(默认为2,老年代占2/3)
  • 配置新生代中Eden区和Survivor区的比例:-XX:SurivorRatio(默认为8,即8:1:1)
  • -Xmn:设置新生代空间大小(一般不设置)

3.4对象分配过程

1.过程:

  1. new的对象先放在伊甸园区,此区有大小限制

  2. 伊甸园区满时,程序又需要创建对象,此时JVM的垃圾回收器(YGC/Minor GC)对伊甸园区进行垃圾回收,将伊甸园区中不被对象所引用的对象进行销毁,再加载新的对象放到此区中。

  3. 然后将伊甸园中剩余的对象(存活的)移动到幸存者0区

  4. 再次垃圾回收时,还是先销毁对象并将存活对象移动到幸存者1区,然后将处在幸存者0区的也移动到幸存者1区(这些对象的年龄++)。

  5. 接下来重复,每次放入幸存者区时,放入空的那个(to区)

  6. 当再次垃圾回收时, 且当幸存者区中的对象的年龄有到达15的(可以更改-XX:MaxTenuringThreshold=),则将此对象移动到老年区

  7. 老年区相对悠闲,当老年区内存不足时,触发Major GC,进行老年区的清理。

  8. 若老年区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常

2.注意:

​ 当幸存者区满时,不会进行垃圾回收,幸存者区的垃圾回收只是和伊甸园区同时进行。

3.总结:

​ 针对幸存者0,1区:复制之后有交换,谁空谁是to区

​ 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。

3.5GC分类

Minor GC,Major GC, Full GC

针对HotSpot VM的实现,它里面的GC按照回收区又分为两大种类型:

一、部分收集

  1. 新生代收集:(Minor GC/ Yong GC)知识新生代的垃圾收集

  2. 老年代收集:(Major GC/ Old GC)知识老年代的垃圾收集

    注意:目前只有CMS GC会有单独收集老年代的行为

    很多时候Major GC和Full GC混淆使用,需要具体分辨

    1. 混合收集:(Mixed GC)收集整个新生代和部分老年代

    只有G1 GC会有这种行为

二、整堆收集

​ 1. Full GC :收集整个java堆和方法区的垃圾

3.6为什么进行java堆分代?

分代的目的就是优化GC性能

如果没有分代,GC就会搜集所有对象,很慢,导致STW的时间很长。
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。

分代过后,就可以专门清理“朝生夕死”的新生代区,较少次数的清理拥有存活时间较长对象的老年代区域。从而提高GC性能。

3.7内存分配策略/对象晋升规则

  • 优先分配到Eden区

  • 大对象直接分配到老年区(尽量避免程序中出现过多的大对象)

  • 长期存活的对象分配到老年代

  • 动态对象年龄判断:

    ​ 如果幸存者区中相同年龄的所有对象大小的总和>幸存者区空间的一半,那么大于等于该年龄的对象可以直接进老年代,防止大规 模的对象进行来回幸存者区间的移动。

  • 空间分配担保:当伊甸园区GC后仍然放不下对象时,老年代进行空闲分配担保,查看剩余空间,然后将对象放入老年代

3.7 堆空间中常用的jvm参数

-XX:+PrintFlagsInitial:查看所有参数的默认初始值

-XX:+PrintFlagsFianl:查看所有参数的最终值

​ 在dos命令行中:查看具体某个参数的指令: jps: 查看当前运行的进程号

​ jinfo -flag SurivorRatio 进程id(表示查看这个进程的SurivorRatio参数的值)

-Xms:初始堆空间内存(默认为物理内存的1/64)

-Xmm:最大堆空间的内存(默认为物理内存的1/4)

-XX:NewRatio:配置新生代与老年代在堆内存中的占比(默认1:2)

-XX:SurvivorRatio:配置新生代中Eden和S0/S1的占比(默认8:1:1)

-XX:MaxTenuringThreshold:设置新生代对象的最大年龄

-XX:+PrintGCDetails:输出详细的GC处理日志

-XX:HandlePromotionFailure:是否设置空间分配担保
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行MinorGC,否则将进行Full GC;

3.8 逃逸分析

1.简介:

在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法:分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。
当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或是返回到调用者子程序。

JVM判断新创建的对象是否逃逸的依据有:

一、对象被赋值给堆中对象的字段和类的静态变量。

二、对象被传进了不确定的代码中去运行。如果满足了以上情况的任意一种,那这个对象JVM就会判定为逃逸。

2.编译器优化

堆空间并不是对象分配的唯一选择!!!

当判断出对象不发生逃逸时,编译器可以使用逃逸分析的结果作一些代码优化:

  1. 栈上分配,将堆分配转化为栈分配,如果判断出对象不会逃逸,则该对象就可以在分配在栈上,而不是在堆上。

  2. 同步消除。如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。

  3. 分离对象或标量替换。如果某个对象的访问方式不要求该对象是一个连续的内存结构,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

推荐阅读