Java的内存区域划分

内存分区简介老生常谈的问题了 , 虽然网上一搜一大把 , 也很详细 , 但是我还是想写一写 , 通过自己的总结整理 , 加深一下印象 。
我不知道学习Java内存分区有什么实际作用 , 但它就是像常识一样 , 一个使用Java语言的人不知道内存分区总感觉差点意思 。
Java程序是运行在JVM虚拟机上的 。 Java虚拟机在运行程序时会把其自动管理的内存划分为以下几个区域:
方法区、堆、程序计数器、虚拟机栈、本地方法栈 。
其中方法区和堆是所有线程共享的 , 而程序计数器、虚拟机栈和本地方法栈是每个线程独立享有的 。
这个不需要死记硬背 , 当我们了解了这几个区域不同的功能之后 , 就知道为什么有些内存区域是线程共享 , 而另外一些是线程私有的了 。
Java的内存区域划分文章插图
各区域职责划分方法区(Method Area)方法区属于线程共享的内存区域 , 《Java虚拟机规范》中将其描述为堆的一个逻辑部分 , 又称Non-Heap(非堆 , 目的是与Java堆区分开来) , 主要用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据 。
拿做菜打个比方 , 我理解方法区就好比一个食谱 , 它里面记录着做菜需要用的食材 , 加工方式等等 。 既然是菜谱 , 那就是公共的东西 , 不能修改 , 没必要每个线程都有一份 , 大家谁需要谁来查看就好了 。 所以它是线程共享的 。
JVM堆(Java Heap)Java 堆在虚拟机启动时创建 , 是Java 虚拟机所管理的内存中最大的一块 , 主要用于存放对象实例 , 几乎所有的对象实例都在这里分配内存 , 是垃圾收集器管理的主要区域 , 因此很多时候也被称做GC 堆(Garbage Collection) , 如果在堆中没有内存完成实例分配 , 并且堆也无法再扩展时 , 将会抛出OutOfMemoryError 异常 。
这次有点牵强 , 但还是可以拿做菜打比方 。
先说说面向对象 。 堆是用来存储对象的 , 对象是什么呢?对象是帮助我们做事情的东西 , 很多人拿洗衣服来说明面向对象编程 。 如果我们面向过程洗衣服 , 那么我们需要揉搓、洗涤、拧水、晾晒 , 每一步都要亲力亲为;而当我们面向对象时 , 只需要将衣服扔进洗衣机这个对象 , 剩下的事就不用管了 , 轻松省力 。 所以对象 , 就是帮助我们做事的工具 。
在做菜中 , 对象可以理解为电烤箱、菜刀 , 这种工具 , 那么在程序中 , 堆就是用来存储这些工具的 , 每个人都可以使用这些工具 , 不会乱套 , 所以堆也是线程共享的 。
程序计数器(Program Counter Register):程序计数器属于线程私有的数据区域 , 是一小块内存空间 , 主要代表当前线程所执行的字节码行号指示器 。 字节码解释器工作时 , 通过改变这个计数器的值来选取下一条需要执行的字节码指令 , 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成 。
用做菜来说 , 程序计数器记录的是你做菜做到了哪个环节了 , 显然不可能每个厨师都恰好在做相同的事 , 大家需要自己把控自己的进度 , 所以程序计数器是线程私有的 。
虚拟机栈(Java Virtual Machine Stacks):属于线程私有的数据区域 , 与线程同时创建 , 总数与线程关联 , 代表Java方法执行的内存模型 。 每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息 。 每个方法从调用直结束就对于一个栈桢在虚拟机栈中的入栈和出栈过程 。
这个对应的就是做菜的过程了 , 比如做一道菜需要三步 , 准备材料→炒菜→出锅 。 拿第一步举例 , 首先要准备材料 , 准备材料这个方法就要入栈了 , 入栈后发现 , 需要先把菜洗了 , 那么洗菜这个方法也要进栈 , 然后又发现需要先摘菜叶子 。 我们假定摘菜叶之前没有其他操作了 , 那么之后从栈顶开始先执行摘菜方法 , 然后摘菜方法弹出 , 然后执行洗菜方法 , 然后洗菜方法弹出 , 再执行准备材料方法 , 是一个后进先出的顺序 , 前一个弹出 , 后一个再执行 。 既然都已经具体到过程了 , 自然每个线程是要隔离开的 , 不能相互干扰 。
本地方法栈(Native Method Stacks):本地方法栈跟 Java 虚拟机栈的功能类似 , Java 虚拟机栈用于管理 Java函数的调用 , 而本地方法栈则用于管理本地方法的调用 。 但本地方法并不是 用 Java 实现的 , 而是由 C 语言实现的(比如Object.hashcode 方法) 。本地方法栈是和虚拟机栈非常相似的一个区域 , 它服务的对象是 native方法 。 你甚至可以认为虚拟机栈和本地方法栈是同一个区域 。