1、JVM从编译到执行
java程序的执行过程
JVM、JRE、JDK的关系
(1)一个Java程序,首先经过javac编译成.class 文件,然后JVM通过类加载器将.class文件加载到方法区,执行引擎会执行这些字节码。执行时,会翻译成操作系统相关的函数。JVM的作用就是将class文件翻译成操作系统能识别的机器码,调用操作系统函数执行相关命令。
执行过程如下:Java文件(我们写的代码)–>编译器–>class字节码–>JVM解释器–>机器码–>OS系统执行
JVM全称Java Virtual Machine,也就是我们所说的Java虚拟机。他们识别class后缀的文件,并且能够解析他们的指令,最终调用操作系统上的函数,完成我们代码写的具体功能。
(2)JDK(Java Development Kit):程序员写代码时需要编译代码、还需要调试代码,打包代码,有时候还需要反编译代码,所以我们会使用JDK,比如javac编译工具,java-jar打包工具,javap反编译工具。
JRE(Java Runtime Environment):它包含JVM,还提供了很多的类库,这些东西就是JRE提供的基础类库。是在JVM标准上实现了一大堆基础类库,就组成了java的运行环境。比如读取或者操作文件,连接网络,使用IO等等之类的。
JVM(java virtual Machine):只是一个翻译,把class文件翻译成操作系统识别的代码,是一个用c、c++语言实现的虚拟化的操作系统,类似于linux或者window的操作系统。
2、JVM的内存区域(JVM运行时数据区)
Java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
在JVM中,JVM内存主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈等。
根据线程的关系可以划分区域:
线程私有区域:一个线程拥有单独的一份内存区域
线程共享区域:被所有线程共享,且只有一份
直接内存:没有被虚拟机虚拟化的操作系统上的其他内存区域
3、线程私有内存区域
虚拟机栈
一个虚拟机栈(线程)由多个栈帧(方法)组成
栈帧和方法
/**
* JAVA 方法的运行与虚拟机栈
* @date 2021-05-29
*/
public class MethodAndStack {
public static void main(String[] args){
System.out.println("A方法正在被调用!");
A();
}
public static void A(){
System.out.println("B方法正在被调用!");
B();
}
public static void B(){
System.out.println("C方法正在被调用!");
C();
}
public static void C(){
System.out.println("C方法被调用中!");
}
}
程序说明:首先起一个主线程,在main方法中调用A方法,A方法中调用B方法,B方法中调用C方法。线程就会有一个对应的虚拟机栈,同时在执行每个方法的时候都会把方法打包成一个栈帧。先入栈,后出栈。
虚拟机栈就是用来存储线程方法中的数据的,每个方法对应着一个栈帧。
栈的设置和溢出
虚拟机栈和本地方法栈统称为栈区,参数-Xss1m
HotSpot版本中栈的大小是固定的,不支持扩展的,StackOverFlowError一般的方法调用是很难出现的,如果出现了可能会是无限递归。
虚拟机栈给我们的启示,方法的执行因为要打包成栈帧,所以天生要比实现同样功能的循环慢,所以树的遍历算法中,递归和非递归(循环来实现都有存在的意义)。递归代码简洁,非递归代码复杂但是速度较快。
OutofMemoryError,不能建立线程,JVM申请栈内存,机器没有足够的内存,同时要注意,栈区的空间JVM没有办法限制的,因为JVM在运行过程中会有线程不断的运行,没有办法限制。所以只能限制单个虚拟机栈的大小。
4、线程共享内存区域
线程共享区域=方法区+运行常量池+堆内存组成
方法区
方法区(Method Area)是可供各条线程共享的运行时内存区域。他存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
方法区是JVM对内存的“逻辑划分”,只是一个逻辑概念,在JDK1.7及之前将方法区称为永久代。在JDK1.8及以后使用元空间来实现方法区。
方法区内存溢出
/**
* 方法区导致的内存溢出:加载类文件和方法的
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*/
public class MethodAreaOutMemory {
public static void main(String[] args){
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestObject.class); //不断加载自身类文件
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
return arg3.invokeSuper(arg0, arg2);
}
});
enhancer.create();
}
}
public static class TestObject{
private double a = 34.53;
private Integer b = 999999;
}
}
运行时常量池(放在方法区中)
运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池的运行时表示形式。它包含了若干种的常量,从编译器可知的数值字面量到必须到运行时解析后才能获得的方法或字段引用。运行时常量池是方法区的一部分。运行时常量池相对于Class常量池的另一个重要特征是具备动态性。
Class常量池(静态常量池)
在class文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池(constant pool table),用来存放编译期间生成的各种字面量和符号引用。
堆空间
堆是JVM上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的,我们常说的垃圾回收,操作的对象就是堆,堆空间一般是程序启动时,就申请了,但是并不一定会全部使用,堆一般设置成可伸缩的。随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在java中,就叫做GC(Garbage Collection)。那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?
Java的对象可以分为基本数据类型和普通对象
对于普通对象来说,JVM会首相在堆上创建对象,然后在其他地方使用的其实是他的引用。
当你在方法体类声明了基本数据类型的对象,他就会直接在栈上分配。其他情况,都是在堆上分配。
堆大小参数:
-Xms:堆的最小值
-Xmx:堆的最大值
-Xmn:新生代的大小
-XX:NewSize:新生代最小值
-XX:MaxNewSize:新生代最大值
堆空间内存溢出
直接内存:初始化内存值小于代码的对象内存空间
/**
* -Xms30m -Xmx30m -XX:+PrintGCDetails
* 设置堆内存大小,并打印GC
* 堆内存溢出(直接溢出)
*/
public class HeapOom {
public static void main(String[] args){
String[] strings = new String[35*1000*1000];//35m数组堆
}
}
垃圾回收器直接就收集失败,因为初始化内存小于对象的内存大小。
代码写的不合理导致堆间接溢出
/**
*-Xms30m -Xmx30m -XX:+PrintGCDetails
* 间接造成堆内内存溢出
*/
public class HeapOom2 {
public static void main(String[] args) throws InterruptedException {
List<Object> list = new LinkedList<>();//list 当前虚拟机栈(局部变量表)中引用的对象
int i = 0;
while (true){
i++;
if(i%1000 == 0) Thread.sleep(10);
list.add(new Object());//不能回收
}
}
}
垃圾回收器尝试多次回收,但是代码写的不合理会导致对象内存一直暴增。出现代码内存溢出。