JVM

JVM

类加载器分类

1.虚拟机自带的加载器

1.1启动类加载器(引导类加载器,Bootstrap ClassLoader)
  • 这个类加载器使用C/C++语言实现,嵌套在JVM内部
  • 它用来加载Java的核心库(JAVA_HOME/jar/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  • 并不继承java.lang.ClassLoader,没有父加载器
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
1.2扩展类加载器(Extension ClassLoader)

![image-20220419144749782](

  • java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK安装目录的jar/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在次目录下,也会自动由扩展类加载器加载
1.3应用程序类加载器(系统类加载器,AppClassLoader)

  • java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 该类加载是程序中默认的类加载器一般来说,java应用的类都是由它来完成的
  • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

2.用户自定义加载器

2.1实现步骤
  1. 继承java.lang.ClassLoader,实现自己的类加载器
  2. 没有太过复杂的需求,可以直接继承URLClassLoder,避免编写findClass()方法以及获取字节码流的方式
2.2作用
  • 在Java的日常应用开发中,类的加载几乎是由上述3种类的加载器互相配合执行的,在必要时,我们还可以自定义类加载器,来制定类的加载方式。
  • 为什么要自定义类加载器
    • 隔离加载类
    • 修改类的加载方式
    • 扩展加载源
    • 防止源码泄露

3.关于ClassLoader

ClassLoader类,他是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)

方法名称 描述
getParent 返回该类加载器的超类加载器
loadClass(String name) 加载名称为name的类,返回结果为java.lang.Class类的实例
findClass(String name) 查找名称为name的类,返回结果为java.lang.Class类的实例
findLoadedClass(String name) 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例
defineClass(String name,byte[] b,int off,int len) 把字节数组b中的内容转换为一个java类,返回结果为java.lang.Class类的实例
resolveClass(Class<?> c) 链接指定的一个Java类

4.获取ClassLoader

  1. 获取当前类的ClassLoader
    • clazz.getClassLoader()
  2. 获取当前线程上下文的ClassLoader
    • Thread.currentThread.getCountextClassLoader()
  3. 获取系统的ClassLoader
    • ClassLoader.getSystemClassLader()
  4. 获取调用者的ClassLoader
    • DriverManager.getCallerClassLoader()

双亲委派机制

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

1.工作原理

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

2.优势

  1. 避免类的重复加载
  2. 保护程序安全,防止核心API被随意篡改
    1. 保护核心类
    2. 保护包
核心同包同名类不能被加载
package java.lang;

public class String {
    static {
        System.out.println("自定义String");
    }
    // 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
    //   public static void main(String[] args)
    //   否则 JavaFX 应用程序类必须扩展javafx.application.Application
    public static void main(String[] args) {
        System.out.println("aaa");
    }
}
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
核心包创建类
package java.lang;

public class StringDemo {
    /**
     * Error: A JNI error has occurred, please check your installation and try again
     * Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
     */
    public static void main(String[] args) {
        System.out.println("asdasdad");
    }
}
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:754)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:601)

3.沙箱安全机制

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

其他

  • JVM中表示两个calss对象是否为同一个的条件是
    • 类的完整类名必须一致,包括包名
    • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
  • 换句话说,在JVM中,即使这两个类对象(Class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载他们的ClassLoader示例对象不同,那么这两个类对象也是不相等的
不太明白的知识点

JVM必须知道一个类型是由启动类加载器加载还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的加载器是相同的

类的主动使用和被动使用

Java程序对类的使用方式分为

  • 主动使用
  • 被动使用

主动使用

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用静态方法
  4. 反射
  5. 初始化一个类的子类
  6. Java虚拟机启动时被标明为启动类的类
  7. JDK7开始提供的动态语言支持
    1. java.lang.invoke.MethodHandle实例的解析结果
    2. REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有被初始化,则初始化

被动使用

除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化

三、运行时数据区概述及线程

3.1 运行时数据区

内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了JAVA在运行过程中内存申请、分配、管理的策略,保证了JVM的高效运行。不同的JVM对内存的划分和管理机制存在着部分差异

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

灰色的为单独线程私有的,红色的为多个线程共享的

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

3.2 线程

线程是一个程序里的运行单元,JVM允许一个应用有多个线程并行的执行。

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

  • 当一个Java线程准备好执行后,此时一个操作系统的本地线程也同时创建。Java线程终止后,本地线程也会回收

操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,他就会调用到Java线程中的run()方法上

Hotspot JVM里主要是以下几个线程

  • 虚拟机线程
  • 周期任务线程
  • GC线程
  • 编译线程
  • 信号调度线程

四、PC寄存器(程序计数器)

JVM中的程序计数寄存器(Program Counter Registers)中,Registers的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载带寄存器才能够运行

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

4.1 作用

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

  • 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域
  • 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址:或者,如果是在执行native方法,则是未指定值(undefined)。
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

4.2 两个常见问题

  1. 使用PC寄存器存储字节码指令地址有什么用呢?
  2. 为什么使用PC寄存器记录当前线程的执行地址呢?

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

JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

  1. PC寄存器为什么设置为线程私有的

我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。

这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈颜,程序计数器在各个线程之间互不影响。

4.3 CPU时间片

CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
在宏观上:
我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
但在微观上:
由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

五、虚拟机栈

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

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

栈是运行时的单位,而堆是存储的单位。
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

5.1 Java虚拟机栈是什么
5.1.1 定义
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。

是线程私有的

5.1.2 生命周期

​ 生命周期和线程一致

5.1.3 作用

​ 主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。

  • 局部变量 成员变量(或属性)
  • 基本数据类型变量 引用类型变量
5.1.4 栈的特点(优点)
  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
  • JVM直接对Java栈的操作只有两个
    • 每个方法执行,伴随着进栈(入栈、压栈)
    • 执行结束后的出栈工作
  • 对于栈来说不存在垃圾回收问题

5.2 栈中可能出现的异常
  • Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
    • 如果采用固定大小的Java虚拟机栈,那每一个线程的ava虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
    • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出-个OutOfMemoryError异常。
5.3 设置栈内存大小

参数:-Xss

-Xss size

Sets the thread stack size (in bytes). Append the letter k or K to indicate KB, m or M to indicate MB, and g or G to indicate GB. The default value depends on the platform:

  • Linux/x64 (64-bit): 1024 KB
  • macOS (64-bit): 1024 KB
  • Oracle Solaris/x64 (64-bit): 1024 KB
  • Windows: The default value depends on virtual memory

The following examples set the thread stack size to 1024 KB in different units:

-Xss1m
-Xss1024k
-Xss1048576
5.4 栈中存储什么
  • 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

复习

  • OOP的基本概念
    • 类、对象
  • 类中基本结构
    • field(属性、字段、域)、method
5.5 栈运行原理
  • JVM直接对Java栈的操作只有两个,就是对栈帧的压栈出栈,遵循“先进后出”/“后进先出”原则。
  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(CurrentMethod),定义这个方法的类就是当前类(Current Class)
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
  • 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
  • Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

posted @ 2022-04-20 16:47  Zmyy  阅读(68)  评论(0)    收藏  举报