作为java开发人员,深入理解jvm的执行原理必不可少,那么一个类从编译到执行期间,在jvm中都发生了些什么呢?本文将一探究竟。首先就得从类的加载开始说起了。类加载说白了,jvm在运行代码前要先获取到可执行的字节码文件,类加载就是指的这个过程。当然中间还有很多细节,首先我们jvm运行时的几个重要内存区域。
- 栈区:也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的数据结构 。在jvm中,每个线程都有自己的栈,栈与栈之间相互独立,互不影响,线程中的每个方法就是一个栈帧。在调用方法时,jvm会为方法创建一个栈帧并推入对应线程栈的栈顶,在方法结束调用时,栈顶的栈帧弹出。
- 堆区:用于存放java的对象,这里包括运行时产生的实例对象,也包括类加载时生成的类对象。这个区域是jvm垃圾回收的主战场。jvm针对此区域的GC做了很多事情,例如分代算法,根节点算法。其中垃圾收集器就有4种,在很大程度上减轻了开发人员管理内存的工作。还要特别注意一点的是,此区域是所有线程共享的,当多个java栈引用堆中的同一数据时,就需注意线程安全了。
- 方法区:方法区在jvm是一块非常重要的区域,同时也被所有线程共享,在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。 此区域还包含一个很重要的区域就是常量池,java中String类型的数据就保存在此区域中,在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代” ,不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。
- 程序计数器:也有称为PC寄存器 ,当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。
- 本地方法栈:本地方法栈与Java栈的作用和原理非常相似 ,只不过本地方法栈是为调用系统底层方法(Native Method )而服务的。
1.类的生命周期
当我们编写一个java的源代码后,经过编译会生成一个后缀名为.class的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在java虚拟机中运行,java类的生命周期就是指一个class文件从加载到卸载的全过程。 类的生命周期主要包括加载、连接、初始化、使用、和卸载五个阶段 。而其中动态代理实现的原理,就是利用了第一个阶段和第二个阶段,即类的加载和连接。首先来了解一下,类加载和连接各做了什么事情
2.类加载
在java中我们常会接触到类加载一词,类加载又可细分为三个阶段;加载,链接,初始化。下面来细说一下这三个阶段各自都干了些什么事情
2.1 加载
类的加载其实就做了一件事情,就是把编译器生成.class文件读取到jvm虚拟机中,说白了就是从磁盘读取文件到内存中。在写程序时,我们几乎不关心这个过程,因为这些都是jvm帮我们做好的,我们只管使用就行了。 如果不细心想想,仔细琢磨。可能这个阶段就这么过去了,无非就是加载个class文件嘛。我刚开始接触时也把这个过程想简单了,其实不然,这里面大有文章。这个过程中有三个重要阶段。一,什么时候加载class文件;二,class文件从哪里加载;三,class文件怎么加载。
除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
2.1.1 什么时候加载class文件
与C/C++不同,C/C++在编译期就将所有类加载并找到他们的直接引用,不论是否使用到 。一个应用程序是由N多个类组成,jvm启动时。并不是一次把所有的类全部加载后再 运行,它总是先把保证程序运行的基类(例如Object)一次性加载到jvm中 。而其他的类等到用到时才会去加载,这样做的好处是节省了内存开销。运行时加载虽然牺牲了一点性能,但得到了更广的应用面,因为jvm在设计之初是为嵌入式系统设计的,内存尤为宝贵。 对于加载的时机,各个虚拟机的做法并不一样。但是有一个原则,就是当jvm“预期”到一个类将要被使用时,就会在使用它之前对这个类进行加载 ,在一段代码中出现了一个类的名字,jvm在执行这段代码之前并不能确定这个类是否会被使用到,于是,有些jvm会在执行前就加载这个类,而有些则在真正需要用的时候才会去加载它,这取决于具体的jvm实现。我们常用的hotspot虚拟机是采用的后者 。因此简单概括来说就是一句话,jvm是在需要用到这个类时就会去加载这个类的class文件。
2.1.2 class文件从哪里加载
通常的理解,class文件的获取路径就是本地文件夹中。但jvm的能力远非如此。下面总结一下class文件的加载路径有哪些
- 从网络中下载.class文件
- 从本地路径中直接加载
- 从zip,jar包中加载.class文件
- 从专有数据库中提取.class文件
- 根据一定的规则实时生成.class文件
而本文要所说的动态代理就是使用第5种方式,动态生成class文件实现的。例如cglib包。在spring中aop实现的原理也是通过cglib包。
2.1.3 class文件怎么加载
jvm装载类的方式分为两种,即隐式装载 和显式装载
- 隐式装载:通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中
- 显式装载:通过class.forname()等方法,此方法通过反射方式,显式加载需要的类
在jvm中类加载器有三个,对应Java的三种类的加载,1.系统类 2.扩展类 3.由程序员自定义的类
- Bootstrap Loader :负责加载系统类 ,这个类加载通过C/C++实现
- ExtClassLoader :负责加载扩展类 ,java实现
- AppClassLoader :负责加载应用类,即程序员自定义的类,java实现
这三个类加载器构成双亲委派模型,保证Java程序安全稳定运行 。
3.链接
加载阶段结束后,便会进入链接阶段。但有一点需要注意,就是有时连接阶段并不会等加载阶段完全完成之后才开始,而是交叉进行,可能一个类只加载了一部分之后,连接阶段就已经开始了。但是这两个阶段总的开始时间和完成时间总是固定的:加载阶段总是在连接阶段之前开始,连接阶段总是在加载阶段完成之后完成。 这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,此阶段又可以细分为三个步骤:验证、准备和解析。
3.1 验证
当一个类被加载之后,必须要验证一下这个类是否合法,确保类文件遵从Java类文件的固定格式。 语义检查,字节码验证,确保字节码流可以被Java虚拟机安全地执行 。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
3.2 准备
准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:
- 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。
- 引用类型的默认值为null。
- 常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 1,则准备阶段中a的初值就是1。
3.3 解析
这一阶段的任务就是把常量池中的符号引用转换为直接引用 ,举个例子
1 | public void doSomeThing() { |
包含了一个对Cat类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。在解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run()方法在方法区内的内存位置,这个指针就是直接引用。
4.静态链接与动态链接
java主要包括静态方法和私有方法两大类。前者与类型直接关联,后者在外部不可被访问,这两种方法的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们适合在类加载阶段进行解析。
打赏一个呗