Java跨平台运行
我们都知道Java语言一次编译到处运行,可以在windows上运行也可以在Liunx上运行,属于跨平台语言,Java其实就是依赖JVM实现的跨平台性,但是我们的JVM本身是不存在跨平台的。
通过Javac.exe编译.java原代码文件生成.class文件,然后再通过类加载器,将class文件加载到JVM中,交由JVM运行,最后输出结果。
我们来记一张简洁的运行图:
JVM的组成
JVM由4大部分组成:ClassLoader(类加载器),Runtime Data Area(运行时数据区域),Execution Engine(执行引擎),Native Interface(本地接口)。
- ClassLoader: 负责加载字节码文件,即是java编译后的.class文件。
- Runtime Date Area: 存放.class文件和分配内存。
- Native Interface: 负责调用本地接口,即是调用不同的语言接口给java使用。
- Execution Engine: 当.class字节码文件被加载后,会把指令和数据信息存放在内存中,此时执行引擎负责把这些命令解释给操作系统。
类加载器
1 类加载器的过程
- 加载:将字节码文件加载到内存
- 校验:检验字节码文件的正确性
- 准备:给类的静态变量分配内存,并赋予默认值
- 解析:类装载器装入类所引用的其他对象
- 初始化:对类的静态变量初始化为指定值,执行静态代码块
2 类加载的种类
-
启动类加载器:负责加载JRE的核心类库
-
扩展类加载器:负责加载JRE扩展的ext中的JAR类包
-
系统类加载器:负责加载ClassPath路径下的类包
-
用户自定义加载器:负责加载用户自定义路径下的类包
3 类加载机制
-
全盘负责委托机制:当类加载器加载一个类时,除非显示的是另一个加载器,该类锁依赖的和应用的类也由这个类加载器载入
-
双亲委派机制:当一个类加载器收到了类加载的请求的时候,他不会直接去加载目标类,首先委派父类加载器去寻找目标类,只有父加载器无法加载这个类的时候,才会在自己路径中查找并载入目标类。
Java虚拟机
采用的是双亲委派模式
,双亲委派机制的优势:避免类的重复加载,保护程序安全,防止核心API被随意篡改
运行时数据区域
运行时数据区域总共分为五部分:分别是Java虚拟机栈、本地方法栈、程序计数器、堆、方法区。
方法区: 负责存储.class文件,并且这块有一个运行常量池,就是存储一些变量或者常量信息的。
堆: 分配内存给对象,比如我们new的对象,就存在堆里面。
java虚拟机栈: 也可称为线程栈,每个线程独享的内存空间。
本地方法栈: 本地native方法独享的内存空间。
程序计数器: 记录线程执行的位置,方便线程切换后再次执行。
Java虚拟机栈
比如我们的main方法,调用sum函数,执行一个和的运算,此时我们的Java虚拟机栈就会为期分配栈帧内存区域。
public class MainDemo {
// 一个方法对应一块栈帧内存区域
public static Integer sum() {
int a = 1;
int b = 2;
return a + b;
}
// main方法也对应一块栈帧内存区域
public static void main(String[] args) {
Integer sum = sum();
System.out.println(sum);
}
}
复制代码
首先我们来看一下他的执行顺序是怎样的?首先是先调用main函数,随后再去调用sum函数,sum运算结束之后,再销毁栈内存,其次再返回到main函数,等到main结束之后,再销毁main的栈内存空间,这个过程main先执行了,却是最后退出,即栈帧内部的数据结构即是先进后出(FILO)。
我们将上面的demo进行反汇编,翻译成JVM虚拟机的汇编代码:
javap -c MainDemo.class > MainDemo.txt
复制代码
然后我们打开MainDemo.txt文件,里面就是一堆的JVM运行的汇编代码。
Compiled from "MainDemo.java"
public class com.dt.thread.java.MainDemo {
public com.dt.thread.java.MainDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static java.lang.Integer sum();
Code:
0: iconst_1
1: istore_0
2: iconst_2
3: istore_1
4: iload_0
5: iload_1
6: iadd
7: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
10: areturn
public static void main(java.lang.String[]);
Code:
0: invokestatic #3 // Method sum:()Ljava/lang/Integer;
3: astore_1
4: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
7: aload_1
8: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
11: return
}
复制代码
这一堆的代码,怎么来解读呢?其实Oracle官方有专门的指令码文档来解读。这里我们就来简单来解读一下
iconst_1 将int类型常量1压入栈
istore_0 将int类型值存入局部变量0
iconst_2 将int类型常量2压入栈
istore_1 将int类型值存入局部变量1
iload_0 从局部变量0中装载int类型值
iload_1 从局部变量1中装载int类型值
iadd 执行int类型的加法
invokestatic 调用类(静态)方法
areturn 从方法中返回引用类型的数据
我们栈帧内部存放的是一些局部变量,操作数栈,动态链表,方法出口。
这里当我们的栈中的局部变量是对象的时候,那么此时我们存储的是堆内存空间中对象的地址。
堆
Java虚拟机启动时创建,用于存放对象实例,几乎所有的对象包括常量池都在堆上分配内存,当对象无法在内存申请内存时,就会抛出OOM(OutOfMemoryError)异常。
所有的类都是在Eden Space(伊甸区)new出来的,当伊甸区空间用完了,程序又需要创建对象,JVM的垃圾回收器将对伊甸区进行垃圾回收(Minor GC),将伊甸区中不再被其它对象所引用的对象销毁,然后被引用的剩余对象移到幸存者0区,当0区空间不够用,再次进行GC,然后移动到1区,如果1区也满了,将会转移到0区,幸存者0区和1区中反复存在,经过多次GC,超过15次的存活对象,最后将会进入到老年区,如果老年区内存空间也满了,将会产生MajorGC,进行老年区的内存清理,如果老年代执行了MajorGC之后,任然无法进行对象的保存,也会产生OOM(OutOfMemoryError)异常。
总结
GC是垃圾回收机制,java中申请的内存可以被垃圾回收装置进行回收,GC可以一定程度的避免内存泄漏,但是会引入一些额外的开销。 GC中主要回收的是堆和方法区中的内存,栈中内存的释放要等到线程结束或者是栈帧被销毁,而程序计数器中存储的是地址不需要进行释放。