2.0引-Java内存区域中,栈内存和堆内存分别装什么,为什么?
- 栈:解决程序的运行问题,即程序如何执行,或者说如何处理数据。
- 堆:解决的是数据存储的问题,即数据怎么放,放在哪儿。
参考链接https://www.cnblogs.com/gdufs/p/6407432.html
2.2运行时数据区
线程隔离的运行时数据区:程序计数器、Java虚拟机栈、本地方法栈
- 程序计数器:当前线程所执行的字节码的行号指示器,由于线程私有,故借助程序计数器线程切换后能恢复到正确的执行位置。
- Java虚拟机栈:每个方法在执行时会创建一个栈帧。每一个方法从调用至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 本地方法栈 ?todo
线程共享的运行时数据区:Java堆、方法区(其中包含运行时常量池)
- Java堆:存放对象实例。是垃圾收集器管理的主要区域。
- 方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。?
- 运行时常量池:是方法区的一部分。?todo
此外,直接内存不属于运行时数据区,但它也可能会产生内存溢出。Java中NIO类可以直接分配堆外内存。
2.3(基于HotSpot虚拟机)对象
对象的创建步骤
- step1:类加载。new指令的符号参数在常量池定位到类的符号引用,检查这个符号引用代表的类是否被加载、解析、初始化过,没有的话首先进行类加载。
- step2:为新对象分配内存。
- 问题一:如何划分可用空间:
- 方法一:指针碰撞。如果虚拟机堆中的内存是规整的,则把堆空闲内存指针移对象大小等大小距离即可。
- 方法二:空闲列表。如果堆内存不规整,就要维护一个空闲列表记录空闲的内存块。
- 问题二:修改指针所指向的位置,如何在并发情况下保证此操作是线程安全的。
- 方法一:对分配内存空间的动作进行同步:虚拟机采用CAS?+失败重试的方式保证更新操作的原子性。
- 方法二:本地线程分配缓冲(TLAB):即每个线程预先在队中分配一小块内存,是内存分配的动作在不同空间进行。只需在分配新TLAB时,进行同步锁定。选择是要用TLAB可通过参数-XX:+/-UseTLAB设定。
- 问题一:如何划分可用空间:
- step3:将分配到的内存空间初始化为零值。
- step4:对对象进行必要的设置。包括这个对象是哪个类的实例等。
- step5:从虚拟机角度,step1-4完成了对象的创建。但从Java程序视角来看,还需要对对象进行初始化(init方法)。
对象的内存布局
HotSpot虚拟机中,对象内存存储=对象头(Mark Word)+实例数据+对齐填充。
其中,对象头包含哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。它是一个非固定的数据结构,根据对象的状态复用自己的存储空间。
对象的访问定位
- 两种,一种是通过句柄访问对象,一种是通过直接指针访问对象。
- 通过句柄访问对象:Java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据地址(位于Java堆)及对象类型数据地址(位于方法区)
- 通过直接指针访问对象:reference中存储的就是对象地址,对象中包括:对象类型数据指针(位于Java堆)+对象实例数据(位于方法区)。
- 对比
- 不同点:就是reference存储的是什么、Java堆中是否有句柄池、对相关类型数据的地址存在句柄池还是对象的对象头中。
- 各自优势:句柄方式:reference中存储的是稳定的句柄地址,当对象被移动reference本身不需要修改;直接指针方式:快,节省了一次指针定位的开销。 - HotSpot使用直接指针访问方式。
2.4OutOfMemoryError异常
虚拟机内存除程序计数器外的其他几个运行时区域都可能发生OutOfMemoryError(OOM)异常。
2.4.1Java堆溢出
- 产生堆溢出:当把堆设置为不自动扩展(设置堆最小值-Xms参数=堆最大值参数-Xmx参数)时,不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么创建的所有对象超过堆的容量限制后,就会产生OutOfMemoryError异常。
-
排查方法:
- 步骤1 通过内存映象分析工具对Dump出来的堆转储快照?进行分析,重点是确认是内存泄漏(指内存中有许多不必要对象)还是内存溢出(内存中的对象都是有必要的)。
- 步骤2 如果是内存泄漏:通过工具,查看泄漏对象到GC Roots的引用链,就可以较准确地定位泄露代码的位置。
- 如果是内存溢出:在物理内存允许的情况下,调大虚拟机的堆参数;此外,检查代码中睡否某些对象生命周期过长,持有状态时间过长?。
2.4.2虚拟机栈和本地方法栈溢出
虚拟机参数:HosSpot虚拟机不区分虚拟机栈和本地方法栈,所以-Xoss参数(设置本地方法栈大小)是无效的。栈容量只由-Xss参数控制。
关于虚拟机栈和本地方法栈,Java虚拟机规范描述了两种异常:已使用的栈空间太大:*Error;内存太小:OutOfMemoryError。但是这本质是对同一件事情的两种描述而已。
-
由栈内存原因产生*Error异常:
- 单线程下:方法一:减少栈内存容量。方法二:定义大量本地变量。单线程下,经测试这两种方法均抛出*Error异常。
-
产生OutOfMemoryError异常:
- 多线程下:通过不断建立线程产生OutOfMemoryError(内存溢出)异常,但这与栈内存容量是否足够大并无关联。事实上,反而每个线程分配的栈内存越大,越容易造成OutOfMemoryError异常。因为OS给每个进程分配的内存(瓜分给:Xmx最大堆容量+MaxPermSize最大方法区容量+最大栈容量(包括虚拟机栈和本地方法栈)+程序计数器占用的(较小,可忽略不计)+虚拟机进程本身耗费的(暂略))是有限制的,所以若每个线程分配的栈内存越大,可以建立的线程数就越少,就越容易在建立线程时把内存耗尽。
-
排查方法:
- 针对*Error,可以阅读错误堆栈,找到问题所在。
- 多线程下,针对OutOfMemoryError,在不能减少线程数或更换64位虚拟机的情况下,则可以通过减少最大堆容量和减少栈容量来换取更多线程。
2.4.3方法区和运行时常量池溢出
- 方法区:
- 造成内存溢出:生成大量类。主流框架当有大量类增强,就可能会造成内存溢出。方法区溢出是常见的内存溢出异常,因为一个类被判定回收的条件非常苛刻。
- 需要注意方法区溢出的场景:动态生成大量类;使用CGLib字节码增强;动态语言;大量JSP的应用;基于OSGi的应用。
- 方法区中的运行时常量池:
- 造成内存溢出:JDK1.6中,String.intern()不断建立常量,且保持引用保证不被GC,会造成内存溢出。JDK1.7以后String.intern()使用引用的方法,不再复制,不会造成内存溢出。
- 排查方法:JDK1.6中,异常会提示OutOfMemoryError:PermGen space,说明是(HotSpot虚拟机)永久代的异常。JDK1.7后开始逐步“去永久代”。
2.4.4 本机直接内存溢出
- 参数: -XX:MaxDirectMemorySize 指定,否则默认与Java堆最大值一样。
- 排查方法:Heap Dump文件? 不会看到明显异常,如果发现OutOfMemoryError后Dump文件很小,程序中又使用了NIO,可以考虑是否是本机直接内存溢出。