jvm(1)

JVM概述

为什么学JVM(java Virtual Machine)

对于程序在运行时的性能方面的管理的需要 架构师/高级程序员必须掌握的

对于我们写代码时,是有帮助的,例如对内存的分布管理… 不同垃圾回收器的选择…

让我们对java程序运行过程更加了解,为我们写出优质的代码做好准备

什么是虚拟机

​ 虚拟机是一种软件,是一种模拟的计算机环境,分为系统虚拟机(VMware)和程序虚拟机(JVM 只运行java程序).

JVM的作用

java程序能够实现跨平台,前提是jvm与平台相关的.

​ jvm首先负责加载字节码到虚拟机中,(类加载器)

负责存储产生变量,对象,类信息…(运行时数据区)

负责将字节码编译/解释为机器码

负责调用操作系统本地方法

一次编译到处运行

自动内存管理

自动垃圾管理

是java程序运行的一个统一管理的虚拟环境.

JVM整体构成部分

虚拟机整体由4个部分构成

1.类加载器(加载字节码文件

  1. 类加载(从硬盘或网络中加载字节码文件到内存(运行时数据区的方法去)中再去通过这个类模板实例n的对象
  2. 链接
    1. 验证
    2. 准备
    3. 解析
  3. 初始化

2.运行时数据区

  1. 方法区(存类信息)
  2. 堆(存对象)
  3. java虚拟机栈(运行java中的方法)
  4. 程序计数器(记录代码运行到哪行了)
  5. 本地方法栈(运行本地方法)

3.本地方法接口

  1. 负责调用本地方法,例如:Object hashCode() 、启动线程 start()、IO 输入read

4.执行引擎

​ 将java字节码 编译/解释为机器码

类加载

类加载子系统

在这里插入图片描述

​ 类加载器子系统负责从文件系统或者网络中加载 class 文件。 classLoader只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。

​ 加载的类信息存放于一块称为方法区的内存空间。
在这里插入图片描述

class file 存在于硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载 JVM 当中来,根据这个模板实例化出 n 个实例. class file 加载到 JVM 中,被称为 DNA 元数据模板. 此过程就要有一个运输工具(类加载器 ClassLoader),扮演一个快递员的角色.

类加载过程

加载
  • 1.通过类名(地址)获取字节流
  • 2.转换成运行时格式,为每个类创建Class类的对象,存储在方法区中
链接(验证、准备、解析)
  1. 验证

    1. 对字节码文件格式进行验证,看是否被污染
    2. 对基本语法格式进行验证,是否符合java语言规范,例如是否有父类(元数据验证)
  2. 准备

    负责为类的静态属性分配内存,并设置默认值;不包含final修饰的static常量,final修饰的static常量是在**编译(将.java文件编译为.class字节码文件)**时进行初始化

    //例如:
    public static int = 123//value 在准备阶段后的初始值是 0,而不是 123.
    
  3. 解析

    • 将类的二进制数据中的符号引用替换成直接引用(符号引用是 Class 文件的逻辑符号,直接引用指向的方法区中某一个地址)
    • 将字节码中的表现形式转换为内存中的表现(在内存的地址)

    解析在某种情况下也分为静态和动态解析,比如当一个java类被编译为Class文件后,假设这个类为A,其中引用了B,在编译阶段A类是不知道B类有没有被编译的,而且B此时也一定没有被加载,所有A不知道B的实际地址,那么编译后的class文件中,将会使用一个字符S来代表B的地址,那么S就被称为B的符号引用,如果A发生类加载并且到了解析阶段发现B还没有被加载,那么此时会触发B的类加载,B被加载后,A中的符号引用就会转换为直接引用,也就是B实际的内存地址。A调用了一个具体的实现类B,这就称为静态解析,因为解析的目标很明确,而如果B使用了多态,比如B是一个接口,它有多个实现类,那么A在解析时就不知道使用那个具体类的直接引用来进行替换,既然不知道那就等一等吧,直到在运行过程中发生了调用,此时虚拟机调用栈中将会得到具体的类信息,这时候再进行解析,就能用明确的直接引用,来替换符号引用,这也就解释了为什么解析阶段有时候会发生在初始化阶段之后,这就是动态解析,用它来实现后期绑定。java的多态是后期绑定来实现的。

初始化
  1. 初始化,为类的静态变量赋予正确的初始值

    public static int = 123//value 在初始化阶段后的初始值是123.
    
  2. 类什么时候初始化

    JVM 规定,每个类或者接口被首次主动使用时才对其进行初始化.

    • 通过 new 关键字创建对象
    • 使用类中的静态属性(变量或方法)0
    • 对某个类进行反射操作 反射Class.forName(“类的地址”)
    • 初始化子类会导致父类被初始化
    • 执行该类的main方法

    其实除了上面的几种主动使用,以下两种情况为被动使用不会加载类.

    1. 引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致类加载,比如:

      public final static int NUMBER = 5 ; //不会导致类初始化,是被动使用
      public final static int RANDOM = new Random().nextInt() ; //会导致类加载
      
    2. 构造某个类的数组时不会导致该类的初始化,比如:

      Student[] students = new Student[10] ;
      
  3. 类的初始化顺序

    如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

    如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

    public class ClassInit {
        static {
            a = 20;
        }
        static int a = 10;
    
        public static void main(String[] args) {
            //变量a的值从准备阶段到初始化的变化过程 a-->0-->20-->10
            System.out.println(ClassInit.a);//10
        }
    }
    

类加载器

​ 站在 JVM 的角度看,类加载器可以分为三种:

  1. 引导类加载器(启动类加载器 Bootstrap ClassLoader).

    这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类库.并不继承于 java.lang.ClassLoader 没有父加载器.

  2. 扩展类加载器(Extension ClassLoader)

    Java 语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现.

    派生于 ClassLoader 类.

  3. 应用类加载器(系统类加载器 Application ClassLoader)

    Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现.

    派生于 ClassLoader 类.

    加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类.

    该类加载器是程序中默认的类加载器.

  4. 自定义类加载器

双亲为派机制

​ Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式.

在这里插入图片描述

工作原理

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行.

  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器.

  3. 如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制.

    如果均加载失败,就会抛出 ClassNotFoundException 异常。

​ 如果我们自己创建一个名为 java.lang 的包,再创建一个名为 String 的类,当我们new String()时,会将加载创建核心类库中的 String 对象还是创建我们自己创建的 String 类对象?

package java.lang;//自己创建的包

public class String {
    static{
        System.out.println("自定义String类");
    }

    public static void main(String[] args) {
        new String();
    }
}
/*
	这段代码其实不会加载我们伪造的String类,因为双亲委派机制会逐级向上委托父加载器去寻找该类,如果加载了我们伪造的类就会覆盖系统的String类,存在安全问题。但是也允许开发者自定义类加载器,去打破双亲为派机制。
*/
/*
	运行结果
	错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
*/

如何打破双亲为派机制

​ 通过继承ClassLoader去重写 loadClass 方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐) 或重写 findClass 方法 (推荐) 。

​ 我们可以通过自定义类加载重写方法打破双亲委派机制。

​ 再例如 tomcat 等都有自己定义的类加载器.(加载部署在tomcat中的项目时,就使用的是tomcat自己的类加载器)

运行时数据区

程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

程序计数器用来存储下一条指令的地址,即将要执行的指令代码.由执行引擎读取下一条指令

  • 它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域.
  • 每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致.
  • 程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址.
  • 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成,在线程切换的时候可以保存上一个线程的执行位置,方便恢复时快速定位。
  • 它是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError(内存溢出)情况的区域,因为程序计数器存储的是字节码文件的行号,这个范围是可知的,在开始分配内的时候就会分配一个绝对不会溢出的内存空间
  • 执行native方法是程序计数器值为空,因为native方法是c语言实现,没有编译成字节码指令,也就不需要去存储了

本地方法栈(Native Method Stack)

  1. 用来运行本地方法的区域
  2. 是线程私有的
  3. 空间大小是可以调整的
  4. 可能会出现栈溢出
  5. 是用C语言实现的
  6. 它的具体做法是在 Native Method Stack 中登记 native(本地) 方法,在 执行引擎执行时加载本地方法库.

Java 虚拟机栈(Java Virtual Machine Stacks)

  • 描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。

  • 特点:

    • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器.

      JVM 直接对 java 栈的操作只有两个:调用方法入栈.执行结束后出栈.

      对于栈来说不存在垃圾回收问题

    • 栈中会出现异常,当线程请求的栈深度大于虚拟机所允许的深度时,会出现StackOverflowError(栈溢出).

  • 栈的运行原理
    • JVM 直接对 java 栈的操作只有两个,就是对栈帧的入栈和出栈,遵循先进后出/后进先出的原则.

    • 在一条活动的线程中,一个时间点上,只会有一个活动栈,即只有当前在正在执行方法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈(Current Frame),与当前栈帧对应的方法称为当前方法(Current Method),定义这个方法的类称为当前类(Current Class).

    • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作.

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

    • 线程私有,不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法).

    • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧. Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常.不管哪种方式,都会导致栈帧被弹出.

在这里插入图片描述

  • 栈帧的内部结构
    • 局部变量表(Local Variables)
      • 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。
    • 操作数栈(Operand Stack)(或表达式栈)
      • 栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
    • 动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)
      • 因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
    • 方法返回地址(Retuen Address)(或方法正常退出或者异常退出的定义)
      • 当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址

在这里插入图片描述

java堆(Java Heap)

  • 基本作用特征

    • 是存储空间,用来存储对象,是内存空间最大的一块区域
    • 在jvm启动时就被创建,大小也就确定了
    • 大小是可调整的
    • 所有线程共享java堆,在这里可以划分线程私有的缓冲区
    • 本区域是存在垃圾回收的
    • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
  • 堆空间的分区

    • 年轻代(youngGen)
      • 伊甸园区(eden)(刚创建的对象存放在这里)
      • 幸存者区(Survivor)
    • 老年代(OldGen)

在这里插入图片描述

  • 为什么分代?

    将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫

    描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

  • 对象创建内存分配过程

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

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

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

    4. 如果再次出发垃圾回收,此时上次幸存下来存放到幸存者0区的对象,如果没有回收,就会被放到幸存者1区,每次会保证有一个幸存者区是空的.

    5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区.

    6. **在对象头中,它是由4位数据来对GC年龄进行保存的,所以最大值为1111,即为15。所以在对象的GC年龄达到15时,就会从新生代转到老年代。**也可以设置参数,MaxTenuringThreshold=N(N<=15)

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

    8. 若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常. Java.lang.OutOfMemoryError:Java heap space

      public static void main(String[] args){
      	List<Integer> list = new ArrayList();
          //集合中的对象一直在创建,但不能销毁掉就会导致堆空间溢出
      	while(true){
      		list.add(new Random().nextInt());
      	}
      }
      
  • 新生区和老年区配置比例

    • 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优
      • 默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3
      • 可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5
    • 在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发人员可以通过选项**-XX:SurvivorRatio**调整这个空间比例。比如:-XX:SurvivorRatio=8
  • 分代收集思想 Minor GC、Major GC、Full GC

    ​ JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的

    都是指新生区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大

    类型:一种是部分收集,一种是整堆收集

    • 部分收集(不是完整收集java堆的垃圾收集,其中又分为两种)
      • 新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.
      • 老年区收集(Major GC / Old GC):只是老年区的垃圾收集.
    • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾
      • 调用System.gc();时(不会立即执行)
      • 老年区空间不足时
      • 方法区空间不足时

方法区(Methed Area)

  • 方法区的基本理解

    • 方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field 等元数据、static final 常量、static 变量、即时编译器编译 后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但

      对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是 要和堆分开.

      所以,方法区看做是一块独立于 java 堆的内存空间.

    在这里插入图片描述

    • 方法区在 JVM 启动时被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的.
    • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展.
    • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误,关闭 JVM 就会释放这个区域的内存.
  • 堆栈方法区交互关系

在这里插入图片描述

局部变量person持有java堆中Person对象实例的引用,Person实例中又存有方法区中Person类的类型信息的引用

在这里插入图片描述

  • 方法区的大小设置

    java方法区的大小不必是固定的,用户可以根据需要去动态的调整。

    • 元数据区的大小可以使用参数MetaspaceSize和MaxMetaspaceSize指定
    • 默认值是依赖于平台的,windows下,MetaspaceSize是21MB
    • MetaspaceSize 初始值是 21M,也称为高水位线,一旦触及就会触发 Full GC,所以为了减少FULL GC那么给它设置一个较高的值
  • 方法区的内部结构

在这里插入图片描述

方法区它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存,运行常量池等。运行时常量池就是一张表,虚拟机指令根据这张表,找到要执行的类名、方法名、常量等信息,存放编译期间生成的各种字面量和符号引用。

  • 方法区的垃圾回收

    • 《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。
    • 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。

    方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型

    类的卸载

    判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

    1. 该类的所有实例都已经被回收,也就是java堆中不存在该类及其任何派生子类的的实例。
    2. 加载该类的类加载器已经被回收,这个条件除非是自己实现的可替换类加载器的情景,否则很难达成
      的代码缓存,运行常量池等。运行时常量池就是一张表,虚拟机指令根据这张表,找到要执行的类名、方法名、常量等信息,存放编译期间生成的各种字面量和符号引用。
  • 方法区的垃圾回收

    • 《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。
    • 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。

    方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型

    类的卸载

    判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

    1. 该类的所有实例都已经被回收,也就是java堆中不存在该类及其任何派生子类的的实例。
    2. 加载该类的类加载器已经被回收,这个条件除非是自己实现的可替换类加载器的情景,否则很难达成
    3. 该类的class对象没有在任何地方被引用,无法在任何地方通过反射访问该类
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值