二、Java内存区域
运行时数据区域
1.程序计数器
是一个比较小的内存空间。线程私有,可以被看作是当前线程所执行的字节码的行号指示器。
对于Java方法:
- 记录正在执行的虚拟机字节码指令的地址
对于Native方法:
- 如果正在执行的是本地方法则为空。
2.Java虚拟机栈
描述的是Java方法执行的内存模型,每个方法执行的时候同时会创建一个栈帧,保存着局部变量表,操作数栈,动态链接,方法出口等信息。线程私有。
大家把JVM的内存分为堆与栈,这里的栈指的就是Java虚拟机栈,具体是其中的局部变量表,这个表存储了各种基本数据类型、引用的地址(reference类型) 以及代表对象的句柄,returnAddress类型,指向了一条字节码指令的地址。
局部变量表的大小在编译期间就已经分配完。运行期间不会改变。
这个区域有两种异常:
- 如果 线程递归太深,申请的栈的深度超过了虚拟机允许的,会出现 StackOverflowError 异常
- 如果固定长度的栈申请的内存超过了机器本身的物理内存,会出现OutOfMemoryError
3.本地方法栈
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
4.Java堆
线程共享。所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”)。
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
- 新生代(Young Generation)
- 老年代(Old Generation)
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M HackTheJava
5.方法区
线程共享。存储已经加载的类信息,常量,静态变量,即时编译后的代码片段
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
6.运行时常量池
是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
非运行时数据区域
1.直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
内存溢出异常
1.对象创建
当我们new 一个对象的时候发生了什么呢?首先会检查这个指令的参数是否能在常量池中一个类的符号引用,并且检查这个符号引用所代表的类是否已经完全被加载、解析、和初始化过,没有的话必须去进行加载。
在加载过后,虚拟机开始为对象分配内存,对象需要多大内存在类加载后便可确定,两种分配方式:
- 指针碰撞 : Java内存分为两块,被占用的放一边,空闲的放一边,中间的是一个指针,当分配内存的时候,直接把指针往空闲的内存地址偏移需要分配的内存大小即可。
- 适合内存绝对规整
- 空闲列表 :虚拟机维护一个列表,存储的哪个内存空闲,并动态更新
- 适合内存不规整
采用哪种分配方式是看内存回收器是否带有压缩功能,
- 适合内存不规整
- 碰撞指针: Serial ParNew
- 空闲列表: CMS 标记-清除算法
在分配内存的受要考虑并发的情况
虚拟机采用CAS+失败重试的方式保证更新的原子性
内存分配完后开始对对象进行必要的设置,比如这个对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码值。
接下来执行<init>
方法,这样可用的对象完成了。
2.对象的内存布局
对象头
对象头包产两部分信息
第一部分用于存储对象自身的运行时的数据比如:哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID 偏向时间戳
第二部分就是类型指针,用于找到这个实例是哪一个类的实例,但不是所有虚拟机都在对象头上保存类型指针。如果对象是一个Java数组,那么,对象头不还需要保存一个数组大小的数据。
对象的访问定位
-
使用句柄
在堆中开辟出一个句柄池,reference保存的是句柄池的地址,对象句柄池包含两个东西,一个是对象实例数据的指针,一个是到对象类型数据的指针。
优点是:对象移动的时候只需要改变到对象实例数据的指针,reference不用修改
-
使用直接指针修改
reference 保存的是堆中分配的对象实例数据的地址。
优点是在改变对象引用的时候直接修改reference的地址,这样非常快。
3.堆溢出
原因就是 堆有大量的实例数据,并且GC Roots 到这些对象有引用链,保证不会被垃圾回收器回收。对象到达最大堆的容量后就会产生内存溢出。
异常信息: java.lang.OutOfMemoryError 后面会跟着 java heap space
检查方法就是 首先通过内存影像分析工具 堆Dump 出来的堆转储快照分析,看是内存泄露还是内存溢出
-
内存泄露的话进一步通过工具 通过GC Roots 引用链到泄露对象的路径查找定位代码。
-
如果是内存溢出,那么尝试增大堆的内存(-Xmx -Xms)长时间小代码运行时的内存消耗
4.虚拟机栈和本地方法栈溢出
- 栈请求的深度大于虚拟机允许的最大深度,抛出
StackOveflowError
- 虚拟机在扩展栈的时候没有申请到足够的内存 抛出
OutOfMemoryError
StackOveflowError
解决方式:减小栈容量与最大堆的内存,这样就可以获得更多的线程了。
原因:操作系统给每个进程的内存是有限制的,假如总共4GB,2G给堆内存,抛却直接内存,剩下的2G给本地方法栈与虚拟机栈,那么每个线程获得的栈容量越大,可以建立线程的数量就越少。
5.方法区与运行时常量池溢出
运行时常量池溢出:
String.intern() 如果字符串常量池有这个字符串对象了,那么返回这个对象,没有就添加到字符串常量池中,并返回这个对象的引用。
String str1 = new StringBuilder("Java").append("se").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("计算机").toString();
System.out.println(str2.intern() ==str2);
true
false
方法去溢出:
方法区存放的是Class的相关信息,比如类名,访问修饰符,常量池,字段描述,方法描述。
6.本地直接内存溢出
略。。。