一、前言
Java虚拟机在执行Java程序的过程中,会把他管理的内存划分为多个不同的数据区域,这些区域各自有各自的用途,以及创建和销毁的时间,有的区域随虚拟机进程的启动而存在,有的区域依赖用户线程的启动和结束而建立和销毁。
Java虚拟机所管理的内存将会包括以下几个运行时数据区域。
1.方法区(线程共享),JDK1.7中已经开始改变,JDK1.8中被元空间替代
2.堆(线程共享)
3.虚拟机栈(线程隔离)
4.本地方法栈(线程隔离)
5.程序计数器(线程隔离)
Java虚拟机运行时涉及到的另外的内存区域
1.直接内存
二、方法区(Method Area)
这是一个线程共享的数据区域,用来保存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机将方法区描述为堆的一个逻辑部分(在HotSpot中具体表现为在方法区的内存管理和堆中的内存管理是一套方案,当然这里的一套方案并不是说他们是一模一样的),但是方法区有一个别名叫做Non-Heap(非堆),应该是为了和堆作区别(Heap)。
在HotSpot虚拟机中,方法区又被很多人叫做“永久代”(Permanent Generation),本质上两者并不等价,仅仅是HotSpot团队将GC分代收集扩展至方法区,也就是使用永久代来实现方法区。这样就和Java堆使用了同样的GC分代收集策略,不必重新实现一套管理策略。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的。
然而在实际应用中,使用永久代来实现方法区并不是一个好的选择,更容易遇到内存溢出的问题。在JDK1.7中,已经将字符串常量池从永久代移出了。
字符串常量池的移出:JDK1.7 被转移到Java堆中(Java Heap)
Java虚拟机规范堆方法区的限制宽松,方法区不需要连续的内存,也可以不实现垃圾收集。事实上,垃圾收集的频率在这个区域是很小的,但是并不是所有在此的数据真的是“永久”的,这个区域的垃圾回收目标主要是针对常量池的回收和对类型的卸载。这个区域的回收成绩难以令人满意。当方法区无法满足内存分配的需求时,会抛出OutOfMemoryError的错误。
关于类型卸载,我理解为在Java虚拟机运行时,会将类信息加载到方法区,当某些类不会用到的时候(unreachable),就会从方法区中卸载这个类以节省内存),具体可以看下面的文章
1.Java类加载原理解析
2.Java虚拟机类型卸载和类型更新解析
在方法区中有一块叫做运行时常量池(Runtime Constant Pool)的区域,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分在类加载到方法区后进入运行时常量池。
字面量(literal): 我的理解为字面量指的几种基本类型。int,boolean,char,float,double,string,long,byte,null
其中float和double统称为floating-point literal,当你创建一个浮点数时,默认的是double类型。这也是为什么float a = 0.1;是错误的,你得float a = (float)0.1;
具体字面量的内容可以参考这里:Java Literals
符号引用: 符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。
JVM中的直接引用和符号引用关于常量池中的一些细节可以看这里的对比
Java常量池方法区的变迁:
1、JDK1.2 ~ JDK6
在 JDK1.2 ~ JDK6 的实现中,HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;2、JDK7
由于 GC 分代技术的影响,使之许多优秀的内存调试工具无法在 Oracle HotSpot之上运行,必须单独处理;并且 Oracle 同时收购了 BEA 和 Sun 公司,同时拥有 JRockit 和 HotSpot,在将 JRockit 许多优秀特性移植到 HotSpot 时由于 GC 分代技术遇到了种种困难,所以从 JDK7 开始 Oracle HotSpot 开始移除永久代。
JDK7中符号表被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。3、JDK8
在 JDK8 中,永久代已完全被元空间(Meatspace)所取代。
引用:Java 内存之方法区和运行时常量池
##三、堆(Java Heap)
和方法区一样,这也是一个线程共享的数据区域。在java虚拟机执行Java程序时,它占据了大多数的内存,几乎所有的对象的存储都是在这个区域。为什么说几乎呢?因为随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配,标量替换优化技术将会导致一系列微妙的变化发生。
JIT编译器:及时编译器(Just-In-Time compiler)
逃逸分析技术:全部变量赋值,方法返回值,实例引用传递三种情况会发生指针逃逸,如果在方法内新建对象,并且这个对象没有离开过这个方法,则这个对象没有必要在堆中分配内存,直接在Java虚拟机栈中分配内存,省去了在堆中分配内存堆GC造成的压力。
什么是逃逸分析(Escape Analysis)?
栈上分配:将对象在栈上分配内存
标量替换优化技术:Java中的原始类型无法再分解,可以看作标量(scalar);指向对象的引用也是标量;而对象本身则是聚合量(aggregate),可以包含任意个数的标量。如果把一个Java对象拆散,将其成员变量恢复为分散的变量,这就叫做标量替换。拆散后的变量便可以被单独分析与优化,可以各自分别在活动记录(栈帧或寄存器)上分配空间;原本的对象就无需整体分配空间了。
HotSpot 17.0-b12的逃逸分析/标量替换的一个演示
Java堆是垃圾收集器管理的主要区域,又成”GC堆”(Garbage Collected Heap)。
从内存回收的角度来看,现在收集器基本都采用分代收集算法。所以Java堆可以细分为:新生代和老年代,再细分一点,可以分为Eden空间J,From Survivor空间,to Survivor空间
从内存分配的角度来看,Java堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
Java堆的内存不要求在空间上是连续的,只要在逻辑上连续即可。
四、虚拟机栈(Java Virtual Machine Stack)
线程私有,和线程的生命周期相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时,都会创建一个栈帧(Stack Frame),用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧从虚拟机栈入栈到出栈的过程。
局部变量表中存放了编译区可知的各种基本数据类型(boolean,byte,char,int,float,double,long,short)、对象引用(reference,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄和其他与此对象相关的位置)
64位的double和long会占据两个局部变量空间(slot),其余数据类型只占一个。局部变量表所需的空间在编译期间分配完成,当进入一个方法时,这个方法需要在帧中分配多少局部变量空间完全时确定的。
在Java虚拟机中,对这个区域规定了两中异常情况。1.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError;如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,则会抛出OutOfMemoryError异常。
五、本地方法栈(Native Method Stack)
跟虚拟机栈类似,不过是用来存储本地方法的。
六、程序计数器(Program Counter Register)
线程私有,这是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都依赖这个。
每个线程都需要一个独立的程序计数器,保证各条线程执行中不会混乱。
如果当前执行的时Java方法,这个计数器记录的正是当前正在执行的虚拟机字节码指令的位置,如果执行的时Native方法,这个计数器为空。此区域时唯一一个在Java虚拟机规范中没规定任何OutOfMemoryError的区域。
七、直接内存(Direct Memory)
在JDK1.4中加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这个内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了频繁的在Java堆中和Native堆中复制数据。