一、什么是JVM
使用过Java语言开发的童鞋,肯定知道,Java的编译就是依靠JVM来实现的,其实JVM就是Java虚拟机(Java Virtual Machine),JVM可以将Java语言解释成具体平台上的机器指令来执行,这也是Java为什么会跨平台的原因,因为无论是什么平台,高级语言都是需要编译成不同的目标代码后,才能被平台识别,而Java虚拟机的出现,使得Java语言在不同的平台上运行不需要重新编译,因为Java编译程序只需要在Java虚拟机上生成目标字节码即可,不需要再编译成其他语言。
说白了,JVM就是在Java编译器和OS(操作系统)平台之间的虚拟处理器,可以在上面执行Java字节码程序。
二、JVM的启动流程和内存结构
1.JVM的启动流程
下图展示了JVM的一个启动流程
首先我们的Java代码会被编译器(javac)编译成class文件,此时就要启动虚拟机对其进行解释,首先JVM需要进行一个装载操作,即读取路径下的jvm.cfg,它是一个配置文件,其中配置了JVM的各项参数,JVM的启动需要依靠里面的相关配置进行启动。然后根据配置找到JVM.dll,它是JVM的主要实现,然后就进行了初始化操作,执行findClass来找到需要解释的class文件,并寻找到mian方法运行。
JVM启动时,是由java命令/javaw命令来启动的。例如编译一个HelloWorld:
首先使用“javac”指令将Java代码编译成JVM可识别的class文件,然后使用“java”指令启动JVM,此时JVM经过装载和初始化之后,就会将class解释成操作系统所理解的字节码,然后找到main方法执行。
2.JVM的内存结构
说完了JVM的启动过程,再来看一下JVM的内存结构:
在我们执行Java程序的时候,需要使用到内存,而为了提高内存的使用效率,一般需要对运行Java的机器的内存空间进行分配,不同类型的内存空间去处理不同的数据类型。
在JVM中一般会将内存空间划分为五块,分别是“栈”、“堆”、“方法区”、“程序计数器”、“本地方法栈”,下面是对各个模块的解释:
(1)栈
①解释:
在栈中主要来存放Java代码中的“局部变量”等参数信息。栈是由一系列帧组成的。
在Java程序中,每一个方法被调用的时候都会创建一个栈帧,这个栈帧存储了局部变量表、操作栈、动态链接以及方法出口等信息。在每一个方法被调用直至执行完毕的过程就对应一个栈帧在虚拟机中从入栈到出栈的过程。
②异常定义:
此区域有两种异常:
StackOverFlowError:栈溢出。通过上面我们可以知道,每一次调用方法的时候都会存储方法的局部变量表、操作栈、动态链接以及方法出口等信息,压入栈内存中,并占据一定内存。如果一个递归方法比较深,此时栈内存就会存储很多栈帧,一旦Xss超出了栈的最大空间,就会报栈溢出的异常。
OutOfMemoryError(OOM):内存溢出。虚拟机的栈内存可以动态设置,我们可以给它设置的非常大,但是一旦拓展到无法申请足够的内存空间时,就会报内存溢出。
③局部变量表
通过上面我们可以知道,一个帧栈中含有方法的局部变量表、操作栈、动态链接以及方法出口等信息,其中局部变量表中存放了基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址),例如下面的代码:
public class StackTest{
//静态方法
public static int staticTest(int a,float b,Object c,double d){
return 0;
}
//实例方法
public int instanceTest(char a,short b,boolean b,long d,byte e){
return 0;
}
}
在上述代码中,静态方法有4个形参,实例方法有5个形参,对应的局部变量表如下:
如果是一个递归方法:
public static int staticTest(int a,float b,Object c,double d){
return staticTest(int a,float b,Object c,double d);
}
当该方法每次被调用的时候,都会创建一个栈帧,方法调用结束后,帧出栈,如下图:
④操作数栈
因为Java没有寄存器,所以所有参数传递只能使用操作数栈,下面是一个实例:
public static int add(int a,int b){
int c = 0;
c = a+b;
return c;
}
在该方法运行时,栈分别做了以下操作:
0:iconst_0 //0压栈
1:istore_2 //弹出int,存放于局部变量2
2:iload_0 //把局部变量0压栈
3:iload_1 //把局部变量1压栈
4:iadd //弹出2个变量,求和,结果压栈
5:istore_2//弹出结果,放于局部变量2
6:iload_2 //局部变量2压栈
7:ireturn //返回
我们调用该方法时,形参给a=100,b=98的时候,局部变量栈和操作数栈变化分别如下:
⑤栈上分配
Java栈还有一个优点,就是栈上分配,下面是一段c++的伪代码:
public void method(){
BcmBasicString* str=new BcmBasicString;
......
delete str;
}
在上面的代码中,我们要使用BcmBasicString类型的类,需要new一个实体对象,此时该对象被分配在堆内存中,而每一次在堆上分配的对象,再c++中使用完毕之后都要被手动清除,而很有可能出现分配了堆空间,但是忘记删除被分配的对象,此时就会发生内存泄露的情况。
而在c++中还有另外一种写法:
public void method(){
BcmBasicString str;
......
}
该写法直接声明了BcmBasicString,此时得到的BcmBasicString不是一个指针,而是一个对象引用,后面可以对这个对象进行操作,且操作完毕之后并不需要delete掉。 在这种情况下我们认为得到的对象是被分配在栈上的,因为这是一个局部变量。这就是c++的栈上分配,栈上分配的好处是永远不会出现内存泄漏问题。
而Java同c++一样,也有栈上分配的机制,见下图:
首先在alloc方法中分配了两个字节的字节数组,然后在主方法循环调用alloc方法1亿次,然后打印出运行花费的时间。在左边,分别有两种针对JVM的内存分配的配置,当使用第一种配置的时候,输出结果为5,而第二种配置输出结果是564,远远大于第一次运行的时间。
这里来说明一下,栈上分配,意思是方法内局部变量(未发生逃逸)生成的实例在栈上分配,不用在堆中分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。而逃逸是指当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸,一般情况返回对象、对全局变量的赋值一般都会发生逃逸。
调整参数时“+DOEscapeAnalysis”的意思就是,需要做“逃逸分析”,相反“-DOEscapeAnalysis”就是不做逃逸分析。
在不做逃逸分析的时候,JVM对未发生逃逸的变量的内存分配方式:
在方法体内,声明了一个局部变量,且该变量在方法执行生命周期内未发生逃逸(在方法体内,未将引用暴露给外面),按照JVM内存分配机制,首先会在堆里创建变量类的实例,然后将返回的对象指针压入调用栈,继续执行。也就是此时对未逃逸的变量也调用了堆内存。
在做逃逸分析的时候,JVM对未发生逃逸的变量的内存分配方式:
分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配,无需进入堆。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
这就说明,在第二次运行过程中,byte数组是在堆内存上分配的,并且分配当中由于内存的不足,触发了GC(垃圾收集)动作;而第一次没有触发GC,并且运行时间很短,这是因为第一次运行是在栈内存上运行的。
所以Java的栈上分配有以下特点:
- 小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
- 直接分配在栈上,可以自动回收,减轻GC压力
- 大对象或者逃逸对象无法进行栈上分配
⑥特点总结:
·线程私有,生命周期和线程相同
·栈由一系列帧组成(因此Java栈也叫做帧栈)
·帧保存一个方法的局部变量、操作数栈、常量池指针
·每一次方法调用创建一个帧,并压栈
(2)堆
①解释:
直白点,堆就是用来存放在Java代码中所有new出来的东西。即是所有的对象都保存在堆中。很重要的一点,Java的垃圾收集工具主要就是针对堆来进行管理的,采用分代收集算法,即堆是分代的,大致的堆空间如下图所示:
JVM的堆内存分为新生代(Young Generation)和旧生代(Old Generation)。新生代分为Eden区和Survivor区。Survivor区分为From Survivor和To Survivor。
有关GC回收的主要机制,我们在后面的章节详细阐述。
②特点总结:
·线程公有,被所有Java线程共享
·存储Java实例或者对象的地方
·是GC管理的主要区域
(3)方法区
①解释:
如果说栈是用来存放方法的定义信息的,则方法区是用来存储类结构信息的,包括常量池、静态变量、构造函数等。在JVM规范中将方法区描述为堆的一个逻辑部分。需要注意的是,方法区和堆一样,是被所有java线程共享的。
②特点总结:
·线程公有,被所有Java线程共享
·存储Java存储类结构信息
(4)程序计数器
①解释:
因为JVM是多线程执行的,线程之间进行轮流切换,为了保证线程切换后可以恢复到原有状态,需要一个独立的计数器来记录当前线程执行的内存地址,所以就有了程序计数器。需要注意的是,程序计数器是线程私有的。
②特点总结:
·线程私有,生命周期和线程相同
·用来记录当前线程执行的内存地址
(5)本地方法栈
①解释:
与java栈的作用相似,是为了JVM使用的native方法服务的。
②特点总结:
·线程私有,生命周期和线程相同
·为native方法服务
下一篇将介绍堆、栈、方法区的交互,以及内存分配机制。
转载请注明出处:https://blog.csdn.net/acmman/article/details/80315322