第一章 内存管理

本文大部分参考这里

第一章 内存管理

1.1运行时数据区

第一章 内存管理

1.1.1 程序计数器

记录当前线程所执行的字节码行号,用于获取下一条执行的字节码。 (如果正在执行的是本地方法则为空)
当多线程运行时,每个线程切换后需要知道上一次所运行的状态、位置。由此也可以看出程序计数器是每个线程私有的。

1.1.2 java虚拟机栈

第一章 内存管理
虚拟机栈由一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。

={ 栈帧=\begin{cases} 局部变量表\\ 操作数栈\\ 动态链接\\ 方法出口等\\ \end{cases}
每创建一个栈帧压栈,当一个方法执行完毕之后则出栈
这块内存区域也是线程私有的。
1 如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 *Error。
2 若线程执行过程中栈帧大小超出虚拟机栈限制,则会抛出 *Error。
3 若虚拟机栈允许动态扩展,但在尝试扩展时内存不足,或者在为一个新线程初始化新的虚拟机栈时申请不到足够的内存,则会抛出 OutOfMemoryError。
局部变量表:存放编译期可知的各种基本数据类型。

1.1.3 本地方法栈

本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

1.1.4Java堆

java堆存放对象实例,堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。
可利用参数 XmsXmx-Xms -Xmx 进行堆内存控制。 第一个参数设置初始值,第二个参数设置最大值。
这块区域也是垃圾回收器重点管理的区域,由于大多数垃圾回收器都采用分代回收算法,所以堆内存也分为 新生代、老年代,可以方便垃圾的准确回收。
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。这块内存属于线程共享区域。

1.1.5 方法区(JDK1.7)

方法区主要用于存放已经被虚拟机加载的类信息,如常量,静态变量。 这块区域也被称为永久代。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
可利用参数 XX:PermSizeXX:MaxPermSize-XX:PermSize -XX:MaxPermSize 控制初始化方法区和最大方法区大小。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
默认情况下元数据区域会根据使用情况动态调整,避免了在 1.7 中由于加载类过多从而出现 java.lang.OutOfMemoryError:PermGenjava.lang.OutOfMemoryError: PermGen
但也不能无线扩展,因此可以使用XX:MaxMetaspaceSize-XX:MaxMetaspaceSize来控制最大内存。

1.1.6 运行时常量池

运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。

1.1.7 直接内存

直接内存又称为 Direct Memory(堆外内存),它并不是由 JVM 虚拟机所管理的一块内存区域。
有使用过 Netty 的朋友应该对这块并内存不陌生,在 Netty 中所有的 IO(nio) 操作都会通过 Native 函数直接分配堆外内存。
它是通过在堆内存中的 DirectByteBuffer 对象操作的堆外内存,避免了堆内存和堆外内存来回复制交换复制,这样的高效操作也称为零拷贝。
既然是内存,那也得是可以被回收的。但由于堆外内存不直接受 JVM 管理,所以常规 GC 操作并不能回收堆外内存。它是借助于老年代产生的 fullGC 顺便进行回收。同时也可以显式调用 System.gc() 方法进行回收(前提是没有使用 XX:+DisableExplicitGC-XX:+DisableExplicitGC 参数来禁止该方法)。
值得注意的是:由于堆外内存也是内存,是由操作系统管理。如果应用有使用堆外内存则需要平衡虚拟机的堆内存和堆外内存的使用占比。避免出现堆外内存溢出。

1.1.8 常用参数

第一章 内存管理
通过上图可以直观的查看各个区域的参数设置。
常见的如下:
Xms64m-Xms64m 最小堆内存 64m.\n
Xmx128m-Xmx128m 最大堆内存 128m.
XX:NewSize=30m-XX:NewSize=30m 新生代初始化大小为30m.
XX:MaxNewSize=40m-XX:MaxNewSize=40m 新生代最大大小为40m.
Xss=256k-Xss=256k 线程栈大小。
XX:+PrintHeapAtGC-XX:+PrintHeapAtGC 当发生 GC 时打印内存布局。
XX:+HeapDumpOnOutOfMemoryError-XX:+HeapDumpOnOutOfMemoryError 发送内存溢出时 dump 内存。
新生代和老年代的默认比例为 1:2,也就是说新生代占用 1/3的堆内存,而老年代占用 2/3 的堆内存。
可以通过参数 XX:NewRatio=2-XX:NewRatio=2来设置老年代/新生代的比例。

1.2 HotSpot虚拟机对象探秘

1.2.1 对象的创建

  1. 执行类加载过程:见《深入理解Java虚拟机》第七章
  2. 分配内存

={ 为新对象分配内存=\begin{cases} 指针碰撞\\ 空闲列表\\ \end{cases}
考虑并发情况下的线程安全性问题

线={线线(TLAB) 线程安全性问题=\begin{cases} 线程同步\\ 本地线程分配缓冲(TLAB)\\ \end{cases}
3. 分配到的内存空间初始化(不包括对象头)
4. 对对象进行必要的设置
5. 执行方法,对象初始化

1.2.2 对象的内存布局

={Header{:GC线线IDInstanceDatapadding 对象=\begin{cases} 对象头(Header)\begin{cases}自身运行时数据:哈希值,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向是时间戳\\类型指针 \end{cases} \\ 实列数据(Instance Data) \\ 对齐填充(padding) \end{cases}

1.2.3 对象的访问定位

1 使用句柄
2 直接指针