JVM内存模型
1.JVM(Java Virtual Machine)
JVM(Java虚拟机)本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是一次编译,多次运行。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
其中对于不同的的操作系统的JVM都是一样的,但是底层和操作系统交互的部分是不一样,JRE中包含这些屏蔽底层操作系统差异的模块。
2.JMM(Java Memroy Model)
Java内存模型根据不同的角度可以分为如下两种:
-
用于定义(所有线程的共享变量, 不能是局部变量)变量的访问规则 (线程的共享的角度)
-
Java运行时的内存模型(JVM运行时内存区域)
2.1线程共享角度下的JMM
按照线程共享的角度将JMM分为两块区域
- 主内存:真实存放数据(变量),线程共享。
- 工作内存:存放主内存中数据的副本,线程私有。
如下图所示:
-
每个线程都只能访问自己工作内存中的数据,不能访问主内存和其它线程的中的数据
-
不同线程之间的数据交互可以通过主内存间接完成。
线程和主内存之间的数据交互
单个线程的和主内存中的数据交互,如下图所示:
1.Lock:将主内存中的变量,表示为一条线程的独占状态
2.Read:将主内存中的变量,读取到工作内存中
3.Load:将2中读取的变量拷贝到变量副本中
4.Use:把工作内存中的变量副本,传递给线程去使用
5.Assign:把线程正在使用的变量,传递给工作内存中的变量副本中
6.Store:将工作内存中变量副本的值,传递到主内存中
7.Write:将变量副本作为一个主内存中的变量进行存储
8.Unlock:解决线程的独占状态
JVM要求以上八个步骤必须是原子性的,但是JVM对于double、long等64位(8个字节)数据类型有一些非原子性协议。也就意味着在写入double、long等64位数据的时候可能会出现只写入一半的情况,可以通过如下两个方案解决:
- 使用商用JVM
- 使用Volatile关键字修饰变量
2.2JVM运行时内存模型
JVM在运行时,将内存分为如下图所示的五个区域
程序计数器
程序计数器又称行号指示器,指向当前线程执行的字节码文件的指令地址。
1.一般情况下,程序计数器指向的值是当前线程执行字节码文件的行号,但是如果程序执行了native方法,那么指向的值为undefine。
2.程序计数器是JVM内存中唯一一个不会出现内存溢出的区域。
PS:在Java有有一个保留的关键字goto,其本质就是改变程序计数器的行号。
虚拟机栈
定义: 描述方法执行的内存模型
- 在方法执行的同时,会在虚拟机栈中为该方法创建一个栈帧
- 栈帧:包含方法的局部变量表(方法中用到的局部变量)、操作数栈(变量的值)、动态链接(多态,引用与指向地址关系)、方法出口等信息。
当方法执行太多或者编写递归方法没有正确结束,虚拟机栈中存放的栈帧数量超过了内存大小,虚拟机栈就会内存溢出。
public static void main(String[] args) {
main(new String[]{"a", "b", "c"});
}
本地方法栈
结构基本上和虚拟机栈类似,不同的是,虚拟机栈存放的是JDK自带和自己编写的Java方法,本地方法栈存放的是底层的操作系统提供的方法。
堆
- 用于存放对象实例(对象、数组)
- 堆是JVM中内存最大的一块区域,在JVM启动的时候就已经创建完毕
- GC(垃圾回收器)主要管理的区域
- 堆本身是线程共享的,但是线程可以在堆中划分出多个线程私有的缓冲区(IO流)
- 允许内存空间物理上的不连续,逻辑上连续(链表),这样会直接导致内存外碎片的产生
- 堆中空间可以分为:新生代 和 老生代。大小比例 新生代 : 老生代 = 1:2
- 新生代中分为: Eden、s0、s1,比例为8:1:1
- 新生代的使用率一般在90%,在使用的时候s0和s1只能使用一块(内存复制算法,避免内存碎片)
- 新生代:存放 1.生命周期比较短的对象 2.小的对象;反之,存放在老生代中。对象的大小,可以通过参数设置 -XX:PretenureSizeThredshold 。一般而言,大对象一般是 集合、数组、字符串。生命周期: -XX:MaxTenuringThredshold
- 新生代、老生代中年龄:MinorGC回收新生代中的对象。如果Eden区中的对象在一次回收后仍然存活,就会被转移到 s区中;之后,如果MinorGC再次回收,已经在s区中的对象仍然存活,则年龄+1。如果年龄增长一定的数字,则对象会被转移到 老生代中。简言之:在新生代中的对象,每经过一次MinorGC,有三种可能:1从eden ->s区 2.(已经在s区中)年龄+1 3.转移到老生代中
新生代在使用时,只能同时使用一个s区:底层采用的是复制算法,为了避免碎片产生
老生代: 1.生命周期比较长的对象 2.大的对象; 使用的回收器 MajorGC\FullGC
新生代特点:
- 大部分对象都存在于新生代
- 新生代的回收频率高、效率高
老生代特点:
- 空间大、
- 增长速度慢
- 频率低
意义:可以根据项目中 对象大小的数量,设置新生代或老生代的空间容量,从提高GC的性能。
虚拟机参数:
-Xms128m :JVM启动时的大小
-Xmn32m:新生代大小
-Xmx128:总大小
jvm总大小= 新生代 + 老生代
堆内存溢出示例
package cn.yu;
import java.util.ArrayList;
// 测试 堆内存溢出
//-Xms128m 虚拟机启动内存大小 -Xmn64m 新生代内存大小 -Xmx128m 总大小
public class TestHeap {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
while (true){
list.add(new int[1024 * 1024]);
}
}
}
方法区
- 存放类的元数据信息(JDK1.7以前是存放在堆中的永久代)、常量池、方法信息(方法代码、数据)
- GC主要回收方法区中类的元数据信息(类卸载的时候回收)、常量池中的数据(String类)
方法区和其它区域的内存联动
常量池: 存放编译时产生的字面量(String s = “abcd”)信息、符号引用(java.lang.String)
关于内存溢出
JVM中内存溢出会导致JVM退出,但是产生内存溢出不只是JVM虚拟机会产生,操作系统产生内存溢出,导致操作系统崩溃,也会间接的导致JVM退出,在NIO技术中会操作直接内存,也会导致内存溢出,操作系统就会终止程序。