一、Java代码运行流程
在初学Java 语言时,我们最初编译执行一个Java 程序是通过dos窗口,执行javac <options><source file>命令将.java文件编译成.class文件(Java字节码文件),然后通过java <source file>命令运行Java程序。其实现如图:
Java程序经过一次编译后,将Java代码编译为字节码,然后在不同操作系统上依靠不同的JVM进行解释,最后再转换为不同平台的机器码,最终得到执行。
一个普通Java 程序的具体执行流程,参考https://www.cnblogs.com/leefreeman/p/7344460.html:
public class HelloWord { public static void main(String[] args) { System.out.println("Hello world!"); } }
以上程序从编译到运行,如图:
二、什么是JVM
JVM(Java Virtual Machine),即Java虚拟机,用来模拟通用的物理机。由于JVM能够将Java 字节码解释成平台的机器指令,所以JVM实现了Java语言的跨平台性。另,JDK、JRE、JVM之间的关系:JDK包含JRE,JRE包含JVM。
二、JVM基本结构
三、JVM内存空间
方法区
在Sun JDK中这块区域对应PermanetGeneration,即持久代。
方法区中存储每个类的信息:
- Classloader 引用
- 运行时常量池
- 数值型常量
- 字段引用
- 方法引用
- 属性
- 字段数据
- 针对每个字段的信息
- 字段名
- 类型
- 修饰符
- 属性(Attribute)
- 针对每个字段的信息
- 方法数据
- 每个方法
- 方法名
- 返回值类型
- 参数类型(按顺序)
- 修饰符
- 属性
- 每个方法
- 方法代码
- 每个方法
- 字节码
- 操作数栈大小
- 局部变量大小
- 局部变量表
- 异常表
- 每个异常处理器
- 开始点
- 结束点
- 异常处理代码的程序计数器(PC)偏移量
- 被捕获的异常类对应的常量池下标
- 每个方法
堆
堆被用来存储对象实例以及数组,可以认为Java中所有通过new 创建的对象的内存都在此分配。
由于堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new 对象的开销比较大。
Sun Hotspot JVM 为了提高对象内存的分配效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB 上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
栈
Java 栈是一块线程私有的内存空间,以栈帧为单位,执行进栈跟出栈操作(FILO)。
每个帧代表一个方法,Java 方法有两种返回方式,return 和抛出异常,两种方式都会导致该方法对应的帧出栈和释放内存。
帧组成:局部变量表、操作数栈、帧数据区。
PC寄存器
每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的饿地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
若thread执行Java方法,则PC保存下一条执行指令的地址。若thread执行native方法,则PC的值为undefined。
本地方法栈
依赖于本地方法的实现,如某个JVM实现的本地方法接口使用C连接模型,则本地方法栈就是C栈,可以说某线程在调用本地方法时,就进入了一个不受JVM限制的领域,也就是JVM可以利用本地方法来动态扩展本身。
本地方法一般来说可以(依赖JVM的实现)反过来调用JVM中的Java 方法。这种native方法调用 Java 会发生在栈(一般是Java栈)上;线程将离开本地方法栈,并在Java栈上开辟一个新的栈帧。
注:
1)线程共享:堆、方法区;
2)线程私有:栈、PC寄存器;
3)栈帧只存储指向堆中对象或数组的引用,与局部变量数组(每个栈帧中的)中的原始类型和引用类型不同,对象总是存储在堆上以便在方法结束时不会被移除。对象只能由垃圾回收器移除;