初识JVM

本文只对jvm中常见的概念进行解读,不做深入讨论,后续会再出一篇文章,对这些概念进行深入说明。

术语介绍

术语

说明

JVM

Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

GC

Garbage Collection,有了GC,程序员就不需要再手动的去控制内存的释放。当Java虚拟机(VM)发觉内存资源紧张的时候,就会自动地去清理无用对象(没有被引用到的对象)所占用的内存空间

年轻代

所有新生成的对象都是优先放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个 Survivor区,S0,S1

老年代

在年轻代中经历了N次垃圾回收后仍然存活的对象或者一些超大对象,就会被放到老年代中。

STW

Stop the world,虚拟机在进行垃圾回收时,需要业务应用线程执行到一些“安全点”,在到达“安全点”时就会暂停当前运行的线程,再执行标记和清除,这个过程会停顿业务应用,所以称之为stop the world。

CMS

Concurrent Mark Sweep,是一款划时代的垃圾回收器,它尽可能的优化STW的时机,使GC线程与业务程序线程并行执行,令程序暂停时间大大降低。

元空间

元空间的概念出现在Java8以后,在Java8以前称为永久代(这里说明下,方法区属于抽象概念,永久代以及元空间都是方法区的实现方式),元空间也是一块线程共享的内存区域,主要用来保存被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。

JVM产品

主流的jvm实现有几种:

  • Oracle HotSpot JVM
    • Oracle官方提供的JVM实现
    • 最广泛使用的JVM
    • 提供客户端(Client)和服务器端(Server)两种模式
  • OpenJDK
    • HotSpot的开源版本
    • 由Oracle和开源社区共同维护
    • 许多Linux发行版的默认JVM
  • IBM J9 (Eclipse OpenJ9)
    • IBM开发的JVM
    • 现以OpenJ9名义开源
    • 专注于低内存占用和高性能
  • GraalVM
    • Oracle开发的多语言虚拟机
    • 支持Java、JavaScript、Python等多种语言
    • 提供原生镜像(Native Image)特性
  • Azul Zing
    • Azul Systems开发的商业JVM
    • 以低延迟著称
    • 包含创新的"ReadyNow"技术
  • Azul Zulu
    • Azul提供的OpenJDK发行版
    • 免费社区版和商业支持版

具体可以通过java -version查看当前实现,下面也重要介绍HotSpot,毕竟用的最多。

HotSpot

  • 架构图如下

Class Files

Java字节码文件(通常以.class为扩展名)是Java源代码编译后的中间表示形式,它包含了JVM可以执行的指令集。字节码文件,也是java语言支持跨平台的基础。

关键组成部分

  1. 魔数(Magic Number):固定值0xCAFEBABE,标识这是一个有效的class文件
  2. 版本号:major_version和minor_version决定class文件版本,例如Java 8对应主版本号52
  3. 常量池(Constant Pool):存储字面量(Literal)和符号引用(Symbolic References)、包括类名、方法名、字段名、字符串常量等。

        -- 我们常常说的符号引用,其实指的是class文件里面的字段信息。从符号引用指向实际引用发生在类加载时。

      4.访问标志(Access Flags):表示类或接口的访问权限和属性,如public、final、abstract等。

示例字节码

简单java代码

package com.example.myapp.service;


public class TestClass {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

转换成字节码javac TestClass.java

查看字节码 javap -c TestClass.class

PS:这里补充说明一点,实际jvm在运行.class的时候,不完全按照文件顺序执行,jvm或者cpu会对命令进行重新排序,以优化其执行效率。这里就是所谓的指令重排序,有兴趣的同学可以了解下。涉及到指令重排序的几个概念有volatile、happens-before原则等。

类加载

Java类加载机制是JVM将.class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型的过程。

类加载的过程包括加载、连接、初始化等。

注意:这里的加载跟类加载不一样,加载只是类加载的第一个环节,不要混淆了。

加载

  • 任务:
    • 查找并加载类的二进制数据
  • 具体工作:
    • 通过类的全限定名获取定义此类的二进制字节流
    • 将字节流代表的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成代表该类的java.lang.Class对象,作为方法区该类的访问入口

验证

  • 任务:
    • 确保Class文件符合JVM规范,不会危害虚拟机安全
  • 验证阶段:
    • 文件格式验证:验证字节流是否符合Class文件格式规范
    • 元数据验证:对类的元数据信息进行语义校验
    • 字节码验证:通过数据流和控制流分析,确定程序语义合法
    • 符号引用验证:发生在解析阶段,验证符号引用能否找到对应的类

准备

  • 任务
    • 为类变量(static变量)分配内存并设置初始值
  • 特点
    • 只分配static变量,不包括实例变量
    • 初始值通常是数据类型的零值(如0、0L、null、false等)
    • 如果static变量是final常量,准备阶段会直接赋值为代码中指定的值

解析

  • 任务
    • 将常量池内的符号引用替换为直接引用
  • 解析对象
    • 类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符

初始化

  • 任务
    • 执行类构造器<clinit>()方法
  • 特点:
    • <clinit>()方法由编译器自动收集类中所有static变量的赋值动作和static代码块合并产生
    • JVM保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
    • 接口中不能使用static代码块,但可以有变量初始化赋值
    • 线程安全,只有一个线程能执行某个类的<clinit>()

类加载器

提到类加载,就在说到类加载器类,类加载器总共分为4种类型:启动类加载器、扩展类加载器、应用程序类加载器、自定义加载器。以下是对应的作用。

双亲委派模型

双亲委派早期被设计出来,主要是为了防止核心的类被篡改,但后期随着时间的发展,逐渐发现类加载器的不适配情况,所以也陆续出现了打破双亲委派的情况。

1、具体的工作流程如下

类加载器收到加载请求,先将请求委派给父类加载器完成,只有父类加载器无法完成时,子加载器才尝试加载。

2、自定义类加载器实例

public class MyClassLoader extends ClassLoader {
    private String classPath;
    
    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadClassData(name);
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }
    }
    
    private byte[] loadClassData(String name) throws IOException {
        name = name.replace(".", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int b;
        while ((b = fis.read()) != -1) {
            baos.write(b);
        }
        fis.close();
        return baos.toByteArray();
    }
}

这里提出一个很有意思的问题,平时我们在开发代码的时候,涉及需要修改某个jar包里面的类,我们会怎么操作呢。很多时候都是简单的直接复制类代码出来,在项目上创建一个一模一样目录,一模一样类名的文件即可实现,有想过原理么,是否跟双亲委派模型有关系呢?

运行时数据区

Java虚拟机在执行Java程序时会把它所管理的内存划分为若干个不同的数据区域,这些区域统称为运行时数据区(Runtime Data Areas)。它们是JVM内存模型的核心组成部分。

以下是对应的架构图,涉及线程共享的有方法区、堆,线程隔离的有程序计数器、本地方法栈、虚拟机栈。

方法区

方法区是各个线程共享的内存区域,在虚拟机启动时创建,虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

String常量存在哪里。

Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享,Java对象实例以及数组都在堆上分配。堆内存空间不足时,也会抛出OOM。一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充

虚拟机栈

  • 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
  • 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
void a() {
    b();
}
void b() {
    c();
}
void c() {
    
}

栈帧

栈帧是Java虚拟机栈的基本组成单元,每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。它包含了局部变量表(Local Variable Table)、操作数栈(Operand Stack)、动态链接(Dynamic Linking)、方法返回地址(Return Address)

本地方法栈

本地方法栈(Native Method Stack)是为JVM运行Native方法服务的内存区域。Native方法是指用非Java语言(如C、C++)编写并通过Java Native Interface (JNI)调用的方法。

像我们在查看一些源码的时候,方法的修饰符里面带了native字段的,他的实现都依赖了本地方法栈。

程序计数器

  • 程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
  • 如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,则这个计数器为空。
  • Java多线程是通过线程轮流切换并分配处理器执行时间实现的,每个线程都需要独立记录自己的执行位置。切换后能恢复到正确的执行位置,确保线程切换后能继续从上次执行点继续执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值