上一篇文章我们谈到了jvm的类加载模式,几种常见的类加载器以及类加载器之间的代理模式。
我们已经知道,我们设计在eclipse上的程序是.Java文档,这类文档是字符型的,也就是我们人能够看懂的语言,然而计算机只认识0和1,如何将我们能够理解的语言转化为计算机能够认识的语言呢?这便是上一篇文章的主要内容。字符型文档通过Java编译器javac编译成.class文件,对于.class文件计算机将怎么样处理呢?通过加载,连接,初始化三个过程,便可以实现将Java程序.class文件加载到jvm中 。类加载有几种常见的类加载器,引导类加载器,继承类加载器,应用类加载器以及自定义类加载器,每一种加载器有其自身加载负责的范围,如何利用类加载器来加载类呢?这边是代理模式,常见的代理模式有双亲代理模式,从下向上查询,从上向下加载,这个过程是为了安全方面考虑。
当加载一个类完成之后,jvm是如何运行每一个程序的呢?这就和jvm的内存结构有关,因此内存分析的过程就是理解jvm运行程序的过程,那么,
(1)jvm的内存有那几个部分组成呢?他们分别有什么作用?
(2)jvm是如何运行程序的?结合具体的程序说明。
文章接下来部分来回答上面的两个问题。
(一)jvm的内存组成
jvm内存主要有三部分组成:堆,方法区,和栈
。
(1)程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来取下一条需要执行的字节码指令,分支、跳转、循环、异常处理、线程恢复等基础功能都需要这个计数器来完成
注:程序计数器是线程私有的,每条线程都会有一个独立的程序计数器
(2)Java栈
- 栈描述的是方法执行的内存模型,每个方法调用都会创建一个栈帧(存储局部变量,操作数和方法入口等),每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈或出栈。当线程调用java方法时,虚拟机压入一个新的栈帧到该线程的java栈中。当方法返回时,这个栈帧被从java栈中弹出并抛弃。
- jvn为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数,局部变量等)。
- 栈属于线程私有,不能实现线程之间的共享,
- 栈的存储特性是先进后出,后进先出,类似于子弹夹。
- 栈是有系统自动分配,并且是一个连续的内存空间。
栈帧的结构:
1) 局部变量表
局部变量表(Local Variable Table)是一组 变量值存储空间,用于存放 方法参数和方法内部定义的局部变量.局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位. 一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference[3]和returnAddress 8种类型,对于 64位的数据类型,虚拟机会以高位对齐的方式为其 分配两个连续的Slot空间(long double).
2) 操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈,当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作,例如,在做算术运算的时候通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。
举个例子:整数假发的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int类型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。
3) 方法返回地址
一个方法开始执行后,只有 两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为 正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中 遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为 异常完成出口(Abrupt Method Invocation Completion)。 一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的
4) 附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现,这里不再详述。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
(3)本地方法栈
(4)Java堆
- 堆是jvm中内存管理中最大的一块,其主要作用是:用于存储创建好的对象和数组,
- jvm只有一个堆,被所有线程所共享
- 堆是一个不连续的内存空间。
- 堆被划分为新生代和老年代。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。
- 新生代 : 程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。
- 老年代: 用于存放经过多次新生代GC任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:①.大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。②.大的数组对象,切数组中无引用外部对象。
- 老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
(5)方法区
- jvm只有一个方法区,且被所有线程共享。
- 方法区本质也是堆,知识用于存储类,常量相关的信息,
- 用于存放类中永远不变或者唯一的内容(类信息class对象,静态变量,字符串常量)
(二)jvm的运行过程
结合上面的分析,我们知道jvm是由堆,栈和方法区组成,栈描述的是方法执行的内存模型,每一个方法的调用都会创建栈帧;堆用于存储创建好的对象和数组,是一个不连续的存储空间;方法区本质上也是堆,用来存储类信息,静态变量,字符串常量等。那么。堆栈方法区具体在一个类的运行中存储那些信息呢?下面通过 例子 说明。
public class Student {
//静态的数据
String name;
int id; //学号
int age;
String gender;
int weight;
//动态的行为
public void study(){
System.out.println(name+"在学校");
}
public void sayHello(String sname){
System.out.println(name+"向"+sname+"說:你好!");
}
public class Test1 {
public static void main(String[] args) {
//通过类加载器Class Loader加载Student类。 加载后,在方法区中就有了Student类的信息!
Student s1 = new Student();
s1.name = "高琪";
s1.study();
s1.sayHello("馬士兵");
Student s2 = new Student();
s2.age = 18;
s2.name="老高";
}
}