闲谈Java内存区域
前不久刚看完了Java虚拟机,看完之后觉得学到了不少东西。但是具体学到了些什么,却很难完整地把它讲述出来,总是会出现知识断层的情况。于是打算从头将其梳理一遍,争取打通任督二脉。。。
首先,先来看看Java内存区域的整个知识框架
Java语言的特点主要有两个:内存自动分配机制和垃圾收集机制。要想了解这两个内容,我们就要先搞清楚Java的内存区域,而Java内存区域主要就是指运行时数据区。由于各个区域的内存大小是有限的,如果使用不当就容易出问题,也就是内存溢出。
运行时数据区根据共享性可以划分为三类:线程私有、不完全的线程共享和线程共享。线程私有的区域有三块:程序计数器、本地方法栈和虚拟机栈。不完全的线程共享区域只有堆,也常被称为“GC堆”,因为它是GC收集的主要区域。线程共享的区域只有方法区。我们依次来看一下这些区域。
程序计数器,它的作用就是保存当前执行的字节码指令位置,让指令有序执行。
虚拟机栈,存放每一个方法对应的栈帧信息。当一个方法被创建时,它就会在虚拟机栈中创建一个栈帧,这个栈帧主要包括局部变量表,操作数栈和方法出口等等。
本地方法栈,它也是存放栈帧,只不过它处理的对象仅限于本地方法。我们通常所说的“栈”都是指虚拟机栈,也有虚拟机把虚拟机栈和本地方法栈给合在一起了。以上三个就是线程私有的区域。
接下来我们看一下堆。从内存回收的角度来看,堆主要被划分为老年代和新生代。而新生代又被划分为两个Survivor区域和一个Eden区域。这三个区域是干嘛的呢?这个就涉及到垃圾收集了,放到后面的博客再讲。从内存分配的角度来看,堆中还划分出了很多线程私有的分配缓存区。因此我把它归结到不完全的线程共享区域中。
最后我们看一下线程共享的区域——方法区。方法区中主要存放已被加载的类信息,常量、静态变量和即时编译器编译后的代码等。方法区中有两个区域可以单独拿出来讲。一个是运行时常量池,还一个就是字符串常量池。提到这两个常量池,我们不免需要提到Class文件的常量池。这三个常量池之间的区别和联系是什么呢?首先Class文件常量池位于本地,它里面存放了我们在java程序中定义的常量,主要是符号引用和字面量。而运行时常量池位于方法区中,它是Class文件的常量池的一份拷贝,在类加载的过程中本地Class文件的常量池就会被拷贝到运行时常量池中。字符串常量池位于方法区,它里面存放的是字符串常量的实例。我们可以举个例子,假如我们在java程序中定义了两个常量,一个是整型常量int a=1,一个是字符串常量string b="Hello"。那么这个程序被JVM编译后生成的Class文件常量池中会存有这两个常量。当Class文件进行类加载之后,这两个常量都会被拷贝到运行时常量池中。其中由于string1还是一个字符串,JVM首先会检查字符串常量池中是否有"Hello"这个字符串实例,如果没有JVM就会在字符串常量池中创建一个值为“Hello”的字符串实例。这个地方需要注意一个细节,就是字符串在实例化时用了new关键字和没有用new关键字是有很大区别的。用了new关键字的实例化会在JVM中产生两个实例对象,一个在堆中,一个在字符串常量池中,变量持有的是堆中实例对象的引用。没有用new关键字的实例化则只会在字符串常量池中生成一个实例化对象,自然变量持有的是字符串中实例对象的引用。(具体可以看我的另一篇博客,里面有详细的解释:《字符串常量池、class文件常量池和运行时常量池》)
至此整个运行时数据区已经讲完了。前面我们说过这些区域的内存都是有限的。自然不可避免地会出现内存溢出的问题。根据溢出区域的不同,内存溢出主要可以分为以下几个方面:堆溢出、栈溢出、方法区溢出、直接内存溢出。前面三个溢出的报错信息都会有相应的溢出区域信息。最后一个则不会带有具体区域。因为它不属于JVM规范中的任何一个内存区域。当溢出发生时,解决方案一般有两个,一个是调整相应区域的内存大小,另一个就是调整代码。比如:我前些时候在写门格海绵的时候就出现了栈溢出的问题。我一开始是直接用递归写的。因为递归看起来比较简洁易懂。当然不足之处就是过多的方法调用,导致虚拟机栈由于栈帧数量过多发生溢出。我当时采用的是第二种解决方案,调整代码结构,递归换成了for循环来实现,由此解决了栈溢出的问题。
该博文是本人阅读完《深入理解Java虚拟机》后做的一个知识点整合,更注重知识的关联性和完整性,因此不像其他博客一样有大小标题。没有JVM基础的建议先看我的另一篇博客《JAVA的内存区域构成(笔记)》