运行时区域包括:
方法区(Method), 堆(Heap), 程序计数器(Program Counter Register), 虚拟机栈(Java Virtual Machine Stack)和本地方法栈(Native Method Stack)五部分, 如图 :
1. 程序计数器(Program Counter Register)
1.2 程序是很小的内存空间, 可以看做线程执行字节码的行号指示器 .
1.2 字节码解释器工作时就是通过改变这个计数器的值来选择下一条要执行的字节码指令 , 分支,循环,跳转,异常处理,线程恢复等基础功能都依赖计数器来完成.
1.2 程序计数器的区域私有原因 : Java的多线程是通过线程轮流切换和控制执行时间来实现的. 在任何时刻,一个处理器(多核表示一个核) 只执行一个线程.为保证线程在切换之后,能恢复到正确的位置,则每条线程都需要一个程序计数器. 各个线程的计数器互不干干涉,独立保存.
1.2 程序计数器的记录内容: Java方法 记录的是正在虚拟机执行的字节码的指令地址 , Native方法, 记录的值为空(undefined).
1.2 异常 : Java虚拟机规范中,没有规定任何的 OutOfMemoryError 情况的区域.
2. Java虚拟机栈(Java Virtual Machine Stacks)
2.1 线程私有 , 生命周期与线程相同.
2.2 虚拟机栈是用来描述Java方法的执行内存:
2.2.1 每个方法在执行的同时会创建一个栈帧;
2.2.2 用来存储局部变量表 , 操作数栈,动态链接和方法出口等信息;
2.2.3 每一个方法的调用直至执行完成, 都对应着一个栈帧在虚拟机中的入栈和出栈过程;
2.2.4 局部变量表中存储着八种基本类型(byte,short,int,long,float,double,boolean,char),引用类型,returnAddress类型(指向了一条字节码指令的地址);
2.2.5 64位长度的long和double类型,会占用2个局部变量空间,其余类型1个; 局部变量表所需的内存在编译期间就分配完成, 所以空间是完全确定的,运行时间不会改变局部变量的表的大小.
2.3 异常
2.3.1 StackOverFlowError异常, 线程请求的栈深度大于虚拟机所允许的深度时抛出异常. 常见情况 , 无限递归调用
2.3.2 OutOfMemoryError 异常, 大部分情况下虚拟机的栈都是可自动扩容的,如果扩展时无法申请到足够的内容就会抛出异常;
3. 本地方法栈(Native Method Stack)
3.1 与Java虚拟机栈功能相似 , 区别在于Java虚拟机执行的Java方法 , 本地方法栈执行的是Native方法服务.
3.2 Java虚拟机规范中, 对本地方法使用的 语言, 使用方式和数据结构都没有强制规定, 可以自由实现
3.3 Sun Hotspot 虚拟机直接把本地方法和 虚拟机站方法合并 .
3.4 会抛出 StackOverFlowError 和 OutOfMemoryError 异常, 同虚拟机栈
4. 堆(Java Heap)
4.1 是Java虚拟机中内存最大的一块, 被线程共享,在虚拟机启动时被创建.
4.2 几乎存放所有实例对象. 因为 JIT编译器的发展和逃逸分析技术成熟 , 栈上分配,标量替换技术, 将绝对变得不绝对.
4.3 Java堆 是垃圾管理等的主要区域, 又称GC堆.
4.4 现在收集器都是采用 分代收集算发 , 将Java细分为 : 新生代和老年代 , 在细分为 Eden空间, From Survivor 空间 , To Survivor空间
4.5 Java堆可以处于物理上不连续的内存空间, 只要逻辑上连续即可
4.6 当前主流虚拟机基本都是可扩展的 , 扩展通过(-Xmx 和 -Xms控制)
4.7 异常 OutOfMemoryError : 在堆中没有完成实例的分配, 堆也无法完成在扩展.
5. 方法区(Method Area)
5.1 是线程共享的区域.
5.2 存储内容: 被虚拟机加载的类信息 ,常量 , 静态变量, 即时编译后的代码等数据.
5.3 方法区 != 永久代 , Hotspot虚拟机的实际团队, 把GC分代收集扩展至方法区,或者是 使用永久代来实现方法区. 这样Hotspot 的垃圾收集器可以和堆一样来管理这部分内存 ,能省去专门为方法区编写内存管理的内容.
5.4 用永久代来实现方法区的问题 :
5.4.1 永久代有 -XX:MaxPermSize的上限, 容易遇到内存溢出的问题;
5.4.2 极少数方法(String.intern()) 会因为这个原因在不同的虚拟机下有不同的表现;
5.5 JDK 1.7 的Hotspot中,已经把原本放在永久代的 字符串常量池 移出.
5.6 方法区的垃圾回收 : 并非垃圾进入了永久代就是 永久存在了 , 这区域的垃圾回收主要针对 常量池和类型的卸载.
5.7 但是这区域的回收能力低 , 尤其类型的卸载, 条件相当苛刻
5.8 异常 OutOfMemoryError , 方法区没法满足内存分配需求时 抛出.
6. 运行时常量池(Runtime Constant Pool)
6.1 是方法区的一部分 , Class文件中包含内容有, 类的版本信息,字段,方法,接口等描述信息 , 还有一项信息是常量池.
6.2 用于存放编译时期生成的各种字面量和符号引用, 在类被加载进方法区的时候存入常量池.
6.3 动态性, Java语言中并不要求常量一定在编译器才产生, 运行时期也能将新的常量放入池中 , 利用的较多的就就是String类的intern 方法.
6.4 异常 OutOfMemoryError : 常量池中无法再申请到内存时抛出.
7. 直接内存(Direct Memory)
7.1 并不是虚拟机的运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域.
7.2 NIO(New Input/Output) , 引入一种基于管道(Channel)与缓冲区(buffer)的I/O方式, 可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作.
7.3 分配内存不会影响Java堆大小的限制, 但是受本机总内存大小和处理器寻址空间限制.
7.5 异常 OutOfMemoryError 配置虚拟机参数时 , 会根据实际内存设置 -XMx等参数新消息, 忽略直接内存, 使各个内存区域总和大于物理内存限制 , 然后抛出异常.
8. 总结(Summary)
8.1 有内存划分, 每个内存的用处
8.2 内存溢出异常(OutOfMemoryError)
8.2.1 虚拟机的栈都是可自动扩容的,如果扩展时无法申请到足够的内容就会抛出异常;(同本地方法栈)
8.2.2 堆中没有完成实例的分配, 堆也无法完成在扩展
8.2.3 方法区没法满足内存分配需求
8.2.4 常量池中无法再申请到内存
8.2.5 忽略直接内存, 使各个内存区域总和大于物理内存限制
8.3 堆得扩展通过 -Xmx 和 -Xms控制
8.4 永久代有 -XX:MaxPermSize限制