浅谈JVM虚拟机

JVM是用来执行字节码文件(.class)的虚拟机,运作与操作系统之上。由于可以将jvm看作安装在操作系统上的软件,因此Java编写的程序可以在不同的操作系统上运行,因为编译后的字节码与平台就无关了,只需要在不同的操作系统上安装一个对应版本的 jvm 虚拟机。

在这里插入图片描述
JVM大致分为如上图所示的三个部分:

  1. 类加载子系统(ClassLoader);
  2. 运行时数据区(包括方法去、堆、JVM栈、程序计数器 和 本地方法栈);
  3. 执行引擎(JIT编译器:对热点数据进行二次编译,将字节码指令变成机器指令,将机器指令存放在方法区缓存;解释器:逐行解释字节码)。

类加载

类加载是把类的 .class 文件中的二进制数据读取到内存,将类的信息放在运行时数据区的方法区中,通过给每个不同类创建一个 java.lang.Class 对象(Class对象封装了类在方法去内的数据结构,保存在堆区),调用其 newInstance() 方法在堆内存中创建对应的实例对象。

在这里插入图片描述

类的加载大致包括五个步骤:加载、验证、准备、解析、初始化(中间三项统称链接)。

加载:将.class文件读到内存,并为之创建一个 java.lang.Class对象。类的加载由类加载器完成,类加载器由 jvm 提供。 jvm提供一些系统类加载器,开发者可以用继承ClassLoader的方式自行设计类加载器。

链接:当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

(1)验证:检验类的加载是否有正确的内部结构,并于其他类协调一致。

(2)准备:为类的静态变量分配内存,并设置默认值。

(3)解析:将类的二进制数据中的符号引用替换成直接引用。(概念可参考

初始化:为类的静态变量赋予正确的初始值。

============================================================================

双亲委派机制

加入我们自己写了一个 String 类,并放在 java.lang 包下面,这个自定义的 String 类包含 main 方法,那么这个程序能否执行(会不会调用到java里面自带的String类?)

答案是不能运行,还是会首先调用java自带的String类,这是什么原因?

因为JVM预定义了三种类加载器:分别是根类加载器(bootstrap class loader)、扩展类加载器(extension class loader)以及 系统类加载器(system class loader)。

根类加载器:用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。

扩展类加载器:负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。

系统类加载器:被称为系统(应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性。如果没有特别指定,用户自定义的类加载器都以此类加载器作为父加载器。

可以发现,java自带的String类需要由根类加载器加载,而我们自己写的String类由系统类加载器加载。在加载的时候首先要看根类加载器能否加载,如果能加在则用根类加载器加载。其次,还要看父类加载器能否加载,如果父类都不能加载的情况下,再由自身加载器来加载。

双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

上面逻辑可能有些模糊,看一下下面的流程就一目了然了:

类加载器加载Class大致要经过如下8个步骤:

  1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
  2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
  3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
  4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
  5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
  6. 从文件中载入Class,成功后跳至第8步。
  7. 抛出ClassNotFountException异常。
  8. 返回对应的java.lang.Class对象。

JVM内存结构

JVM内存结构主要包括五个部分:程序计数器、本地方法栈、虚拟机栈、方法区和堆。其中前三者是线程私有的。而方法区、堆属于线程共享的两个数据区,也是JVM优化的关键所在。

程序计数器:一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。程序计数器是线程私有的,每一个线程都有自己独立的程序计数器。

虚拟机栈:每次执行一个方法,就会在虚拟栈中创建一个栈帧。栈帧用于存储局部变量表(包括八大基本类型,以及对象的引用)、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。虚拟机栈也是线程私有的。

本地方法栈:由于Java最初的设计是基于c/c++的,因此有些底层的方法(如线程的start)需要调用c/c++的方法,此类方法称为native方法。与虚拟机栈类似,调用这类方法的时候就需要用到本地方法栈了。本地方法栈也是线程私有的。

:堆是JVM内存中最大的一块区域,new出来的对象实例和数组都需要在堆上分配内存。对空间是线程共享的。垃圾回收GC也是对堆内存的回收优化。Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。年轻代存储刚创建出来的对象,如果栈内存中没有引用指向该对象是,就会被垃圾回收。老年代存放的是15次都没被回收的对象实例。

方法区:与 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分。JDK1.8以后,方法去由元空间实现,因此并不在堆内存中。

关于JVM内存更详细的内容,请参考一文读懂JVM内存结构

Reference:

https://blog.youkuaiyun.com/m0_38075425/article/details/81627349

<think>嗯,用户想了解JVM的工作原理和基本概念。首先,我需要回顾一下JVM的基本结构和运行机制。根据用户提供的引用,JVM主要包括类加载子系统、运行时数据区、执行引擎和本地方法接口等部分。运行时数据区又分为方法区、堆、虚拟机栈、本地方法栈和程序计数器。用户之前提到的引用中提到了堆、栈、方法区等,所以需要详细说明这些部分。 首先,类加载过程可能需要分步骤解释,加载、链接、初始化这三个阶段。然后运行时数据区中的各个部分,比如堆是对象实例存储的地方,是GC的主要区域,分代结构(年轻代、老年代、永久代/元空间)。虚拟机栈和本地方法栈的区别,栈帧的结构,局部变量表、操作数栈这些内容。程序计数器的作用,线程私有,记录当前执行地址。 执行引擎部分需要解释解释器和JIT编译器,以及它们的协作。本地方法接口调用本地库。垃圾回收机制的基本原理,分代收集算法,不同区域的GC策略,比如年轻代的Minor GC,老年代的Major GC,还有Full GC。 可能还需要提到JVM的内存管理,比如如何分配内存,对象的创建和回收过程。此外,用户提供的引用中有提到永久存储区(方法区),在JDK8之后被元空间替代,使用本地内存,避免OOM。 用户的问题需要结构清晰的回答,所以应该分点讲解,每个部分简明扼要。同时,生成的相关问题要围绕JVM的结构、内存管理、类加载、垃圾回收等方面,确保覆盖用户可能感兴趣的后续问题。 在回答时要注意使用正确的中文术语,比如“栈帧”而不是“栈框架”,确保LaTeX格式正确,行内公式用$...$,独立公式用$$...$$。比如在讲解对象内存分配时,可能需要公式表示内存地址,但用户提供的引用中没有具体公式,所以可能不需要太多数学表达式,但如果有涉及的话要正确应用。 最后,检查引用是否正确标注,例如方法区的信息引用[4],堆的结构引用[3],栈的局部变量引用[2]等。确保每个引用标记自然,不干扰阅读。同时,生成的问题要有助于用户深入理解,比如JVM内存结构、类加载机制、垃圾回收优化、元空间与永久代的区别等。</think>### JVM工作原理与基本概念详解 --- #### **一、JVM核心组成** Java虚拟机JVM)是Java程序运行的核心环境,主要包含以下模块: 1. **类加载子系统** 负责加载`.class`文件,解析为JVM可识别的数据结构,过程分为三个阶段: - **加载**:查找并加载类的二进制数据。 - **链接**:验证、准备(分配静态变量内存)、解析(符号引用转直接引用)。 - **初始化**:执行类构造器`<clinit>()`方法[^4]。 2. **运行时数据区** JVM内存分为以下区域: - **方法区(Method Area)** 存储类元数据(如类名、方法代码)、静态变量、常量池等。JDK8后由“永久代”改为“**元空间(Metaspace)**”,直接使用本地内存[^4]。 - **堆(Heap)** 所有线程共享,存储对象实例和数组,是垃圾回收的主战场。采用分代结构: - 年轻代(Young Generation):存放新对象,分为Eden、Survivor区。 - 老年代(Old Generation):长期存活的对象。 $$ \text{堆内存} = \text{年轻代} + \text{老年代} $$ [^3] - **虚拟机栈(JVM Stack)** 线程私有,存储栈帧(Frame),每个栈帧对应一个方法调用,包含局部变量表、操作数栈等[^2]。 - **本地方法栈(Native Method Stack)** 为本地方法(如C/C++代码)服务。 - **程序计数器(PC Register)** 记录当前线程执行指令的位置。 3. **执行引擎** - **解释器**:逐行解释字节码。 - **即时编译器(JIT)**:将热点代码编译为机器码以提升性能。 - **垃圾回收器(GC)**:自动回收堆中无用对象。 4. **本地方法接口(JNI)** 调用操作系统或硬件相关的本地库。 --- #### **二、JVM运行流程** 1. **类加载**:将`.class`文件加载至方法区。 2. **内存分配**:对象实例分配在堆中,局部变量存储于虚拟机栈。 - 例如:`Object obj = new Object();`中,`obj`引用在栈中,对象实例在堆中[^2][^3]。 3. **执行字节码**:解释器或JIT将字节码转换为机器指令。 4. **垃圾回收**:GC自动回收堆中不可达对象,避免内存泄漏。 --- #### **三、垃圾回收(GC)原理** - **分代收集算法**: - **年轻代**:使用复制算法(Minor GC),Eden区满时触发。 - **老年代**:使用标记-清除或标记-整理算法(Major GC/Full GC)[^3]。 - **GC Roots**:通过根对象(如栈中局部变量、静态变量)判断对象存活性。 --- #### **四、关键特性** 1. **跨平台性**:字节码通过JVM适配不同操作系统。 2. **内存自动管理**:GC减少内存泄漏风险。 3. **多线程支持**:独立栈和程序计数器实现线程隔离。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值