1
内存
分配策略
按照 编译 原理的 观 点 , 程序运行 时 的内存分配有三 种 策略 , 分 别 是静 态 的 , 栈 式的 , 和堆式的 .
静 态 存 储 分配是指在 编译时 就能确定 每 个数据目 标 在运行 时 刻的存 储 空 间 需求 , 因而在 编译时 就可以 给 他 们 分配固定的内存空 间 . 这种 分配策略要求程序代 码 中不允 许 有可 变 数据 结 构 ( 比如可 变 数 组 ) 的存在 , 也不允 许 有嵌套或者 递归 的 结 构出 现 , 因 为 它 们 都会 导 致 编译 程序无法 计 算准确的存 储 空 间 需求 .
栈 式存 储 分配也可称 为动态 存 储 分配 , 是由一个 类 似于堆 栈 的运行 栈 来 实现 的 . 和静 态 存 储 分配相反 , 在 栈 式存 储 方案中 , 程序 对 数据区的需求在 编译时 是完全未知 的 , 只有到运行的 时 候才能 够 知道 , 但是 规 定在运行中 进 入一个程序模 块时 , 必 须 知道 该 程序模 块 所需的数据区大小才能 够为 其分配内存 . 和我 们 在数据 结 构所熟知 的 栈 一 样 , 栈 式存 储 分配按照先 进 后出的原 则进 行分配。
静 态 存 储 分配要求在 编译时 能知道所有 变 量的存 储 要求 , 栈 式存 储 分配要求在 过 程的入口 处 必 须 知道所有的存 储 要求 , 而堆式存 储 分配 则专门负责 在 编译时 或运行 时 模 块 入口 处 都无法确定存 储 要求的数据 结 构的内存分配 , 比如可 变长 度串和 对 象 实 例 . 堆由大片的可利用 块 或空 闲块组 成 , 堆中的内存可以按照任意 顺 序分配和 释 放 .
按照 编译 原理的 观 点 , 程序运行 时 的内存分配有三 种 策略 , 分 别 是静 态 的 , 栈 式的 , 和堆式的 .
静 态 存 储 分配是指在 编译时 就能确定 每 个数据目 标 在运行 时 刻的存 储 空 间 需求 , 因而在 编译时 就可以 给 他 们 分配固定的内存空 间 . 这种 分配策略要求程序代 码 中不允 许 有可 变 数据 结 构 ( 比如可 变 数 组 ) 的存在 , 也不允 许 有嵌套或者 递归 的 结 构出 现 , 因 为 它 们 都会 导 致 编译 程序无法 计 算准确的存 储 空 间 需求 .
栈 式存 储 分配也可称 为动态 存 储 分配 , 是由一个 类 似于堆 栈 的运行 栈 来 实现 的 . 和静 态 存 储 分配相反 , 在 栈 式存 储 方案中 , 程序 对 数据区的需求在 编译时 是完全未知 的 , 只有到运行的 时 候才能 够 知道 , 但是 规 定在运行中 进 入一个程序模 块时 , 必 须 知道 该 程序模 块 所需的数据区大小才能 够为 其分配内存 . 和我 们 在数据 结 构所熟知 的 栈 一 样 , 栈 式存 储 分配按照先 进 后出的原 则进 行分配。
静 态 存 储 分配要求在 编译时 能知道所有 变 量的存 储 要求 , 栈 式存 储 分配要求在 过 程的入口 处 必 须 知道所有的存 储 要求 , 而堆式存 储 分配 则专门负责 在 编译时 或运行 时 模 块 入口 处 都无法确定存 储 要求的数据 结 构的内存分配 , 比如可 变长 度串和 对 象 实 例 . 堆由大片的可利用 块 或空 闲块组 成 , 堆中的内存可以按照任意 顺 序分配和 释 放 .
2
堆和
栈
的比
较
上面的定 义 从 编译 原理的教材中 总结 而来 , 除静 态 存 储 分配之外 , 都 显 得很呆板和 难 以理解 , 下面撇 开 静 态 存 储 分配 , 集中比 较 堆和 栈 :
从堆和 栈 的功能和作用来通俗的比 较 , 堆主要用来存放 对 象的, 栈 主要是用来 执 行程序的 . 而 这种 不同又主要是由于堆和 栈 的特点决定的 :
在 编 程中,例如 C/C++ 中,所有的方法 调 用都是通 过栈 来 进 行的 , 所有的局部 变 量 , 形式参数都是从 栈 中分配内存空 间 的。 实际 上也不是什 么 分配 , 只是从 栈顶 向上用就行 , 就好像工厂中的 传 送 带 (conveyor belt) 一 样 ,Stack Pointer 会自 动 指引你到放 东 西的位置 , 你所要做的只是把 东 西放下来就行 . 退出函数的 时 候,修改 栈 指 针 就可以把 栈 中的内容 销毁 . 这样 的模式速度最快 , 当然要用来运行程序了 . 需要注意的是 , 在分配的 时 候 , 比如 为 一个即将要 调 用的程序模 块 分配数据区 时 , 应 事先知道 这 个数据区的大小 , 也就 说 是 虽 然分配是在程 序运行 时进 行的 , 但是分配的大小多少是确定的 , 不 变 的 , 而 这 个 " 大小多少 " 是在 编译时 确定的 , 不是在运行 时 .
堆是 应 用程序在运行的 时 候 请 求操作系 统 分配 给 自己内存,由于从操作系 统 管理的内存分配 , 所以在分配和 销毁时 都要占用 时间 ,因此用堆的效率非常低 . 但是堆的 优 点在于 , 编译 器不必知道要从堆里分配多少存 储 空 间 ,也不必知道存 储 的数据要在堆里停留多 长 的 时间 , 因此 , 用堆保存数据 时 会得到更大的灵活性。事 实 上 , 面 向 对 象的多 态 性 , 堆内存分配是必不可少的 , 因 为 多 态变 量所需的存 储 空 间 只有在运行 时创 建了 对 象之后才能确定 . 在 C++ 中,要求 创 建一个 对 象 时 ,只需用 new 命令 编 制相 关 的代 码 即可。 执 行 这 些代 码时 ,会在堆里自 动进 行数据的保存 . 当然, 为 达到 这种 灵活性,必然会付出一定的代价 : 在堆里分配存 储 空 间时 会花 掉更 长 的 时间 ! 这 也正是 导 致我 们刚 才所 说 的效率低的原因 , 看来列宁同志 说 的好 , 人的 优 点往往也是人的缺点 , 人的缺点往往也是人的 优 点 ( 晕 ~).
上面的定 义 从 编译 原理的教材中 总结 而来 , 除静 态 存 储 分配之外 , 都 显 得很呆板和 难 以理解 , 下面撇 开 静 态 存 储 分配 , 集中比 较 堆和 栈 :
从堆和 栈 的功能和作用来通俗的比 较 , 堆主要用来存放 对 象的, 栈 主要是用来 执 行程序的 . 而 这种 不同又主要是由于堆和 栈 的特点决定的 :
在 编 程中,例如 C/C++ 中,所有的方法 调 用都是通 过栈 来 进 行的 , 所有的局部 变 量 , 形式参数都是从 栈 中分配内存空 间 的。 实际 上也不是什 么 分配 , 只是从 栈顶 向上用就行 , 就好像工厂中的 传 送 带 (conveyor belt) 一 样 ,Stack Pointer 会自 动 指引你到放 东 西的位置 , 你所要做的只是把 东 西放下来就行 . 退出函数的 时 候,修改 栈 指 针 就可以把 栈 中的内容 销毁 . 这样 的模式速度最快 , 当然要用来运行程序了 . 需要注意的是 , 在分配的 时 候 , 比如 为 一个即将要 调 用的程序模 块 分配数据区 时 , 应 事先知道 这 个数据区的大小 , 也就 说 是 虽 然分配是在程 序运行 时进 行的 , 但是分配的大小多少是确定的 , 不 变 的 , 而 这 个 " 大小多少 " 是在 编译时 确定的 , 不是在运行 时 .
堆是 应 用程序在运行的 时 候 请 求操作系 统 分配 给 自己内存,由于从操作系 统 管理的内存分配 , 所以在分配和 销毁时 都要占用 时间 ,因此用堆的效率非常低 . 但是堆的 优 点在于 , 编译 器不必知道要从堆里分配多少存 储 空 间 ,也不必知道存 储 的数据要在堆里停留多 长 的 时间 , 因此 , 用堆保存数据 时 会得到更大的灵活性。事 实 上 , 面 向 对 象的多 态 性 , 堆内存分配是必不可少的 , 因 为 多 态变 量所需的存 储 空 间 只有在运行 时创 建了 对 象之后才能确定 . 在 C++ 中,要求 创 建一个 对 象 时 ,只需用 new 命令 编 制相 关 的代 码 即可。 执 行 这 些代 码时 ,会在堆里自 动进 行数据的保存 . 当然, 为 达到 这种 灵活性,必然会付出一定的代价 : 在堆里分配存 储 空 间时 会花 掉更 长 的 时间 ! 这 也正是 导 致我 们刚 才所 说 的效率低的原因 , 看来列宁同志 说 的好 , 人的 优 点往往也是人的缺点 , 人的缺点往往也是人的 优 点 ( 晕 ~).
3 JVM 中的堆和 栈
JVM 是基于堆 栈 的虚 拟 机 .JVM 为每 个新 创 建的 线 程都分配一个堆 栈 . 也就是 说 , 对 于一个 Java 程序来 说 ,它的运行就是通 过对 堆 栈 的操作来完成的。堆 栈 以 帧为单 位保存 线 程的状 态 。 JVM 对 堆 栈 只 进 行两 种 操作 : 以 帧为单 位的 压栈 和出 栈 操作。
我 们 知道 , 某个 线 程正在 执 行的方法称 为 此 线 程的当前方法 . 我 们 可能不知道 , 当前方法使用的 帧 称 为 当前 帧 。当 线 程激活一个 Java 方法 ,JVM 就会在 线 程的 Java 堆 栈 里新 压 入一个 帧 。 这 个 帧 自然成 为 了当前 帧 . 在此方法 执 行期 间 , 这 个 帧 将用来保存参数 , 局部 变 量 , 中 间计 算 过 程和其他数据 . 这 个 帧 在 这 里和 编译 原理中的活 动纪录 的概念是差不多的 .
从 Java 的 这种 分配机制来看 , 堆 栈 又可以 这样 理解 : 堆 栈 (Stack) 是操作系 统 在建立某个 进 程 时 或者 线 程 ( 在支持多 线 程的操作系 统 中是 线 程 ) 为这 个 线 程建立的存 储 区域, 该 区域具有先 进 后出的特性。
每 一个 Java 应 用都唯一 对应 一个 JVM 实 例, 每 一个 实 例唯一 对应 一个堆。 应 用程序在运行中所 创 建的所有 类实 例或数 组 都放在 这 个堆中 , 并由 应 用所有的 线 程 共享 . 跟 C/C++ 不同, Java 中分配堆内存是自 动 初始化的。 Java 中所有 对 象的存 储 空 间 都是在堆中分配的,但是 这 个 对 象的引用却是在堆 栈 中分配 , 也 就是 说 在建立一个 对 象 时 从两个地方都分配内存,在堆中分配的内存 实际 建立 这 个 对 象,而在堆 栈 中分配的内存只是一个指向 这 个堆 对 象的指 针 ( 引用 ) 而已。
4 GC 的思考
Java 为 什 么 慢 ?JVM 的存在当然是一个原因 , 但有人 说 , 在 Java 中 , 除了 简单类 型 (int,char 等 ) 的数据 结 构 , 其它都是在堆中分配内存 ( 所以 说 Java 的一切都是 对 象 ) , 这 也是程序慢的原因之一。
我的想法是 ( 应该说 代表 TIJ 的 观 点 ), 如果没有 Garbage Collector(GC), 上面的 说 法就是成立的 . 堆不象 栈 是 连续 的空 间 , 没有 办 法指望堆本身的内存分配能 够 象堆 栈 一 样拥 有 传 送 带 般的速度 , 因 为 , 谁 会 为 你整理 庞 大的堆空 间 , 让 你几乎没有延 迟 的从堆中 获 取新的空 间 呢 ?
这 个 时 候 ,GC 站出来解决 问题 . 我 们 都知道 GC 用来清除内存垃圾 , 为 堆 腾 出空 间 供程序使用 , 但 GC 同 时 也担 负 了另外一个重要的任 务 , 就是要 让 Java 中堆 的内存分配和其他 语 言中堆 栈 的内存分配一 样 快 , 因 为 速度的 问题 几乎是众口一 词 的 对 Java 的 诟 病 . 要达到 这样 的目的 , 就必 须 使堆的分配也能 够 做到象 传 送 带 一 样 , 不用自己操心去找空 闲 空 间 . 这样 ,GC 除了 负责 清除 Garbage 外 , 还 要 负责 整理堆中的 对 象 , 把它 们转 移到一个 远 离 Garbage 的 纯净 空 间 中无 间 隔的排列起来 , 就象堆 栈 中一 样紧 凑 , 这样 Heap Pointer 就可以方便的指向 传 送 带 的起始位置 , 或者 说 一个未使用的空 间 , 为 下一个需要分配内存的 对 象 " 指引方向 ". 因此可以 这样说 , 垃圾收集影响了 对 象的 创 建速度 , 听起来很怪 , 对 不 对 ?
那 GC 怎 样 在堆中找到所有存活的 对 象呢 ? 前面 说 了 , 在建立一个 对 象 时 ,在堆中分配 实际 建立 这 个 对 象的内存 , 而在堆 栈 中分配一个指向 这 个堆 对 象的指 针 ( 引 用 ), 那 么 只要在堆 栈 ( 也有可能在静 态 存 储 区 ) 找到 这 个引用 , 就可以跟踪到所有存活的 对 象 . 找到之后 ,GC 将它 们 从一个堆的 块 中移到另外一个堆的 块 中 , 并 将它 们 一个挨一个的排列起来 , 就象我 们 上面 说 的那 样 , 模 拟 出了一个 栈 的 结 构 , 但又不是先 进 后出的分配 , 而是可以任意分配的 , 在速度可以保 证 的情况下 , Isnt it great?
但是,列宁同志 说 了 , 人的 优 点往往也是人的缺点 , 人的缺点往往也是人的 优 点 ( 再 晕 ~~).GC() 的运行要占用一个 线 程 , 这 本身就是一个降低程序运行性能 的缺陷 , 更何况 这 个 线 程 还 要在堆中把内存翻来覆去的折 腾 . 不 仅 如此 , 如上面所 说 , 堆中存活的 对 象被搬移了位置 , 那 么 所有 对这 些 对 象的引用都要重新 赋值 . 这 些 开销 都会 导 致性能的降低 .
此消彼 长 ,GC() 的 优 点 带 来的效益是否盖 过 了它的缺点 导 致的 损 失 , 我也没有太多的体会 ,Bruce Eckel 是 Java 的支持者,王婆 卖 瓜, 话 不能全信 . 个人 总 的感 觉 是 ,Java 还 是很慢 , 它的 发 展 还 需要 时间 .