本来是想写一篇关于 OOM 的文章, 不过感觉所要涵盖的东西会比较多, 所以把内存模型分出来单独写一篇
因为最近都在看其他大牛的博客, 碎片化的学习, 所以总结起来可能还有些凌乱, 后续会慢慢再整理, 也希望读者可以给点建议或补充, 谢谢
Java 运行时数据区
先上一张图, 这是我找到的最好的一张了
Java 虚拟机运行时数据区
从图来看, 我们可以把 Java 内存区分为堆内存 (Heap) 和栈内存 (Stack) 虽然这种分法比较粗糙, 实际上要复杂的多, 不过初学者来说这是我们最关注的的两块区域
总内存 = 堆内存(Xmx)+ 方法区内存(MaxPermSize)+ 栈内存(Xss)* 线程数 + 直接内存(MaxDirectMemorySize, 堆外)+ 虚拟机内存
程序计数器
程序计数器是一块较小的内存空间, 它可以看作是当前线程所执行的字节码的行号指示器在虚拟机的概念模型里, 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令, 分支循环跳转异常处理线程恢复等基础功能都需要依赖这个计数器来完成
学过汇编的朋友应该比较容易理解通俗的讲就是储存了下一条要执行的代码的编号
Java 虚拟机的多线程就是通过线程轮流切换并分配处理器执行时间的方式来实现的, 任何一个确定的时刻, 一个处理器只会执行一条线程中的指令所以为了线程切换后能恢复到正确的执行位置, 每条线程都需要有一个独立的程序计数器
每个线程都有自己单独的程序计数器, 互不影响这个属于线程私有的内存
如果线程正在执行的是一个 Java 方法, 这个程序计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是 Native 方法, 这个计数器值则为空
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域
Native Method(原生方法), 一个原生方法就是一个用 Java 调用非 Java 代码的接口, 方法的实现由非 Java 方法实现, 比如 C 和 C++ 此处需要记一下, 因为调用非 Java 方法也会涉及 GC 和 OOM
Java 虚拟机栈
Java 虚拟机栈也是线程私有的, 它的生命周期与线程相同虚拟机栈描述的是 Java 方法执行的内存模型, 每个方法被执行的时候都会同时创建一个栈帧 (Stack Frame) 用于存储局部变量表操作栈动态链接方法出口等信息
当线程调用 Java 方法时, 虚拟机压入一个新的栈帧到对应线程的虚拟机栈中; 当方法返回时, 这个栈帧就被从栈中弹出并抛弃
局部变量表所需的内存空间在编译期间完成分配, 当进入一个方法时, 这个方法需要在帧中分配多大的局部变量空间是完全确定的, 在方法运行期间不会改变局部变量表的大小
这块区域存在两种异常情况: 如果线程请求的栈深度大于虚拟机允许的深度, 抛出 StackOverflowError 异常; 如果虚拟机栈可以动态扩展, 且扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常
在这里也说一个题外话, 由于每个方法从进入到返回对应着栈帧的压入和弹出, 这个过程需要耗费一定的时间和资源, 所以也意味着代码中调用的方法越多, 执行效率也会越低可以不拆分的方法就不拆吧?
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的, 其区别不过是虚拟机栈为虚拟机执行 Java 方法 (也就是字节码) 服务, 而本地方法栈则是为虚拟机使用到的 Native 方法服务
Java 堆
Java 堆 (Java Heap) 是被所有线程共享的一块区域, 所有的对象实例以及数组都要在堆上分配
Java 堆是垃圾收集器管理的主要区域从内存回收的角度看, 由于现在收集器基本都是采用的分代收集算法, 所以 Java 堆中还可以细分为: 新生代和老年代; 新生代再细致一点的有 Eden 空间 From Survivor 空间 To Survivor 空间等
如果堆中没有内存完成实例分配, 并且堆也无法再扩展时, 将会抛出 OutOfMemoryError 异常
方法区
方法区是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据
这个区域的内存回收目标主要是针对常量池和对类型的卸载
当方法区无法满足内存分配需求时, 将抛出 OutOfMemoryError 异常
运行时常量池
运行时常量池是方法区的一部分 CLass 文件中除了有类的版本字段方法接口等描述等信息外, 还有一项信息的常量池, 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后存放到方法区的运行时常量池中
直接内存
它并不是虚拟机运行时数据区的一部分, 也不是 JAVA 虚拟机规范中定义的内存区域在 JDK1.4 中加入了 NIO 类, 引入了一种基于通道 (Channel) 于缓冲区 (Buffer) 的 I/O 方式, 他可以使用 Native 函数库直接分配堆外内存, 然后通过一个存储在 JAVA 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作这样能在一些场景中显著提高性能, 因为避免了在 JAVA 堆中和 Native 堆中来回复制数据
Java 内存模型
这个可以直接参考 Java 内存模型, 觉得说的挺清晰的
需要提一下的是 volatile 和 static 的区别, 两者并没有交叉关系, static 表示变量是所有实例共享的, 唯一的; volatile 表示线程每次要使用该变量都必须要从堆内存中读取变量的最新值, 并且线程中更新了 volatile 变量的话必须马上写会堆内存中(在内存模型中规定了 volatile 变量在 use 之前必须 read 和 load, 在 assign 后必须 store 和 write)
如果需要一个变量保证唯一性和线程操作安全, 或者用作线程间通信, 可以使用 static volatile 来定义该变量
参考:
深入理解 Java 虚拟机
Java 内存模型与 JVM (二)
来源: http://www.jianshu.com/p/2cdd069a4d5f