JVM的基本概念

  1. 什么是JVM?

    1. JVM是Java的核心和基础,是介于Java编译器和操作平台之间的虚拟处理器,基于底层操作系统和硬件平台使用软件方法实现的抽象计算机,可以在JVM上面执行编译后的Java字节码程序。Java源文件经过Java编译器编译成JVM可执行的字节码文件,通过JVM将字节码翻译(解释)成不同平台机器码,通过特定平台运行。

    2. 参考链接
  2. JVM的生命周期

    1. JVM的诞生:当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
    2. JVM实例的运行:main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,Java程序也可以标明自己创建的线程是守护线程。
    3. JVM实例的消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出。
  3. JVM运行内存模型

  4. JVM类加载机制

    1. JVM把文件.class字节码加载到内存,对数据进行校验,准备和解析,并初始化,最终形成可以被虚拟机直接使用的java类型。加载、验证、准备、解析、初始化 的执行过程就是类的加载过程。

    2. 类加载机制指将.class文件中的二进制数据读入到JVM内存中,并对数据进行校验,解析和初始化。最终每一个类都会在方法区保存一份它的元数据,在堆中创建一个与之对应的Class对象。
  5. 类的加载阶段

    1. 加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”。
    2. 主要完成以下功能:
      1. 通过“类全名”来获取定义此类的二进制字节流
      2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构----模板
      3. 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区模板数据的访问入口
  6. 类的验证阶段

    1. 验证是链接阶段的第一步,主要目的是确保class文件中字节流包含的信息符合当前虚拟机要求,并且不会危害虚拟机自身安全。
    2. 验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
  7. 类的准备阶段

    1. 准备阶段正式为类变量分配内存并设置类变量初始值,这些内存都将在方法区中进行分配。
    2. 这个阶段中有两个容易产生混淆的知识点:
      1. 这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。
      2. 这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为: public static int value = 123;那么变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何java方法,把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会被执行。上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,建于上面类变量value定义为:public static final int value = 123;编译时java才将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。
  8. 类的解析阶段

    1. 解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
      符号引用:一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
      直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
      解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。
      1.类、接口的解析
      2.字段解析
      3.类方法解析
      4.接口方法解析
  9. 类的初始化阶段

    1. 类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

  10. 类加载的时机

    1. 使用new实例化对象时、读取或者设置一个类的静态字段或方法时
    2. 反射调用时,例如 Class.forName("com.xxx.MyTest")
    3. 初始化一个类的子类,会首先初始化子类的父类
    4. java虚拟机启动时标明的启动类
    5. JDK8 之后,接口中存在default方法,这个接口的实现类初始化时,接口会其之前进行初始化
    6. 参考链接
  11. Java类加载器有哪些?

    1. 类加载器的任务是根据一个类的全限定名,来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例
    2.  java中有一下四种类加载器:
      1. 启动类加载器(Bootstrap ClassLoader)-----c++编写,加载java核心库 java.*
        ■负责加载放在<JAVA_HOME>\lib目录或被-Xbootclasspath参数所指定的路径中,并且是虚拟机可识别的类库。由于引导类加载器涉及到虚拟机本地实现细节,用户无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
      2. 扩展类加载器(Extension ClassLoader)-----java编写,加载扩展库
        ■由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录或被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接用。
      3.  应用程序类加载器(Application ClassLoader)---------java编写,加载程序所在的目录
        ■这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。
      4. 自定义类加载器(User ClassLoader)
        ■用户自己定义的类加载器。
  12. 类装载器的内部机制----双亲委派机制

    1. 当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

    2. 双亲委派机制的作用
      1. 保证安全性:防止重复加载同一个class。通过委托去向上面问,加载过了就不用再加载一遍。保证数据安全。
      2. 保证唯一性:保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象。不同加载器加载同一个.class也不是同一个Class对象,这样保证了Class执行安全。
      3. 解释:如果没有双亲委派模型而是由各个类加载器自行加载,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证,因为Object都各不相同那么程序运行启动就会出错,也保证了JVM能够正常的安全运行。
    3. 参考链接
  13. Java类的生命周期

    1. 从被加载到虚拟机内存中开始到卸载出内存为止,经历7个阶段,分别是加载、验证、准备、解析、初始化、使用、卸载。

    2.  

  14. 运行时数据区

    1. JVM运行时会分配好方法区和堆,而JVM每创建一个线程,就为其分配一个程序计数器、Java虚拟机栈、本地方法栈。当线程终止时,三者(程序计数器、Java虚拟机栈、本地方法栈)所占用的内存空间也会释放掉。
    2. 程序计数器、Java虚拟机栈、本地方法栈的生命周期与线程相同,方法区和堆的生命周期与JAVA程序运行生命周期相同,所以gc只发生在线程共享的区域(大部分发生在Heap上)
  15. 堆:线程共享

    1. Java中的堆是用来存储对象实例以及数组(当然,数组引用是存放在Java栈中的)。堆是被所有线程共享的,因此堆上进行对象的内存分配均需要进行加锁,这导致new对象的开销比较大。JVM中只有一个堆,是Java垃圾收集器管理的主要区域,Java的垃圾回收机制会自动进行处理。
    2. 堆空间分为老年代和年轻代。刚创建的对象存放在年轻代,老年代中存放生命周期长久的实例对象。
    3. 年轻代中又被分为Eden区和两个Survivor区。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次GC仍然存活的,就会被转移到老年代。 当一个对象大于eden区而小于old区(老年代)的时候会直接扔到old区。 而当对象大于old区时,会直接抛出OutOfMemoryError(OOM)
    4.  

  16. 方法区=元空间=永久代:存储在物理硬盘(非堆)——线程共享

    1. 方法区中存储了每个类的信息(包括类的名称、修饰符、方法信息、字段信息)、类中静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息以及编译器编译后的代码等
    2. 程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区,同时方法区也是全局共享的,一定条件下也会被GC,主要是常量池和类型的卸载。当方法区需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
    3. 在方法区中有一个非常重要的部分就是运行时常量池,用于存放静态编译产生的字面量和符号引用。运行时生成的常量也会存在这个常量池中,比如String的intern方法。它是每一个类或接口常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。
    4. 为什么方法区又称为非堆: 因为在内存模型中他存储的数据和堆有些不同,习惯性把他从堆中单独区分出来。 方法区属于非堆内存。它存储每个类结构,如运行时常数池、字段和方法数据,以及方法和构造方法的代码。它是在 Java 虚拟机启动时创建的。
  17. Java虚拟机栈:——非线程共享

    1. 常常说的栈。JVM栈是线程私有的,每个线程创建时都会创建自己的JVM栈,互不干扰。 JVM栈是Java方法执行的内存模型。
    2. JVM栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法。栈帧包括局部变量表、操作数栈、指向当前方法所属的类的运行时常量池的引用、方法返回地址和一些额外信息。
    3. 当线程执行一个方法时,会随之创建一个对应的栈帧,并将建立的栈帧压栈。 方法执行完毕后,会将栈帧出栈。因此,线程当前执行的方法所对应的栈帧必定位于JVM栈的顶部。
    4. 局部变量表: 存储方法中的局部变量,对于引用类型的变量,则存的是指向对象的引用,对于基本数据类型的变量,则直接存储它的值。
    5. 操作数栈: 栈最典型的一个应用就是用来对表达式求值。一个线程执行方法的过程,就是不断执行语句的过程,归根到底就是计算的过程。程序中所有计算都是借助操作数栈完成。
    6. 指向运行时常量池的引用: 方法执行过程中可能用到类中的常量,必须要有一个引用指向运行时常量。
    7. 方法返回地址: 当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
  18. 本地方法栈:——非线程共享

    1. JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。
    2. 本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
    3. 在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
  19. 程序计数器:——非线程共享

    1. 在JVM中多线程是通过线程轮流切换获得CPU执行时间,任一时刻一个CPU的内核只会执行一条线程中的指令。为了让每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都有独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。
    2. 在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
    3. 程序计数器中存储的数据所占空间大小不随程序的执行发生改变,程序计数器不会发生内存溢出现象(OutOfMemory)。
  20. 创建对象内存分析

    1. 首先会在方法区加载Class模板,每个模板都有该类的属性以及方法和常量池,以及静态变量随着类加载而加载到静态方法区,并在java堆中生成一个代表这个类的Class对象的内存区域,为类变量分配内存并设置类变量初始值,例如String类型为null, 而后在栈中存储该对象的引用地址,地址指向堆中类的内存地址,最后初始化,例如构造方法,赋值给堆中的对象内存数据中。每一个线程都会给他分配指定的 java栈,程序计数器,本地方法栈,所以说这三个是线程独享线程安全。堆和方法区线程一起使用线程不安全。

JVM系列文章

posted @   雨也飘柔  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示