JVM 学习第二天

双亲委派机制

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

工作原理

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
  • 如果父类加载器可以玩成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

image-20201208124956550

image-20201208125212774

神奇的现象,我这边定义了一个String,也叫java.lang.String,而不会加载我自己的这是因为被启动类加载器,加载了核心代码中的java.lang.String了。理解上面的那三句话就可以了解到这是什么情况了。

image-20201208125956821

如果加载第三方的jar包依赖,那么就会出现反向委托,通过线程获取应用上下文获取加载器,那么当前这个类如果不是核心等包下的,那么自然就是appClassLoader,应用加载器进行直接加载。

  • 优势:

    • 避免类的重复加载
    • 保护程序安全,防止核心API被随意篡改

      • 自定义类: java.lang.String
      • 自定义类:java.lang.ShkStart

沙箱安全机制

自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中javalangString.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样保证对Java核心源代码的保护,这就是沙箱安全机制

判断两个类是不是同一个:

  • 类的完整类名必须一致,包括包名。
  • 加载这个类的类加载器必须相同。

运行时数据区

image-20201208131955162

image-20201208132048116

编译成class文件,通过加载 -》 链接 -》 初始化之后,通过类加载机制,把类加载到运行时数据区,运行时数据区干了啥事,现在不知道。

复习一下:

  • 加载,将class文件进行一个加载,二进制字节流的形式,放入到方法区中。
  • 验证,目的是校验class文件字节码中是否符合class文件的规范,是否保证安全性,主要有一个魔术值,开头咖啡宝贝。
  • 准备,将收集类变量并且赋予零值,这个0不是0而是无的意思。不包含finnal修饰的static,因为这个在编译的时候就会分配。
  • 解析,主要是将符号引用变为直接引用。
  • 初始化,对类变量进行一个用户赋值的操作。

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

  • 每个线程:独立包括程序计数器、栈、本地方法栈
  • 线程间共享:堆、堆外内存(永久代或元空间、代码缓存)

方法区在JDK8 换成了 本地内存的元空间。

一个虚拟机只有一个运行时数据区,即RunTime。

线程

  • 线程是一个程序里的运行单元,JVM允许一个应用有多个线程并行的执行。
  • 在Hotspot JVM中,每个线程都与操作系统的本地线程直接映射。

    • 当一个Java线程准备好执行之后,此时一个操作系统的本地线程同时创建。Java线程执行终止后,本地线程也会回收。Java线程与本地线程同步。
  • 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run() 方法。

程序计数器(PC寄存器)

JVM中的程序计数寄存器中, Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装在到寄存器才能够运行。

这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器) 会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

作用:

  • PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取吓一跳指令。

image-20201208204805680

image-20201208205014669

image-20201208205203793

image-20201208210539571

左边的0、2、3、5....就是指令地址(偏移地址),右边的bipush等等就是操作指令

比如执行到了5,程序计数器中就会保存这个5,之后执行引擎就会从程序计数器中取出5所对应的字节码指令去操作局部变量表、操作数栈,之后转为机器指令,把该机器指令发送给CPU进行操作。

两个常见的问题

  • 使用程序计数器存储字节码指令地址有什么用呢?为什么使用程序计数器记录当前线程的执行地址呢?

    • 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪里开始继续执行。
    • JVM字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
  • 程序计数器为什么要设定为线程私有的?

    • 如果不设定为线程私有的,每一个线程执行不同的代码,如果程序计数器进行共享,那么切换过去之后,再次切回来,那么指令该从哪里开始执行?就会造成代码执行顺序混乱。

虚拟机栈

虚拟机栈概述

​ 由于跨平台性的设计,Java的指令都是根据栈来设计的。不同的平台CPU架构不同,所以不能设计为基于寄存器的。

有点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

  • 内存中的栈和堆

    • 栈是运行时的单位,而堆是存储的单位。

      • 即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放在哪里。
  • Java虚拟机栈是什么?

    • Java虚拟机栈,早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用。

      • 是线程私有的。
    • 生命周期

      • 生命周期和线程一致。
    • 作用

      • 主管Java程序的运行,它保存方法的局部变量(如果是基本的8种基本数据类型,就进行一个存放,如果是引用对象,那么只会存储一个对象的引用,对象真实存储在堆中的)、部分结果,并参与方法的调用和返回。

        • 局部变量 vs 成员变量(或属性)
        • 基本数据变量 vs 引用类型变量(类、数组、接口)
    • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
    • JVM直接堆Java栈的操作只有两个:

      • 每个方法执行,伴随着进栈(入栈、出栈)
      • 执行结束后的出栈工作
    • 对于栈来说不存在垃圾回收问题

      • 不会又GC,但是会存在OOM和SEO(栈深度无法扩展)

(P45。。。)

最后修改:2020 年 12 月 09 日 08 : 44 AM
如果觉得我的文章对你有用,请随意赞赏