对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
运行时数据区
线程私有的包括:程序计数器、虚拟机栈、本地方法栈
线程共享的:堆、方法区、直接内存
程序计数器
记录正在执行的虚拟机字节码指令的地址。由于是多线程,线程轮流切换,切换线程后为了能恢复到正常的执行位置,每个线程需要一个独立的程序计数器。如果执行的是本地(Naive)方法,计数器为空。此内存区域是唯一一个没有规定任何OutOfMemoryError情况的区域,它的生命周期随着线程创建而创建,随着线程结束而死亡。
Java虚拟机栈
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。
Java虚拟机栈也是线程私有的,生命周期与线程相同。描述的是Java方法执行的内存模型。每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用、动态链接、程序出口等信息。每一个方法从调用到执行完成的过程,对应一个栈帧在Java虚拟机栈中入栈和出栈的过程。
局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference类型,不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddressleixing (字节码指令地址)。局部变量表所需内存在编译期间完成分配,运行期间不会改变。
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小: java -Xss 512M
可能抛出的异常情况:
- 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候(栈帧过多),就抛出StackOverFlowError异常。
- 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
本地方法栈
本地方法栈与 Java 虚拟机栈类似,虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。有的虚拟机如HotSpot虚拟机把二者合二为一。抛出的异常与上述一致。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
Java堆
Java堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配,是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java堆是垃圾收集器管理的主要区域(方法区也需要回收),因此又称为GC堆(Garbage Collected Heap)。现在收集器基本采用分代收集算法,可以将堆分为新生代和老年代。划分的好处是可以方便垃圾的准确回收。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。堆还可以动态增加其内存,当堆中无法申请到新内存创建实例,并且堆也无法再扩展时,将会抛出OutOfMemroyError。
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M
方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也是所有线程共享。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
方法区和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
在HotSpot虚拟机中,把方法区当做永久代来进行GC,对起回收的目标主要是针对常量池的回收以及对类型的卸载,但是一般比较难实现。垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。在JDK1.8中,已经移除了永久代,用元空间来替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
运行时常量池
运行时常量池是方法区中的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。
JDK1.4中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会收到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
欢迎工作一到五年的Java工程师朋友们加入Java架构开发: 855835163 群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!