2.自动内存管理
2.1 概述
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。
C++需要对构建的每个对象的生命周期进行管理和维护。
Java把对象的生命周期(内存管理)交给了JVM,简化编码,但一旦出现问题则很难排查。
2.2 运行时数据区
Java运行时将内存分为不同区域以方便进行内存管理(例如垃圾回收)。
分为线程私有和线程共享
线程私有
- 程序计数器
- 本地方法栈
- 虚拟机栈
线程共享
- 堆
- 方法区
2.2.1 程序计数器
程序计数器(Program Counter Register)是唯一一块不会产生OOM的区域
功能:
- 在线程切换时保存现场。在现场恢复运行时恢复现场
- 实现语句的判断分支功能
2.2.2 Java虚拟机栈
生命周期与线程相同,描述Java方法执行的线程内存模型
一个方法执行从开始到结束与一个虚拟机栈的**栈帧(Stack Frame)**从入栈到出栈相同
栈帧中存储了
- 局部变量表
- 操作数栈
- 动态连接
- 方法出口
JVM可被笼统的划分为堆和栈这里的占据是只Java虚拟机栈,或者说指虚拟机栈中的局部变量表部分
局部变量表存放了两种数据
-
基本数据类型(primitive)
直接指向基本数据类型的值
-
对象引用
对象引用两种实现
- 直接指向对象在内存中的起始地址,这就要求对象在对象头中记录对象所述的class信息
- 句柄池,由句柄池去指向对象及对象的class信息,可进行解耦减少垃圾回收导致的对象移动对引用产生影响
产生OOM
-
方法不断循环调用自身并且非尾递归造成栈帧越界
stackOverFlow
-
若栈支持可扩展,并且无法申请到足够的内存,产生OutOfMemoryError
2.2.3 本地方法栈
调用本地方法服务
《Java虚拟机规范》没有对本地方法栈中方法使用的语言、方式做强制规定,在Hotspot中将本地方法栈与虚拟机栈合二为一。
2.2.4 Java堆
线程共享的内存区域,在虚拟机启动时创建。
几乎所有实例都在这里被分配
思考:若堆是线程共享,那么线程私有变量如何存放,threadlocal中如何实现线程私有变量
推论:在堆中开辟线程私有的空间
如果从分配内存的角度来看,所有线程共享的Java堆中可划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配效率。
在后面会提到对象初始化时的内存分配策略:指针碰撞或空闲列表
Java堆可通过-Xms
或-Xmx
来指定大小,两者可设置成一样避免堆扩容造成的性能浪费。
堆空间不足抛出OutOfMemoryError异常。
2.2.5 方法区
方法区(Method Area)与Java堆一样是线程共享区域,用于存储已被虚拟机加载的类型信息,静态变量,常量,即时编译器编译后的代码缓存等数据。
方法区可视作存储不易变化的对象的地方,例如
- 常量
- 多次GC仍未被回收的对象
- class信息
- etc
永久代≠方法区
只不过hotspot将分代设计扩展至方法区或者说用永久代实现方法区,方便与堆一样管理方法区内存
方法区空间不足OOM
jdk8后完全废弃永久代概念,用元空间(Meta Space)代替
2.2.6 运行时常量池
方法区的一部分
用于存放class文件除了基础信息还有常量池表(Constant Pool Table)
常量池空间不足OOM
2.2.7 直接内存
NIO、unsafe都可直接分配内存导致OOM
2.3 对象探秘
2.3.1对象的创建
- 在常量池找到合适的class信息
- 判断能否被实例化
- 根据内存情况(内存情况由垃圾收集器对应的收集算法导致)决定如何分配内存
- 指针碰撞,使用带整理的算法(标记-复制、标记-整理)通过中间指针将内存划分为已分配和未分配,对象内存分配简单,只需移动指针
- 空闲列表,使用不带整理的算法(标记-清除),由于算法产生内存碎片所以不适合指针碰撞,需要维护一块空闲列表让对象的内存分配在逻辑上的连续的
- 分配的内存空间初始化
- 设置对象头(hashcode,分代年龄,锁,class等信息)
- 执行对象初始化方法
- 返回对象指针
指针碰撞带来的问题:多线程情况下分配内存产生并发问题
解决方案
-
CAS 异步转同步
Atomic::cmpxchg
-
每个线程分配一块自己的内存空间(Thread Local Allocation Buffer),只有用完了才需要采用同步机制扩容
通过命令-XX: +/-UseTLAB
控制
2.3.2 对象的内存布局
三部分组成
- 对象头(Header)
- 对象自身运行数据(Mark Word)
- HashCode
- GC分代信息
- 锁状态标志位
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- class信息
- 对象自身运行数据(Mark Word)
- 实例数据(Instance Data)
- 对其填充(Padding)
2.3.3 对象的访问定位
reference
-
句柄池
解耦,避免GC堆引用产生影响
-
直接内存
简单,速度快,hotSpot主要采用
2.4 OutOfMemory异常
2.4.1 Java堆溢出
不断创建对象,并且保持对象被可达性
要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析。第一步首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。图2-5显示了使用Eclipse Memory Analyzer打开的堆转储快照文件。
2.4.2虚拟机栈和本地方法栈溢出
栈容量-Xss
设定
-
栈超出深度 stackOverFlow
方法的递归调用(非尾递归) 造成广度溢出
某个方法调用的primitive参数太多造成宽度溢出
-
栈允许扩容(HotSpot不支持扩容)但无法申请足够内存OutOfMemory
2.4.3方法区和运行时常量池溢出
运行时常量池是方法区的一部分
方法区=运行时常量池+永久代
通过String.value(i++).intern()
并限制永久代大小导致OOM
字符串常量池在JDK7之后放到堆中,用-XX:PermSize=6M -XX:MaxPermSize=6M
限制永久代大小用上面方法无效,需限制堆大小
String对象调用intern()
方法会将对象指向某个常量池引用
@Test
public void test8(){
// jdk8
// true
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
// false
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
jdk6 双false
jdk7 true false
原因:jdk6 intern
将首次遇到字符放入常量池并返回常量池引用,而StringBuilder#toString
是new String代表是在堆上分配空间,因此两者不同是false
jdk7之后字符串不需要拷贝到永久代,常量池在堆中因此是true
java
这个字符串特殊
这是因为“java”[插图]这个字符串在执行String-Builder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,“计算机软件”这个字符串则是首次出现的,因此结果返回true。
方法区溢出可通过反射循环构建方法实现
jdk8后前面的举例很难产生异常因为把方法区放到了元空间中
可通过设置参数来控制元空间大小
·-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。·-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。·-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。
2.4.4 本机直接内存溢出
直接内存(Direct Memory)可通过-XX:MaxDirectMemorySize
指定否则与-Xmx
一样
可通过unsafe直接分配内存
小结
- 讲述内存区域划分
- 对象创建过程
- OOM
OOM各情况
- 栈溢出 stackOverFlow OOM
- 堆溢出
- 方法区溢出,运行时常量池溢出
- 线程过多
- 直接内存溢出