初探JVM原理与结构
————————————————
版权声明:本文为CSDN博主「Deegue」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zyzzxycj/article/details/89848442
前言
这篇文章是JVM的扫盲篇,通俗易懂,对不清楚
- ClassLoader、
- Garbage Collection,
- 堆(heap)、
- 栈(stack)和
- 方法区(method)
之间的关系,可以有个大致的了解。
Garbage
英
/ˈɡɑːbɪdʒ/
n.
<美>垃圾,废物;<美>垃圾箱;<非正式>废话,无聊的东西;无用信息
HelloWorld 及流程
在还是小白阶段的时候,我们都会写一个HelloWorld.java,然后执行javac 获得HelloWorld.class
public class HelloWorld {
public static void main(String[] args) {
System.out.println("HelloWorld");
}
}
在执行java HelloWorld的时候,
- Java命令首先启动虚拟机进程,
- 虚拟机进程成功启动后,读取参数“HelloWorld”,把他作为初始类加载到内存,对这个类进行初始化和动态链接,
- 然后从这个类的main方法开始执行。
也就是说我们的.class文件不是直接被系统加载后直接在cpu上执行的,而是被一个叫做虚拟机的进程托管的。
- 首先必须虚拟机进程启动就绪,
- 然后由虚拟机中的类加载器加载必要的class文件,包括jdk中的基础类(如String和Object等),
- 然后由虚拟机进程解释class字节码指令,把这些字节码指令**翻译成本机cpu能够识别的指令,**才能在cpu上运行。
根据上图表达的内容,我们编译之后的
class文件是作为Java虚拟机的原料被输入到Java虚拟机的内部的,
那么具体由谁来做这一部分工作呢?其实在Java虚拟机内部,有一个叫做类加载器的子系统,这个子系统用来在运行时根据需要加载类。
注意上面一句话中的“根据需要”四个字。在Java虚拟机执行过程中,只有他需要一个类的时候,才会调用类加载器来加载这个类,并不会在开始运行时加载所有的类。就像一个人,只有饿的时候才去吃饭,而不是一次把一年的饭都吃到肚子里。一般来说,
- 虚拟机加载类的时机,在第一次使用一个新的类的时候。
从这个层面上来看,在执行一个所谓的java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程,而不是我们写的一个个的class文件。这个叫做虚拟机的进程处理一些底层的操作,比如内存的分配和释放等等。
我们编写的class文件只是虚拟机进程执行时需要的“原料”。这些“原料”在运行时被加载到虚拟机中,被虚拟机解释执行,以控制虚拟机实现我们java代码中所定义的一些相对高层的操作,比如创建一个文件等,
- 可以将class文件中的信息看做对虚拟机的控制信息,也就是一种虚拟指令。
我们知道,Java虚拟机会进行自动内存管理。具体说来就是自动释放没有用的对象,而不需要程序员编写代码来释放分配的内存。这部分工作由垃圾收集子系统负责。
Java虚拟机 三个子系统
从上面的论述可以知道, 一个Java虚拟机实例在运行过程中有三个子系统来保障它的正常运行,分别是
- 类加载器子系统,
- 执行引擎子系统和
- 垃圾收集子系统。
如下图所示:
虚拟机的运行,必须加载class文件,并且执行class文件中的字节码指令。它做这么多事情,必须需要自己的空间。就像人吃下去的东西首先要放在胃中。虚拟机也需要空间来存放个中数据。首先,
- 加载的字节码,需要一个单独的内存空间来存放;
- 一个线程的执行,也需要内存空间来维护方法的调用关系,存放方法中的数据和中间计算结果;在执行的过程中,无法避免的要创建对象,
- 创建的对象需要一个专门的内存空间来存放。
运行时内存区
虚拟机的运行时内存区大概可以分成下图所示的几个部分:
- Class 文件:方法区
- 线程执行数据:栈
- 对象存储区:堆
- 复杂的划分
运行时数据区
- Java栈
- 本地方法栈
- 堆
- 新生代、老年代和永久代。
- 新生代:8:1:1 Eden, Survivor1, Survivor2 幸存1区
- 新生代、老年代和永久代。
- 方法区
- 程序计数器
堆、栈、方法区概念
堆区:
- 存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
- jvm只有一个堆区(heap),被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈区:
-
每个线程包含一个栈区,栈中只保存基础数据类型的对象的引用和自定义对象的引用(不是对象),对象都存放在堆区中。
-
每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
-
栈分为3个部分:
- 基本类型变量区、
- 执行环境上下文、
- 操作指令区(存放操作指令)。
方法区:
-
又叫静态区,跟堆一样,被所有的线程共享。
- 方法区包含所有的class和static变量。
-
方法区中包含的都是在整个程序中永远唯一的元素,
- 如class,static变量。
具体解释
为了更清楚地搞明白发生在运行时数据区里的黑幕,我们来准备2个小道具(2个非常简单的小程序)。
AppMain.java
//运行时, jvm 把appmain的信息都放入方法区
public class AppMain {
//main 方法本身放入方法区。
public static void main(String[] args) {
//test1是引用,所以放到栈区里,
//Sample是自定义对象应该放到堆里面
Sample test1 = new Sample(" 测试1 ");
Sample test2 = new Sample(" 测试2 ");
test1.printName();
test2.printName();
}
}
Sample.java
//运行时, jvm 把appmain的信息都放入方法区
public class Sample {
/**
* 范例名称
* new Sample实例后, name 引用放入栈区里, name 对象放入堆里
*/
private name;
/**
* 构造方法
*/
public Sample(String name) {
this.name = name;
}
/**
* 输出
* print方法本身放入 方法区里。
*/
public void printName() {
System.out.println(name);
}
}
图解和总结
系统收到了我们发出的指令,
- 启动了一个Java虚拟机进程,这个进程首先从classpath中找到AppMain.class文件,读取这个文件中的二进制数据,
- 然后把Appmain类的 类信息存 放到 运行时数据区的方法区中。这一过程称为AppMain类的加载过程。
- 接着,Java虚拟机定位到方法区中AppMain类的Main()方法的字节码,开始执行它的指令。这个main()方法的第一条语句就是:
Sample test1=new Sample(“测试1”);
语句很简单啦,就是让java虚拟机创建一个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虚拟机的一点点底细了,COOL!
接下来,JAVA虚拟机将继续执行后续指令,
- 在堆区里继续创建另一个Sample实例,然后依次执行它们的printName()方法。当JAVA虚拟机执行test1.printName()方法时,
- JAVA虚拟机根据局部变量test1持有的引用,定位到堆区中的Sample实例,
- 再根据Sample实例持有的引用,定位到方法去中Sample类的类型信息,
- 从而获得printName()方法的字节码,接着执行printName()方法包含的指令。
总结:
- 虚拟机并不神秘,在操作系统的角度看来,它只是一个普通进程。
- 这个叫做虚拟机的进程比较特殊,它能够加载我们编写的class文件。如果把JVM比作一个人,那么class文件就是我们吃的食物。
- 加载class文件的是一个叫做类加载器的子系统。就好比我们的嘴巴,把食物吃到肚子里。
- 虚拟机中的执行引擎用来执行class文件中的字节码指令。就好比我们的肠胃,对吃进去的食物进行消化。
- 虚拟机在执行过程中,要分配内存创建对象。当这些对象过时无用了,必须要自动清理这些无用的对象。
- 清理对象回收内存的任务由垃圾收集器负责。就好比人吃进去的食物,在消化之后,必须把废物排出体外,腾出空间可以在下次饿的时候吃饭并消化食物。