<导航

Java内存模型(简版)

Java 运行时的内存划分

程序计数器

记录当前线程所执行的字节码行号,用于获取下一条执行的字节码。

当多线程运行时,每个线程切换后需要知道上一次所运行的状态、位置。由此也可以看出程序计数器是每个线程私有的

虚拟机栈

虚拟机栈由一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。

每一个栈帧由局部变量区操作数栈等组成。每创建一个栈帧压栈,当一个方法执行完毕之后则出栈。

虚拟机栈线程私有,生命周期与线程相同。

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

  • 如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 StackOverflowError
  • 若线程执行过程中栈帧大小超出虚拟机栈限制,则会抛出 StackOverflowError
  • 若虚拟机栈允许动态扩展,但在尝试扩展时内存不足,或者在为一个新线程初始化新的虚拟机栈时申请不到足够的内存,则会抛出 OutOfMemoryError

这块内存区域也是线程私有的。

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。

其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

动态链接:Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。

方法返回:无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。

Java 堆

Java 堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。

可利用参数 -Xms -Xmx 进行堆内存控制。

这块区域也是垃圾回收器重点管理的区域,由于大多数垃圾回收器都采用分代回收算法,所有堆内存也分为 新生代老年代,可以方便垃圾的准确回收。

这块内存属于线程共享区域。

方法区(JDK1.7)

方法区主要用于存放已经被虚拟机加载的类信息,如常量,静态变量。 这块区域也被称为永久代

可利用参数 -XX:PermSize -XX:MaxPermSize 控制初始化方法区和最大方法区大小。

方法区与堆有很多共性:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。

正因为如此相像,Java虚拟机规范把方法区描述为堆的一个逻辑部分,但目前实际上是与Java堆分开的(Non-Heap)。

方法区个性化的是,它存储的是已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是回收确实是有必要的。

元数据区(JDK1.8)

在 JDK1.8 中已经移除了方法区(永久代),并使用了一个元数据区域进行代替(Metaspace)。

默认情况下元数据区域会根据使用情况动态调整,避免了在 1.7 中由于加载类过多从而出现 java.lang.OutOfMemoryError: PermGen

但也不能无限扩展,因此可以使用 -XX:MaxMetaspaceSize来控制最大内存。

运行时常量池

运行时常量池是方法区的一部分,其中存放了一些符号引用。当 new 一个对象时,会检查这个区域是否有这个符号的引用。

直接内存

直接内存又称为 Direct Memory(堆外内存),它并不是由 JVM 虚拟机所管理的一块内存区域。

有使用过 Netty 的朋友应该对这块并内存不陌生,在 Netty 中所有的 IO(nio) 操作都会通过 Native 函数直接分配堆外内存。

它是通过在堆内存中的 DirectByteBuffer 对象操作的堆外内存,避免了堆内存和堆外内存来回复制交换复制,这样的高效操作也称为零拷贝

既然是内存,那也得是可以被回收的。但由于堆外内存不直接受 JVM 管理,所以常规 GC 操作并不能回收堆外内存。它是借助于老年代产生的 fullGC 顺便进行回收。同时也可以显式调用 System.gc() 方法进行回收(前提是没有使用 -XX:+DisableExplicitGC 参数来禁止该方法)。

值得注意的是:由于堆外内存也是内存,是由操作系统管理。如果应用有使用堆外内存则需要平衡虚拟机的堆内存和堆外内存的使用占比。避免出现堆外内存溢出。

常用参数

通过上图可以直观的查看各个区域的参数设置。

常见的如下:

  • -Xms64m 最小堆内存 64m.

  • -Xmx128m 最大堆内存 128m.

  • -XX:NewSize=30m 新生代初始化大小为30m.

  • -XX:MaxNewSize=40m 新生代最大大小为40m.

  • -Xss=256k 线程栈大小。

  • -XX:+PrintHeapAtGC 当发生 GC 时打印内存布局。

  • -XX:+HeapDumpOnOutOfMemoryError 发送内存溢出时 dump 内存。

新生代和老年代的默认比例为 1:2,也就是说新生代占用 1/3的堆内存,而老年代占用 2/3 的堆内存。

可以通过参数 -XX:NewRatio=2 来设置老年代/新生代的比例。

 

参考文章:https://github.com/crossoverJie/JCSprout/blob/master/MD/MemoryAllocation.md

 

posted @ 2020-09-04 14:08  字节悦动  阅读(99)  评论(0)    收藏  举报