JVM内存模型
根据线程公有和线程私有,JVM内存模型分为两部分。
-
程序计数器
每个线程都有一个程序计数器,存放着当前线程的下一条指令的地址,线程之间的程序计数器相互独立,互不干扰。
-
Java虚拟机栈
虚拟机栈描述JAVA方法执行时的内存模型,每个Java方法从调用到结束都伴随着一个栈帧在虚拟机栈中入栈出栈。
栈帧:局部变量表、操作数栈、动态链接、方法出口等信息。
-
本地方法栈
与虚拟机栈作用相同,描述Java方法执行时的内存模型,每个方法从调用到结束都伴随着一个栈帧入栈出栈。栈帧:局部变量表,操作数栈,动态连接,方法出口。不过本地方法是为Native方法服务的。
-
Java堆
对象和数组实例化的地址,也是垃圾回收的主要区域
-
本地方法区
-
已经被虚拟机加载的类的类信息
-
存储编译后的代码
-
常量
-
静态变量
-
运行时的常量池等信息
-
运行时的常量池:编译器生成的各种字面量和符号引用
-
类加载
虚拟机把描述的类从class文件->到内存中,并对类进行解析、初始化,最终形成Java虚拟机直接使用的类型 class文件->内存->虚拟机直接使用
-
加载
- 通过类的全限定名找到定义这个类的二进制字节流代码
- 将这个类的二进制字节流代表的静态存储结构--->方法区中运行时的数据结构->(被虚拟机加载的类的类信息)
- 在内存中生成这个类的java.lang.class对象,作为方法区中这个类的各种数据的入口
-
验证
- 文件格式的验证:保证输入的信息要能正确的解析并存储在方法区中,格式上符合Java的要求
- 元数据的验证:元数据对类的元数据的信息进行语义校验,保证类的元数据的信息符合Java规范
- 字节码的验证:验证程序是否是合法的,符合逻辑的,对数据流和控制流进行分析
- 符号引用的验证:发生在虚拟机将符号引用转化为直接引用的时候,在解析过程中发生。
- 通过字符串描述的全限定名能否找到对应的类。
-
准备
为类的变量分配内存,并设置初始值的阶段 仅仅指的是被static修饰的变量,而不是实例化的变量,实例化的变量会存储在Java堆中。
-
解析
编译时虚拟机并不知道引用类的地址,所以用符号引用代替,但是解析阶段,Java将常量池内的符号引用替换成直接引用的过程。
-
初始化
-
对于初始化阶段,《Java虚拟机规范》严格规定了只有以下六中情况才会触发类的初始化
- 在遇到 new、getstatic、putstatic 或者 invokestatic 这四条字节码指令时,如果没有进行过初始化,那么首先触发初始化。
- 在初始化类的时候,如果父类没有初始化,就要先初始化父类
- 在使用 java.lang.reflect 包的方法进行反射调用的时候。
- 虚拟机 会先初始化 main 方法这个类
new一个对象的过程
当虚拟机遇到new指令的时候
- 首先检查指令的参数能否在常量池中定位到一个符号引用,并且检测这个符号引用代表的类是否已经被加载、验证、准备、解析、初始化,如果没有就执行类加载的过程。
- 虚拟机会为新生对象在Java堆中分配内存
- 将Java堆中的刚分配的内存初始化为0,保证对象实例字段能够不赋值就直接使用
- 虚拟机为对象头进行必要的设置,gc代年龄,哈希值,对象是哪个类的实例,将这些信息存放在对象头中
- 对象的内存布局包括对象头,(gc代年龄,哈希值,对象引用类),实例数据,对象填充
- 完成以上过程,从虚拟机的角度来说一个新的对象就创建完毕了
对象分配内存的方式:
-
指针碰撞:假设 Java 堆中内存是规整的,所有使用过的内存放在一边,未使用的内存放在一边,中间放着一个指针,这个指针为分界指示器。那么为新对象分配内存空间就相当于是把指针向空闲的空间挪动对象大小相等的距离。例如Serial、ParNew收集器
-
空闲列表:Java堆的内存是不规整的。空闲列表维护了一个列表,这个列表记录了哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如CMS这种基于清除算法的收集器