Java程序运行原理(一)- JVM内存区域
我们先简要描述下,一般的高级语言是如何工作的:
我们在编辑器或者IDE里,根据每种语言的语法规则输入源代码(源代码一般比较容易理解),这些代码是以二进制存储在我们的硬盘中,编辑器/IDE读取这些二进制还原成我们能读懂的英文。当运行程序的时候,有个源代码文件转化成可执行的二进制文件的过程(这个过程通常由编译器完成)。有些编译器直接将源代码编译成机器码,载入内存后CPU直接运行。而机器码的格式是跟具体的CPU架构相关,Intel的CPU机器码对于ARM来说,ARM的CPU来说是无法理解的,因此,同样的源代码需要根据不同的硬件进行特定的编译。高级语言到低级语言的桥梁就是编译器。我们写好源代码,编译器将源码编译成可执行的机器码,然后CPU读取机器码,执行程序。
名词解释
名词 | 解释 |
---|---|
编译器 | 编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。从由高级语言生成的低级语言代码重新生成高级语言代码的又被叫做反编译器。 |
机器码 | 计算机直接使用的程序语言,其语句就是机器指令码。机器指令码是用于指挥计算机应做的操作和操作数地址的一组二进制数。 |
JVM | JAVA虚拟机,用于提供Java解释环境,形成机器可执行的文件。不同平台的JVM各有不同,但均提供相同的接口。 |
JRE | JAVA运行环境 |
JDK | 核心开发包,提供Java编译器、运行相关的环境、工具和类库。 |
Class字节码 | 一个二进制文件,包含了JAVA程序执行的字节码,包含的信息有版本、访问标志、常量池、当前类、超级类、接口、字段、方法、属性等,中间没有任何分隔符,文件开头有一个特殊标志,用16进制表示为0xcafebabe。Class文件反编译后,可通过“JVM指令码表”查看指令,这些指令运行时都保存在方法区。 |
一般都有一个表格对解释型和编译型语言进行对比。
类型 | 原理 | 优点 | 缺点 |
---|---|---|---|
编译型语言 | 通过专有编译器,将高级语言一次性编译成可被平台执行的机器码 | 编译后脱离开发环境可独立运行,效率高 | 对编译器依赖严重 |
解释型语言 | 由专门的解释器,将源程序解释成特定平台可执行的指令 | 跨平台性好,通过不同平台的解释器,将其解释成各平台可识别指令 | 编译和解释一起执行,效率较低 |
Java相关概念
Java语言则在遵循上述过程(源代码–>机器码)中,特殊在它并没有直接转化为机器码,而是转化成一种中间格式,即:Java源代码(.java)经过java编译器(javac.exe)编译之后,成为字节码(.class)。字节码再经过Java虚拟机转化成特定CPU架构的机器码。也正因此,java才有跨平台的概念。在windows平台上编译好的字节码,copy到Linux平台后,经过为Linux设计的Java虚拟机解释后即可执行。经过编译后的class文件,不依赖于平台环境,在各种操作系统均可运行,跨平台这一特征,是通过字节码和JVM来实现的。因此,想搞清楚Java程序到底是如何运行的,重点在于弄明白字节码是如何被转化成跟CPU架构相关的机器码然后被执行的。也就是要理解JVM到底是如何工作的。
虚拟机,通俗理解就是通过软件模拟出具有完整硬件系统(包括寄存器、存储器等)功能的、运行在一个完全隔离环境中的完整计算机系统。在计算机中创建虚拟机时,需要将实体机的部分硬盘和内存容量作为虚拟机的硬盘和内存容量。每个虚拟机都有独立的CMOS、硬盘和操作系统,可以像使用实体机一样对虚拟机进行操作,最终的工作由原来机器的CPU完成。宽泛地说,它就是执行字节码的一整个环境。
一、JAVA程序的运行原理
-
源文件(.java源代码)通过Java编译器编译成.class字节码文件。
-
通过JVM中的解释器将字节码文件生成可执行的机器码文件,运行。
-
将编译后的程序加载到方法区,存储类信息。
-
运行时,JVM创建线程来执行代码,在虚拟机栈和程序计数器分配独占的空间。根据方法区里的指令码,在虚拟机栈对线程进行操作,程序计数器保存线程代码执行到哪个位置。
二、JVM内存区域
JVM运行时数据区:包含线程共享部分和线程独占部分。
线程独占: 每个线程都会有它独立的空间,随线程的生命周期而创建和销毁,包含虚拟机栈、本地方法栈、程序计数器。
线程共享: 所有线程都能访问这块内存数据,随虚拟机或GC而创建和销毁,包含方法区和堆内存
### 方法区
- 方法区是各个线程共享的内存区域
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据
- 在JVM规范中,这是一个逻辑区, 但它却有一个别名叫Non-Heap, 目的应该是与Java堆区分开来
- Oracle的Hotspot虚拟机在Java7中方法区放在‘永久代’(Permanent Generation),
Java8放在元数据空间, 并且通过GC机制对这个区域进行管理。目前有三大Java虚拟机:HotSpot,Oracle JRockit,IBM J9 - 运行时常量池是方法区的一部分
Java堆
- Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建
- 存放对象的实例
- 垃圾收集器的主要管理区域
- Java堆还可以细分为: 新生代和老年代, 新生代又可以细分为Eden 空间, From Survivor空间和ToSurvivor空间
- 空间满了会抛OutOfMemoryError
Java虚拟机栈
- Java虚拟机栈是虚拟机执行Java代码的栈,线程私有的, 它的生命周期与线程相同。
- Java虚拟机栈描述的是Java方法执行的内存模型,线程栈由多个栈帧(Stack Frame)组成,一个线程会执行一个或多个方法,一个方法对应一个栈帧。栈帧内容包括:局部变量表、操作数栈、动态链接、方法出口、附加信息等。
- 栈内存默认最大是1M, 超出则抛出StackOverFlowError
本地方法栈
- 本地方法栈与虚拟机栈的功能类似, 虚拟机栈是为虚拟机执行Java方法而准备的, 本地方法栈是为虚拟机使用Native本地方法而准备的。
- Hotspot虚拟机中虚拟机栈与本地方法栈的实现方式一样, 超出大小后也会抛StackOverFlowError 程序计数器。
程序计数器
- 程序计数器是线程私有的一块较小的内存空间
- 记录当前线程执行的字节码位置, 存储的是字节码指令地址, 如果执行Native方法, 则计数器为空
- CPU同一时间, 只会执行一条线程的指令,JVM多线程会轮流切换并分配CPU的执行时间。线程切换后,需要通过程序计数器来恢复正确的执行位置
示例
环境
操作系统:WIN7
Java版本信息:
public class Demo {
public static void main(String[] args) {
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a + b);
}
}
- 编译:javac Demo.java
生成Demo.class文件 - 执行:java Demo1
输出执行结果 55 - 反编译 javap –c Demo1.class
版本号规则:JDK 5,6,7,8分别对应49,50,51,52
Compiled from "Demo.java"
public class Demo {
public Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: return
public static void main(java.lang.String[]);
Code:
0: sipush 500
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 50
13: istore 4
15: getstatic #2 // Field java/lang/System.out:Ljava/
io/PrintStream;
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 // Method java/io/PrintStream.printl
n:(I)V
25: return
}
JVM怎么实现类加载后续再分析。