平时我们用的大多是Sun(现已被Oracle收购)JDK提供的JVM,但是JVM本身是一个规范,所以可以有多种实现,除了Hotspot外,还有诸如Oracle的JRockit、IBM的J9也都是非常有名的JVM。
一,JVM 结构
从上图可以看出,JVM 主要由类加载子系统, 运行时数据区(内存空间),执行引擎,以及本地方法接口等组成。其中运行数据区又包括 方法区,堆,Java栈,本地方法栈,PC寄存器。
在途中还可以看到,堆,方法区是所有线程共享,而Java栈,本地方法栈,PC寄存器是线程私有的。
1,类加载子系统 Class Loader
负责加载编译好的 .class 文件,并装入内存,使JVM 可以实例化或者以其他方法使用加载后的类。JVM的类加载子系统支持在运行时的动态加载。
类加载子系统 工作原理
类加载分为装载、链接、初始化三步。
装载
通过类的全限定名和ClassLoader加载类,主要是将指定的.class文件加载至JVM。当类被加载以后,在JVM内部就以“类的全限定名+ClassLoader实例ID”来标明类。
在内存中,ClassLoader实例和类的实例都位于堆中,类的其他信息位于方法区中。
装载过程采用了一种“双亲委派模型”的方式。
链接
链接的任务是把二进制的类型信息合并到JVM 运行状态中去。
初始化
初始化类中的静态变量,并执行类中的static 代码,构造函数。
JVM规范严格定义了何时需要对类进行初始化:
a、通过new关键字、反射、clone、反序列化机制实例化对象时
b、调用类的静态方法时。
c、使用类的静态字段或对其赋值时。
d、通过反射调用类的方法时。
e、初始化该类的子类时(初始化子类前其父类必须已经被初始化)。
f、JVM启动时被标记为启动类的类(简单理解为具有main方法的类)。
2,运行数据区(内存)
2.1Java 栈
Java栈是由许多栈帧组成的,
一个栈帧对应一个方法调用。当线程调用一个Java 方法时,虚拟机压入一个新的栈帧到该线程的Java栈中(Java栈是线程私有的),当该方法返回时,这个栈帧被弹出并抛弃。
Java栈的主要任务是存储方法参数,局部变量,中间运算结果,并提供部分其他模块工作需要的数据。
2.2本地方法栈
在Sun JDK中,本地方法栈和Java栈是同一个。
2.3方法区
线程共享。
方法区存储类元数据、常量、静态变量。方法区中对于每个类存储了以下数据:
a.类及其父类的全限定名(java.lang.Object没有父类)
b.类的类型(Class or Interface)
c.访问修饰符(public, abstract, final)
d.实现的接口的全限定名的列表
e.常量池
f.字段信息
g.方法信息
h.静态变量
i.ClassLoader引用
j.Class引用
Hotspot 将其称为永久代(Permanent Generation),但是这部分区域也需要GC。运行时常量池(Runtime Constant Pool)是方法区的一部分,用来存放符号引用,常量,直接引用等。JDK 1.7开始,字符串常量池从方法区中移出。
2.4堆 Heap
堆用于存储几乎所有的对象实例以及数组值。是线程共享的。他也被称为”GC堆”,因为堆是垃圾收集器的主要管理区域。
在堆内存的管理上,Sun JDK从1.2版本开始引入了分代管理的方式。
分为年轻代(Young Generation),老年代(Old Generation)。这种分代方式大大改善了垃圾收集的效率。
新生代:每次GC 时,这块区域的对象都有大批死去,因此称为新生代。新生代可以继续分为伊甸园(Eden)和两个存活区(survivor space) 。
老年代:每次GC 时,这块区域的对象存活率较高。
在这里看到一个形象的图片
最后堆中的需要等GC 来清理,栈中的直接销毁了。
3,执行引擎
执行引擎是 JVM 执行Java字节码的核心。
二,堆,栈,方法区
上面看了JVM的体系结构,现在对比了解JVM 内存中的3个区:堆heap,栈 stack,方法区 method。
堆区:
1. 全部是对象,每个对象都包含与之对应的 class 信息。(class的目的是得到操作指令)。不存放基本类型和数据引用,只存放对象本身.
栈区:
2. Java栈是由许多栈帧组成的。每个线程包含一个栈区。
3. 栈中只保存基础数据类型的对象和自定义对象的引用(对象都放在堆区)
4. 每个栈中的数据都是私有的,其他栈不能访问。
5. 栈分为3 个部分:基本类型变量区,执行环境上下文,操作指令区(存放操作指令)
方法区:
1. 线程共享,方法区包含所有的 class 和 static 变量
2. 方法区中包含的都是在整个程序中永远唯一的元素,如 class ,static 变量。
为了更清楚地搞明白发生在运行时数据区里的黑幕,我们来准备2个非常简单的小程序。
public class Demo { //运行时,JVM 把demo 的信息都放入方法区
public static void main(String[] args) { //mian 方法本身放入方法区
//test1 是引用,所以放到栈区,Sample 是自定义的对象,放到堆中
Sample s1=new Sample("测试1");
Sample s2=new Sample("测试2");
s1.printName();
s2.printName();
}
}
class Sample{ //运行时,JVM 把sample 的信息都放入方法区
/* 范例名称 */
// new Sample 实例后,name 引用放入到栈区里,name 对象放入堆里
private String name;
/* 构造方法 */
public Sample(String name){
this.name=name;
}
/* 输出 */
public void printName(){ // print 方法本身放入方法区里
System.out.println(name);
}
}
发出指令:java Demo ,系统收到指令,启动一个Java 虚拟机进程,这个进程首先从classpath中找到Demo.class 文件,读取文件中的二进制数据,然后把Demo 类的类信息存放到运行时数据区。这一过程就是Demo 类的加载过程。
接着,Java 虚拟机定位到方法区中 Demo类的Main() 方法,开始执行它的指令:
第一条:Sample test1=new Sample("测试1");
就是让虚拟机创建一个Sample 实例,并且使用 test1 引用这个实例。貌似小case一桩哦,就让我们来跟踪一下Java虚拟机,看看它究竟是怎么来执行这个任务的:
1、
Java虚拟机一看,不就是建立一个Sample实例吗,简单,于是就直奔方法区而去,先找到Sample类的类型信息再说。结果呢,嘿嘿,没找到@@,这会儿的方法区里还没有Sample类呢。可Java虚拟机也不是一根筋的笨蛋,于是,它发扬“自己动手,丰衣足食”的作风,立马加载了Sample类,把Sample类的类型信息存放在方法区里。
2、 好啦,资料找到了,下面就开始干活啦。Java虚拟机做的第一件事情就是在堆区中为一个新的Sample实例分配内存,
这个Sample实例持有着指向方法区的Sample类的类型信息的引用。这里所说的引用,实际上指的是Sample类的类型信息在方法区中的内存地址,其实,就是有点类似于C语言里的指针啦~~,而这个地址呢,就存放了在Sample实例的数据区里。
3、
在JAVA虚拟机进程中,每个线程都会拥有一个方法调用栈,用来跟踪线程运行中一系列的方法调用过程,栈中的每一个元素就被称为栈帧,每当线程调用一个方法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、局部变量和运算过程中的临时数据。OK,原理讲完了,就让我们来继续我们的跟踪行动!位于“=”前的Test1是一个在main()方法中定义的变量,可见,它是一个局部变量,因此,它被会添加到了执行* main() 方法的主线程*的 JAVA 方法调用栈中。而“=”将把这个test1变量指向堆区中的Sample实例,也就是说,它持有指向Sample实例的引用。
OK,到这里为止呢,JAVA虚拟机就完成了这个简单语句的执行任务。参考我们的行动向导图,我们终于初步摸清了JAVA虚拟机的一点点底细了
接下来,JAVA虚拟机将继续执行后续指令,在堆区里继续创建另一个Sample实例,然后依次执行它们的printName()方法。当JAVA虚拟机执行test1.printName()方法时,JAVA虚拟机根据局部变量test1持有的引用,定位到堆区中的Sample实例,再根据Sample实例持有的引用,定位到方法去中Sample类的类型信息,从而获得printName()方法的字节码,接着执行printName()方法包含的指令.——-printName 方法在方法区???不太理解(类的所有信息都存储在方法区中。)