虚拟机内存区域划分
在《java虚拟机规范》中,并没有提到java虚拟机内存结构,而是使用的”运行时数据区”这个术语,也就是说java虚拟机内存结构 = 运行时数据区。
在hotspot中,通常来讲,运行时数据区被划分为两块区域:线程独占区和线程共享区。
线程独占区
程序计数器
当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。
程序计数器是唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
特点:
- 程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器
- 处于线程独占区
- 如果线程执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 native方法,这个计数器的值为 undefined
java虚拟机栈
虚拟机栈描述的是Java执行的动态内存模式
java虚拟机的每一条线程都有自己私有的Java虚拟机栈,这个Java虚拟机栈跟线程同时创建,所以它与线程有着相同的生命周期。
栈帧:
- 每个java方法执行,都会在虚拟机栈中创建一个栈帧,伴随着方法从创建到执行完成。用于存储局部变量表,操作数栈,动态链接、方法出口等。
- 如果栈满了,再放入新的栈帧,就会报 StackOverflowError错误
- 常见的递归深度太过,就会抛出此异常
- 如果把栈的深度设置为非常非常大,超出了java虚拟机的内存或物理机器的内存,然后不断的往虚拟机栈中存放栈帧,直到超出了实际物理内存的容纳限制之后,会报OutOfMemory错误
局部变量表:
- 存放编译器可知的各种基本数据类型,引用类型,returnAddress类型
- returnAddress类型的值是指向jvm指令操作符的指针,不对应java类型,不能被运行程序修改
- 局部变量表的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,在方法运行期间是不会改变局部变量表的大小的。
本地方法栈
和虚拟机栈类似,只不过服务于native方法,线程私有
本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
线程共享区
Java堆
Java堆是所有线程共享,在虚拟机启动时就会被创建
- 占据内存空间最大的一块区域
- 用于存放对象实例以及数组
- new出来的对象都放这里
Java堆可以根据分代划分:
- 年轻代
- Eden区
- Survivor1(from)
- Survivor2(to)
- 一般来说,三个区的大小比例为: Eden:from:to = 8:1:1
- 老年代
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC(默认次数)还能在新生代中存活的对象,才会被送到老年代。
可以通过指令指定java堆大小:
- -Xms:堆容量初始大小(堆包括新生代和老年代)。 例如:-Xms 20M
- -Xmx: 堆最大容量。 例如:-Xmx 30M
- -Xmn: 新生代容量大小。例如:-Xmn 10M
- -XX: SurvivorRatio 设置参数Eden、form和to的比例 【比例参数Eden、form和to默认是8:1:1】例如:-XX: SurvivorRatio=8 代表比例8:1:1
当Java 堆内没有足够的空间去完成实例分配时,并且堆也无法扩展,将会抛出我们常见的OutOfMemoryError 异常,也就是我们常说的OOM 异常
方法区
在JDK1.8以前,方法区被称为永久代。
方法区是各线程共享,它存储了每个类的结构信息,例如运行时常量池、字段、方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
运行时常量池:
- 存在一个字符串常量池,相当于一个HashSet
,每次创建一个String对象,就会往这里面放一次,但是HashSet这种数据结构不允许重复元素,所以创建的相同字符串的String对象,只会存在一个 - 这里说的创建String对象,不是通过new,是 String str = “abc”;这种方式创建String对象
- new创建对象一定是在堆内存
- String.intern()方法,会把堆内存中的String对象放到运行时常量池中的字符串常量池中去。
- 当常量池无法申请到内存时,也会抛出 OutOfMemory错误
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。
在 JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象**作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据**。
本机直接内存的分配不会受到Java堆大小的限制,受到本机总内存大小限制
配置虚拟机参数时,不要忽略直接内存,防止出现OutOfMemoryError异常。
参考
- 本文作者: xczll
- 本文链接: https://xczllgit.github.io/2020/03/23/jvm/2020-03-23-jvmMemoryRegion/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!