JVM 的内存模型粗略可以分成堆和栈:

  • 堆区分为 堆(Heap)和方法区(Method Area);
  • 栈区分为 程序计数器(Program Counter Register), 虚拟机栈(VM Stack), 本地方法栈(Native Method Stack).

堆区的内存为所有线程共享, 栈区的内存为各个线程独占.

  • 程序计数器 PC Register:

    java 的线程对应 OS 中的线程, java 多线程通过线程轮流切换, 依次分配 CPU 时间来实现. 在某个确定的时刻, 一个 CPU 内核只会执行一个线程. 程序计数器记录了 JVM 在执行本线程时, 字节码指令的位置. 为线程切换后, 让 JVM 知道指令要在哪继续执行, 每个线程都持有各自的程序计数器.

  • 虚拟机栈 VM Stack:

    每个线程维护自己的虚拟机栈. 每执行一个方法, JVM 就同步创建一个栈帧(Stack Frame), 用来存放该方法的局部变量表, 操作数栈, 动态连接, 方法出口等. 执行方法, 栈帧入栈; 方法执行完成, 栈帧出栈.

  • 本地方法栈 Native Method Stack:

    用于执行本地方法的 Stack.

  • 堆 Heap:

    所有对象实例和数组都在堆上分配, 垃圾收集器管理他们的回收工作. 堆可以分配在连续的物理内存上, 也可以分配在不连续的内存空间上, 但堆在逻辑上是连续的. Hotspot 的堆是可以扩展的.

  • 方法区 Method Area:

    JVM 规范把方法区视为堆的一个逻辑部分. 主要存放: 类型信息, 常量, 静态变量, 编译缓存.

Q: 什么情况下会抛出 OutOfMemoryErrorStackOverFlowError?

StackOverFlowError 栈溢出,

  • 深递归耗尽栈空间;

OutOfMemoryError 内存不足.

  • 当堆空间耗尽后(已经达到最大限制而不能再扩容), 再要求实例化对象将报告内存不足;
  • 堆空间耗尽要求扩容, 机器内存不足;
  • 对于栈空间可以扩容的 JVM(Hotspot不可对栈扩容), 要求扩容栈空间时, 机器内存不足;
  • Direct Memory 请求内存时, 机器内存不足;

Q: JVM 如何保证对象创建时Heap的线程安全?

解决线程安全问题一般有乐观锁和悲观锁的方式. 由于对象创建是高频操作, 悲观锁的开销过大, 所以选用乐观锁的方式, 具体来说就是 CAS: Compare and Swap.

另一种方式是为每个线程单独划分一块自用的堆, 即 Thread Local Allocation Buffer, TLAB, 本地线程分配缓冲. 只在初始分配和扩容分配时才需要锁, 在线程内部便没有竞争条件.