JVM内存模型
运行时数据区->方法区、堆、虚拟机栈、本地方法栈、程序计数器
程序计数器:
-
它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域。
-
在JVM规范中,每个线程都有它自己的程序技术器,是线程私有的,生命周期与线程的生命周期保持一致。
-
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
-
它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
-
问:使用PC寄存器存储字节码指令地址有什么用?(为什么使用pc寄存器记录当前线程的执行地址?)
答:因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪里继续开始执行。
JVM的字节码解释器就需要通过改变pc寄存器的值来明确下一条应该执行什么样的字节码指令。
GC只存在方法区和堆中!
栈是运行时的单位,堆是存储的单位。
虚拟机栈:
-
栈中的数据都是以栈帧的格式存在的。(在这个线程上正在执行的每个方法都各自对应一个栈帧)
-
每个栈帧中存储局部变量表,操作数栈、动态链接、方法返回地址、一些附加信息
-
局部变量表:(用来存放各种变量),最基本的存储单元是Slot(变量槽),槽位可以重复利用
-
操作数栈(表达式栈):主要用于保存计算结果的中间结果,同时作为计算过程中变量的存储空间
-
动态链接:指向运行时常量池的方法引用,作用就是为了将这些符号引用转换为调用方法的直接引用
-
方法返回地址:(方法正常退出(正常return)或者异常退出(遇到异常)的定义)
区别在于:通过异常完成出口退出的不会给他的长层调用者任何的返回值。
-
局部变量表中的变量是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都会被回收。
本地方法栈:
- java虚拟机栈用于管理java方法的调用,而本地方法栈用于管理本地方法的调用。也是线程私有的。
- 允许被实现成固定或可动态扩展的内存大小,也会出现SOF,OOM
- 本地方法栈使用C语言实现的。
- 并不是所有的jvm都支持本地方法。
堆:
-
一个jvm实例只存在一个堆内存,堆也是java内存管理的核心区域
-
java堆区在jvm启动的时候即被创建,其空间大小也就确定了,是jvm管理的最大一块内存空间
=>堆内存的大小是可以调节的
-
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
-
所有的线程共享java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)
-
“几乎”所有的对象实例都在这里分配内存
-
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
-
在方法结束之后,堆中的对象不会马上被移除。仅仅在垃圾收集的时候才会被移除。
-
堆 是GC(垃圾回收器)执行垃圾回收的重要区域
-
堆空间
-
内存设置大小参数:
“-Xms” 起始内存 ->默认:物理内存大小/64
“-Xmx" 最大内存 ->默认:物理内存大小/4
通常将这两个参数配置相同的值,
其目的为了能够在java回收机制清理完内存之后不需要重新分隔堆区的大小,从而提高性能
-
内存逻辑:
java7 及之前分为: 年轻代 + 老年代 + 永久区
java8 及之后分为: 年轻代 + 老年代 + 元空间
默认:新生代 : 老年代 = 1: 2
-
年轻代又分为:Eden区,Survivor0区和Survivor1区(有时也叫from区和to区)
几乎所有的java对象都是在Eden区被new出来的。
绝大部分的java对象的销毁都在新生代进行了。新生代80%的对象都是“朝生夕死”的
-
-
垃圾回收GC
分类:
jvm在进行回收的时候,并非每次都是一起回收的。大部分回收的都是指新生代。
-
部分收集:
新生代收集(Young GC):只针对新生代进行垃圾收集
老年代收集(Old GC):只针对老年代的垃圾收集
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
-
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集
*过程(非常重要):
- 首先在Eden区new对象,当伊甸园区满了之后,就会触发young GC,将Eden区中的不再被其他对象所引用的对象进行销毁。将剩余的对象放入到幸存者0区,
- 再new对象,Eden区再满之后,再次触发Young GC,进行无用对象销毁,将eden区和s0区剩余的对象放入到s1区。
- 如此反复,默认阈值是15,就会将剩余对象放入到老年代区。
- 当老年代区内存不足时,就会触发Old GC,进行内存清理。
- 最后当老年区执行了Old GC 之后发现依然无法进行对象的保存,就会产生OOM异常。
-
方法区:
-
其实方法区就是永久代(元空间)。
-
方法区与堆的关系呢,就像台湾与大陆的关系。逻辑上是一体的,但是也是自己独立存在的。
-
方法区和堆一样,是各个线程共享的内存区域
-
方法区在jvm启动的时候被创建,并且它的实际物理内存空间中和堆区一样都是可以不连续的。
-
方法区的大小,跟堆空间一样,可以选择固定大小或者可拓展。
-
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。
-
关闭jvm就会释放这个区域的内存
-
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型
只要常量池中的常量不被任何地方引用,就可以被回收
-
存储:已被jvm加载的 类型信息、运行时常量池、静态变量、JIT代码缓存、域信息、方法信息
-
为什么永久代会被元空间所替代
-
为永久代设置空间大小是很难确定的
在某些场景下,如果动然加载类过多,容易产生永久代的oom。比如某个实际web工程中,因为某个实际web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。
而元空间与永久代最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
因此,在默认情况,元空间的大小仅受本地内存限制。
-
对永久代进行调优是很困难
-
OOM、 SOF
StackOverflowError:栈溢出,常见于递归
OutOfMemoryError:(OOM)内存溢出
java虚拟机规范允许java栈的大小是动态的或者是固定不变的。
->如果采用固定大小的java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候独立选定。
如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出SOF异常。
->如果java虚拟机可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,
或者在创建新线程时没有足够的内存去创建对应的虚拟机栈,将会抛出OOM异常。