什么是内存管理
对内存空间进行分配和回收,即内存动态分配和垃圾回收
Java:虚拟机自动管理
C/C++:程序员手动管理
!!自动管理相比于手动管理不容易出现内存泄露和内存溢出,但仍然存在内存泄漏和溢出的问题
虚拟机规范足够严谨也足够宽松。严谨是为了正确性,宽松是为了实现的灵活性,即规范只是要求具备哪些功能,而实现请自由发挥
JVM运行时数据区域
1.模型图
题外话:为什么要划分区域和分类——为了方便管理
- 线程私有:程序计数器、栈(虚拟机栈、本地方法栈)
- 线程共享:方法区、堆
2.程序计数器
当前线程执行的字节码的行号指示器。
- 一块较小内存空间
- 线程私有
- 执行java方法,计数器值为字节码指令地址
- 执行本地方法,计数器值为空
- 唯一不存在内存溢出的区域(OOM)
3.Java虚拟机栈
Java方法执行的内存模型。一个线程对应一个线程栈,一个方法对应线程栈里面的一个栈帧,方法的调用到执行结束对应栈帧的入栈和出栈。
- 虚拟机栈为线程私有
- 栈帧的组成
- 局部表量表
- 操作数栈
- 动态链接
- 方法出口
- ……
- 局部变量表存储的数据类型
- 基本数据类型
- 引用数据类型(对象起始地址、对象句柄、间接指向对象的地址)
- returnAddress(指向字节码指令的地址)
- 存在两种异常
- OutOfMemoryError
- StackOverflowError
- 分类
- 固定大小栈
- 可扩展栈
- 大部分虚拟机都是可扩展栈(-Xss:虚拟机启动时,每个java线程栈的大小)
- 固定大小栈超过最大栈深请求会报错StackOverflowError;可扩展栈扩展时申请不到足够内存会报错OutOfMemoryError
4.本地方法栈
本地方法的执行模型。
- 与虚拟机栈类似,只不过执行的方法是本地方法(-Xoss:虚拟机启动时,每个本地线程栈的大小)
- 虚拟机规范中没有规定其语言、数据结构、执行方式,虚拟机可自由实现
- hotspot虚拟机栈将本地方法栈与虚拟机栈合二为一(-Xoss这个参数在hotspot中无效)
5.堆
存储对象实例和数组。
- 所有线程共享
- 内存占用最大,几乎所有的对象实例和数组都存放在这里(几乎:因为即时编译器和逃逸分析成熟,使得有了栈上分配和标量替换等优化技术,使得对象数组可以分配在栈上)
- 垃圾回收器管理的主要区域(GC只针对线程共享,方法区的GC收效甚微)
- 区域划分:新生代+ 老年代 [ + 永久代 ]
- 虽说这个区域线程共享,但也存在线程私有的概念(线程私有分配缓存区TLAB thread local allocation buffer ;-XX:+/-UseTLAB来决定是否使用TLAB),是为了让同一线程需要存储的对象尽可能放在物理位置相近的地方,使得使用和回收的时候更加方便和高效
- 虚拟机堆在物理内存上可以不连续,但在逻辑内存行一定连续
- 分类
- 固定大小堆
- 可扩展堆(-Xmx:虚拟机运行时内存最大大小 -Xms:虚拟机启动时内存大小)
- 当对象分配不到足够内存空间就会出现OutOfMemoryError
6.方法区
存储已被虚拟机加载的类信息、常量、静态变量、即时编译代码等数据。
- 线程共享
- 有些虚拟机将方法区用永久代的方式实现,为了简化内存管理(-XX:MaxPermSize 永久代的最大占用内存大小)。永久代意味着内存上限是虚拟机规定的永久代内存大小,而没有永久代意味着本地内存大小是上限
- JRockit和J9就没有永久代的概念
- hotspot的方法区现在已经部分使用虚拟机内存部分使用本地内存了,如jdk1.7将字符串常量池移到了本地内存中
- 方法区物理上可以不连续,但逻辑上连续
- 方法区的GC主要针对常量池和类卸载,而类卸载的条件相当苛刻
- 方法区可以先择不进行垃圾回收
- 无法申请到足够内存是会出现OutOfMemoryError
- 运行时常量池
方法区的一部分,存储字面量和符号引用,具体细节规范内没有明确写出。
- 区别于class文件中的常量池,它具有动态性,会随着程序执行而新增常量
- String的intern()方法就是从常量池直接找对象,存在就返回,不存在创建并返回
- 受方法区内存大小的限制,也存在OutOfMemoryError
7.直接内存
不属于虚拟机运行时数据区,是物理内存。
- 存在OutOfMemoryError
- 如果内存溢出,dump文件很小,且程序直接或者间接使用了NIO,则考虑直接内存OOM
- jdk1.4提出了NIO(new io),基于通道、缓存,实现将对象实例直接分配在虚拟机内存外的本地内存中,并在堆中创建一个DirectByteGBuffer对象来引用直接内存中存的对象实例,避免了在堆和直接内存中来回的对象复制
- Xmx越大,直接内存越小
JVM内存异常
1.内存异常
- 内存溢出:内存不够用
- 内存泄漏:垃圾回收机制无法回收的dead对象
2.Java虚拟机中存在的两种内存异常
- OutOfMemoryError:申请内存时,内存不够用
- 在堆中,新生成一个对象需要分配内存。一般情况下(除非对象特别大,直接分配在老年代),先从新生代分配内存空间。如果内存不够用,则进行垃圾回收;如果GC后还不够用(扩容,扩容失败或扩容后还不够用),则抛异常。
- 在栈中,申请栈空间时内存不够用,则先进行扩容,扩容失败或扩容后仍然不够用,则抛异常。
- 方法区类似于堆。
- StackOverflowError:超过最大栈深
- 栈帧大小超过该线程栈剩余空间大小且无法扩容时,抛异常。
3.JVM的“内存”
- JVM的“内存”总空间 = 栈大小 + 方法区大小 + 堆大小 + 程序计数器大小(相比其它来说过于小,可忽略)
- 基本的内存调控思路
- 总的物理内存一定,若JVM内存越大,直接内存就越小
- 堆空间越大,栈空间越小
- 希望线程数多,则减小最大堆大小以扩大栈空间大小,同时减少栈容量即每个线程占用空间
- 方法区也可以调小来扩大其它内存空间
参数
- -Xms:堆的最小值
- -Xmx:堆的最大值
- -XX:+HeapDumpOnOutOfMemoryError:让虚拟机在内存溢出时dump出内存块的转储快照以便事后分析
- -Xss:线程栈的初始大小
- -Xoss:本地线程栈的初始大小
- -XX:MaxDirectMemorySize:指定直接内存容量,如果不指定默认与最大堆内存一样大
- -XX:MaxPermSize:永久代的最大值