注意:本文说的是Java运行时数据区
Java虚拟机在执行Java程序的过程中会把它管理的内存划逻辑上分成若干个不同的数据区域。这些不同区域一些是共享一些是私有的,他们相互协作管理程序。
线程私有: 程序计数器 虚拟机栈 本地方法栈
线程共享的: 堆 方法区
1 程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果是线程执行一个Java方法那程序计数器就是字节码行号数,如果是native方法,就是空(underfind)。
它还是虚拟机规范里唯一没有规定指出内存溢出错误的地方。
2 虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是Java方法执行的内存模型。每个方法执行时(当然包括main)就会创建一个栈帧,用于储存局部变量表,操作数栈,动态链接,方法出口等。方法执行时,这个桢栈就会被压入栈中,方法执行结束后就会出栈
Java内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈。
在Java虚拟机规范中,对栈区域定义了二种异常:
第一种:线程请求的栈深度大于虚拟机的栈深度将报错,StackOverflowerror(堆栈异常)—就是执行函数时方法参数和局部变量等占用空间大于虚拟机设置的空间。解决办法使用xss增大每个线程的栈空间大小。
第二种:函数调用多个方法请求栈空间,如函数调用链很深(递归的时候很常见),栈的空间不够就会出outofmemoryerror。一般解决可以减少每个线程栈空间的大小,保持整个栈空间内存的容量;或者调整堆内存和方法区内存的大小。
3 本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是十分相似的,他们之间的区别不过是虚拟机栈为Java方法字节码服务,而本地方法栈则为Native方法服务。在虚拟机规范中对本地方法使用的语言和使用方法与数据结构没有强制规定,因此具体的虚拟机可以自由实现它。Sun HotSpot虚拟机把本地方法栈和虚拟机栈合二为一。和虚拟机栈一样,本地方法栈也会抛出OutOfMemoryError 和 StackOverflowError异常。
4 堆
堆(heap)是虚拟机中最大的一块内存区域了,被所有线程共享,在虚拟机启动时创建。它的目的便是存放对象实例。
堆是垃圾收集器管理的主要区域,因此 很多时候也被成为‘GC’堆(Garbage Collected Heap)。
从垃圾回收的角度来讲,现在的收集器包括HotSpot都采用分代收集算法,所以堆又可以分为:新生代(Young)和老年代(Tenured),JVM 提供了参数 -Xmn 来设置年轻代内存的大小,但没有提供参数设置老年代的大小。但其实老年代的大小就等于堆大小减去年轻代大小。
对于新生代可以再细致一点,新生代又可分为Eden、From Survivor空间和To Survivor空间,他们的比例是8:1:1。
从内存分配的角度来讲,又可以分为若干个线程私有的分配缓冲区(Thread Local Allocation Buffer ,TLAB)。
当堆空间不足切无法扩展,会抛出OutOfMemoryError异常,空间不足,造成不足也分为二种情况:
一种是内存泄漏——就是已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中导致内存溢出。这种可以通过排查工具找出gcroots不合理的引用并优化。
内存溢出——顾名思义就是堆内存的对象太多了不够用,
解决办法就是增大堆空间内存,我们可以使用我们使用 -Xms 设置堆的初始空间大小,使用 -Xmx 设置堆的最大空间大小。
5 方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,方法区也常被大家称为 “永久代”。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了,只是回收发生情况少而已,当内存不足时会抛出 OutOfMemoryError 异常。可以通过-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize设置初始大小和最大大小的。
6 运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常,解决办法和方法区一样。
JDK1.8及之后版本的 JVM 已经将运行时常量池实现方式取而代之的是一个叫元空间(Metaspace)的鬼东西。
常量池区分:https://blog.csdn.net/qq_26222859/article/details/73135660
https://blog.csdn.net/qq_31615049/article/details/81611918
永久代和元空间:http://www.cnblogs.com/paddix/p/5309550.html
7 直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。
JDK1.4中新加入的NIO(New Input/Output)类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的I/O方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在Java堆和Native堆之间来回复制数据。
本机直接内存的分配不会收到Java堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。