一、java堆
对于java程序来说,java堆是虚拟机所能管理的内存中最大的一块,也是被所有线程所共享的一块内存区域,在虚拟机启动时被创建,在内存区域的唯一目的就是存放对象。可以说是“几乎”所有的对象实例都在这里分配内存,为什么说是几乎呢?因为一些迹象表明,日后可能出现值类型的支持了,例如这标量优化等。
1.1内存回收
java堆也是垃圾收集器管理的内存区域,也被称作“GC”堆(Garbage Collected Heap)。从内存回收的角度来看,由于大部分的垃圾回收器都采用分代收集算法,出现了:新生代、老年代、永久代等概念,又将新生代分为:Eden区,幸存区。又可以将幸存区再次细分为“From Survivor区”和“To Survivor”区。当然,这些分类仅仅只是对于分代垃圾收集方法来讲的,毕竟HotSpot虚拟机仍然是业界的主流,内部大部分都是基于这种垃圾收集方法,但是也出现了不采用分代设计的新型垃圾收集器,这种情况下,本人个人认为java堆中就不在有这些“各种代”了
1.2内存分配
再从分配内存的角度上来讲讲吧,所有的线程共享的java堆中还可以划分处多个“线程私有”的分配缓冲区,用来提升对象分配时的效率,不过无论怎么划分,如何划分。是不会改变java堆中存储内容的共性,无论哪个区域,存储的都只能是对象的实例,对java堆的各种细分,本人看来仅仅是为了更好的回收内存,或者说是更好的分配内存吧。
java堆在实际存储数据时,并不一定要求连续的内存空间,但是在逻辑上还是要求是一种连续的空间,就像是我们去使用磁盘空间,并不一定要求文件都是连续的存储。但是对于大的对象,例如数组这种,还是会要求连续的内存空间,多数是为了实现简单、存储高翔的目标。
1.3java对内存设置
java堆还可以被设置为固定大小,也可以被设置为可以扩展的,不过内存不足时会报出OutOfMemoryError异常,也就是我们常说的OOM异常。现在多数采用的是可扩展方法。
二、方法区
方法区(Mehod Area)与java堆一样,是各个线程共享的内存区域,它用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名作“非堆(Non-Heap),目的是与Java堆区分开来。
2.1永久代
说到方法区,不得不提一下“永久代”这个概念,尤其是在JDK以前,许多人都习惯在HoSpot虚拟机上开发、部署程序,很多人都更愿意把方法区称呼为“永久代”(Permanent Generation),或将两者混为一谈。本质上这两者并不是等价的,因为仅仅是当时的HotSpo虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永代来实现方法区而已,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作,但是对于其他虚拟机实现,替如BE JRockit、IBM J9等来说,是不在在永久代的概念的。原则上如何实现方法区属于虚拟机分医代现细节,不受《Java虚拟机规范)管束,并不要求统一。
但现在回过头来看,当年使用永久代来实现方法区的决定并不是一个好主意,这种设计导致了Java应用更容易遇到内存溢出的问题(永久代有-xX:MaxPermSize的上限,即使不设置也有默认大小,而J9和JRock虚拟机只要没有到进程可用内存的上限,例如32位系统中的4GB限制,就不会出问题),而且有极少数方法(例如String::intern())会因永久代的原因而导致不同虚拟机下有不同的表现。
当Oracle 收购 BEA 获得了JRockit的所有权后,准备把JRockit中的优秀功能、譬如 Java Mission Control 管理工具,移植到HotSpot虚拟机时,但因为两者对方法区实现的差异面而临诸多困难。考虑到HotSpot 未来的发展,在JDK6的时候 HotSpot 开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了 JDK7的HotSpot。已经把原本放在永久代的字符串常量池、静态变量移至Java 堆中。
2.2元空间
而到了 JDK 8。终于完全废弃了永久代的概念,改用与 JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替。把JDK7 中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
《Java 虚拟机规范》对方法区的约束是非常宽松的。除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外。甚至还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进人了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的自收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
2.3运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java虚拟机对于Class文件每一部分(自然也包括常量池)的格式都有严格规定,如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行,但对于运行时常量池,《Java虚拟机规范》并没有做任何细节的要求,不同提供商实现的虚拟机可以按照自己的需要来实现这个内存区域,不过一般来说,除了保存Class 文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生。也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法----使所有相同内容的字符串共享一个内存对象。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。