6.你了解Java的内存模型(JMM)吗
递归为什么会引发 java.lang.StackOverflowError异常?
原因 : 函数调用栈太深了,注意代码中是否有了循环调用方法而无法退出的情况
原理:StackOverflowError 是一个java中常出现的错误:在jvm运行时的数据区域中有一个java虚拟机栈,当执行java方法时会进行压栈弹栈的操作。在栈中会保存局部变量,操作数栈,方法出口等等。jvm规定了栈的最大深度,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误。
JVM内存模型(JDK8) - JMM
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享: MetaSpace、Java堆
线程私有
程序计数器(Program Counter Register) 也叫PC寄存器
JVM支持多个线程同时运行,每个线程都有自己的程序计数器。倘若当前执行的是JVM方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native方法,则PC寄存器为空。
- 当前线程所执行的字节码信号指示器(逻辑)
- 改变计数器的值来选取下一条需要执行的字节码指令
- 和线程是一对一的关系即 “线程私有”
- 对Java方法计数,如果是Native方法则计数器值为Undefined
- 不会发生内存泄露
Java虚拟机栈(Stack)与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关位置)和returnAddress类型(指向了一条字节码指令的地址)。
- 操作数栈:入栈、出栈、复制、交换、产生消费变量,操作数栈(Operand Stack)也常被成为操作栈,是一个后入先出栈,用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。其最大深度在编译时就被写到了Code属性的max_stacks中。
- 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。Class 文件的常量池中存在大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化成为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
- 方法出口、方法返回地址:
- 一种是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层方法的调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。
- 另一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是 Java 虚拟机内部产生的异常,还是代码中使用 athrow 字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。这种称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给上层调用者产生任何返回值的。
本地方法栈
- 与虚拟机栈相似,主要用于标注native得方法
线程共享:
方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- JDK1.8开始使用元空间来 实现 方法区。
- 元空间(MetaSpace)与永久代(PermGen)的区别:
- 元空间使用本地内存,而永久代使用的是jvm的内存。
- 永久代在JDK8中被完全的移除了。所以永久代的参数-XX:PermSize和-XX:MaxPermSize也被移除了。
- 元空间(MetaSpace)相比永久代(PermGen)的优势:
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出。
- 类和方法的信息大小难以确定,给永久代的大小指定带来困难。
- 永久代会为GC带来不必要的复杂性。
- 方便HotSpot与其他JVM如Jrockit的集成。
JAVA堆(Heap)Java Heap是被所有线程共享的区,在虚拟机启动时创建,唯一目的便是存放对象实例,几乎所有对象实例都在这里分配内存,通过-Xmx控制其大小
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以 细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
- 对象实例的分配区域
- GC管理的主要区域
JVM三大性能调优参数 -Xms -Xmx -Xss的含义:
- -Xss: 规定了每个线程虚拟栈(堆栈)的大小
- -Xms: 堆的初始值,jvm启动时分配的内存,比如-Xms200m,表示分配200M
- -Xmx: 堆能达到的最大值,为jvm运行过程中分配的最大内存
Java内存模型中堆和栈的联系
- 引用对象、数组时,栈里定义变量保存堆中目标的首地址。
Java内存模型中堆和栈的区别
- 管理方式:栈自动释放,堆需要GC
- 空间大小: 栈比堆小
- 碎片相关: 栈产生的碎片远小于堆
- 分配方式: 栈支持静态分配和动态分配,而堆仅支持动态分配
- 效率: 栈比堆效率高
JAVA内存分配策略:
- 静态存储: 编译时确定每个数据目标在运行时的存储空间需求
- 栈式存储: 数据区需求在编译时未知,运行时模块入口前确定。
- 堆式存储: 编译时或运行时模块入口都无法确定,动态分配。
JDK6+: 当调用intern方法时,如果字符串常量池先前创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则堆中将此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。
Nice to see you all!