关于Java的内存模型,先上图
总的来说包括三部分:类加载、运行时数据区、执行引擎。一个类文件要想成为可执行文件,离不开上面三大部分的支持,接下来咱们逐一来了解。
一、类加载子系统
类加载子系统主要负责加载由java编译器生成的Class文件,Class可以理解为是一个类模板,经过类加载后,便可以按照这个模板创建实例对象。类加载一般需要经过三个阶段:加载阶段--》链接阶段--》初始化阶段。
1. Loading
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口
加载来源有:网络获取、jar包、war包、动态代理、jsp应用
类的加载是由类加载器完成的,根据需要可以选择不同的加载器加载,java中有四类加载器:
- 引导类加载器(BootStrap ClassLoader)
负责加载核心库类(如包名为java、javax、sun开头的类),使用C/C++实现,不继承ClassLoader,是最顶层加载器,没有父类加载器,会加载扩展类加载器合应用程序加载器,并指定自己为其父类加载器。 - 扩展类加载器(Extension ClassLoader)
负责加载扩展类,使用java实现,派生于ClassLoader。 - 应用程序类加载器(System ClassLoader)
负责加载环境变量、系统属性等,使用java使用,派生于ClassLoader。 - 用户自定义类加载器
除了上面三种加载器,在必要时,开发者还可以自定义类加载器。自定义类加载器作用有:隔离加载类、修改类加载的方式、扩展加载源、防止源码泄漏。
了解了类加载的过程和类加载器之后,还有一个比较重要的类加载机制,即双亲委派机制。 原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,口请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若孺类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
这种机制,有如下几点优势:
- 可以防止重复加载,只有一个类加载器加载。
- 保护程序安全,避免类加载器加载混乱造成程序终止。
- 防止核心API被修改,核心API由指定的父类加载器加载
2.Linking
链接阶段包括三个阶段:
- 验证(Verify):
- 目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 准备(Prepare):
- 为类变量分配内存并且设置该类变量的默认初始值,即零值。
- 这里不包含用final修饰的staic,因为final在编译的时候就会分配了,准备阶段会显式初始化;
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
- 解析(Resolve):
- 将常量池内的符号引用转换为直接引用的过程。
- 事实上,解析操作往往会伴随着JvM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、cONSTANT_Fieldref_info、cONSTANT_Methodref_info等.
3.Initialization
- 初始化阶段就是执行类构造器方法 < clinit >()的过程。
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
- < clinit >()不同于类的构造器。(关联:构造器是虚拟机视角下的< init >())
- 若该类具有父类,JVM会保证子类的< clinit >()执行前,父类的< clinit >()已经执行完毕。
- 虚拟机必须保证一个类的< clinit > ()方法在多线程下被同步加锁。
二、运行时数据区
1.程序计数器
程序计数器是物理PC寄存器的一种抽象模拟,在java虚拟机中,理解程序计数器,只需要掌握下面两个问题即可。
使用PC寄存器存储字节码指令地址有什么用?
为什么使用PC寄存器记录当前线程的执行地址?
要回答这两个问题,就需要理解在操作系统中CPU切换线程的机制,CPU在运行程序时会不停的切换线程,在线程切换期间需要记录当前运行线程的地址,这个地址就保存在PC计数器中。
2.虚拟机栈
栈是一种先进后出的数据结构,在Java虚拟机中,每个线程对应一个栈、栈中有多个栈帧,栈帧对应方法的一次调用。在Java程序运行时,虚拟机栈保存着方法的局部变量、部分结构等,参与方法的调用和返回。
3.堆
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
- 堆内存的大小是可以调节的。
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( ThreadLocal Allocation Buffer,TLAB),所以不是全都共享
堆内存进一步划分如下图
Java8之后,永久代被元空间取代