java内存区域与内存溢出异常
2017-03-30 12:04
183 查看
一、java虚拟机所管理的内存包括几个运行数据区域
程序计数器
(1)在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指 令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
(2)每一个线程都有一个独立的程序计数器,线程私有的。
(3)程序计数器记录的正在执行线程的一个java方法的虚拟机的字节码指令的地址。
(4)程序计数器内存区是唯一一个内存区域不会抛出OutOfMemoryError错误的区域。
java虚拟机栈
(1)线程私有的,生命周期与线程相同。
(2)java虚拟机栈中存储正在执行的方法的局部变量、操作数栈、动态链接、方法出口,出入栈过程对应于方法执行过程。
(3)局部变量表存放编译期可知的各种基本数据类型、对象引用和returnAddress类型,只有long、double占两个局部变量空间,并且局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。
(4)请求栈深度大于虚拟机所允许的深度,抛出StackOverflowError错误,扩展时无法申请到足够的内存抛出OutOfMemoryError错误。
本地方法栈
(1)本地方法栈和虚拟机栈的区别:虚拟机栈为虚拟机执行java方法服务;本地方法栈为虚拟机使用到的Native方法服务。
(2)HotSpot虚拟机把虚拟机栈和本地方法栈合二为一。
(3)请求栈深度大于虚拟机所允许的深度,抛出StackOverflowError错误,扩展时无法申请到足够的内存抛出OutOfMemoryError错误。
java堆
(1)java虚拟机中管理内存最大的一块,被所有线程共享,在虚拟机启动时创建,存放对象实例,几乎所有对象实例和数组都在这里分配内存(JIT编译器技术发展和逃逸技术成熟,使这条也不是那么绝对)。
(2)java堆是垃圾回收器管理的主要区域,称作GC堆。从内存回收角度可划分“新生代”、“老年代”,或者细致一点“Eden空间”、“From Survivor空间”、“To Survivor空间”;从内存分配角度,可以划分多个线程私有的分配缓冲区(TLAB)。
(3)堆中没有内存完成实例分配并且堆也无法再扩展时,抛出OutOfMemoryError错误。
方法区
(1)方法区是被所有线程共享的内存区域,被用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
(2)使用永久代来实现方法区,这样hotspot垃圾收集器可以像管理java堆一样管理这部分内存。但是永久代实现方法区更容易遇到内存溢出问题;并且极少数方法会因这个原因导致不同虚拟机下有不同表现,因此jdk1.7的hotspot已把字符串常量池移除方法区。
(3)方法区的内存回收目标主要是针对常量池的回收和对类型的卸载。
(4)方法区无法满足内存分配需求时候抛出OutOfMemoryError错误。
运行时常量池
(1)运行时常量池用于存放编译期生成的各种字面量和符号引用,还会把翻译出来的直接引用存储在其中。
(2)运行时常量池相对于Class文件常量池具备动态性,运行期间也可能将新的常量放入池中。
(3)常量池无法再申请到内存时抛出OutOfMemoryError错误。
直接内存
(1)jdk1.4中加入NIO使用直接内存
(2)所有区域内存加在一起大于物理内存限制的时候抛出OutOfMemoryError错误。
二、HotSpot虚拟机对象的奥秘
对象的创建
(1)对象创建的过程:虚拟机遇到一条new指令,首先检查在常量池中能否定位到这个类的引用,并且检查这个类是否已被加载、解析、初始化过;如果没有,则执行类加载过程;然后为对象在java堆上进行内存分配,分配的方式有:指针碰撞和空闲列表;内存分配完成后,要对分配的内存空间初始化成零值;然后虚拟机对对象进行设置(包括设置一些对象头信息);最后执行init方法
(2)对象在内存分配的时候如何保证线程安全:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;每个线程在java堆上面预先分配一小块内存,称为本地线程分配缓冲区(TLAB),只有在TLAB使用完,在分配新的TLAB的时候才会使用同步锁定
对象内存布局
(1)HotSpot虚拟机中,对象在内存中的存储布局可以分为3块区域:对象头、实例数据和对其填充。
(2)对象头分为两部分信息:用于存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分在32位和64位虚拟机中数据长度分别为32bit、64bit,并且这个长度是可变分配的;类型指针,虚拟机通过这个指针来确定这个对象是哪个类实例。
(3)实例数据部分是对象真正存储的有效信息,也是程序代码中定义的各种类型字段内容。这部分存储顺序会受到虚拟机分配策略参数和字段在java源码中定义顺序影响。hotspot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops,从分配策略看相同宽度的字段被分配到一起。在满足这个前提下,父类定义的变量出现在子类之前,如CompactFields参数值为true,那么子类中较窄的变量也可能会插入到父类变量的空隙中。
(4)对象的大小必须是8字节的整数倍,对象头正好是8字节的整数倍(1倍或者2倍),因此当实例数据部分没有对齐时候,需要通过对其填充来补全。
对象的访问定位
(1)在栈中定位访问堆中的对象有两种方式:句柄式,java堆中划出一块句柄池,reference中直接存储对象的句柄地址,而句柄地址中包含对象实例数据的指针(指向实例池)和对象类型数据的指针(指向方法区)分别指各自的具体地址信息,句柄的最大的好处就是reference中存储是稳定的句柄地址,对象被移动时只改变对象中的实例数据指针,reference不变;直接指针访问式,reference中存储的直接就是对象地址,它最大的好处就是速度快,节省一次指针定位的时间开销,hotspot就是采用这种方式访问的。
三、OutOfMemoryError异常
java堆溢出
虚拟机栈和本地方法栈溢出
方法区和运行时常量池溢出
本机直接内存溢出
程序计数器
(1)在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指 令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
(2)每一个线程都有一个独立的程序计数器,线程私有的。
(3)程序计数器记录的正在执行线程的一个java方法的虚拟机的字节码指令的地址。
(4)程序计数器内存区是唯一一个内存区域不会抛出OutOfMemoryError错误的区域。
java虚拟机栈
(1)线程私有的,生命周期与线程相同。
(2)java虚拟机栈中存储正在执行的方法的局部变量、操作数栈、动态链接、方法出口,出入栈过程对应于方法执行过程。
(3)局部变量表存放编译期可知的各种基本数据类型、对象引用和returnAddress类型,只有long、double占两个局部变量空间,并且局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。
(4)请求栈深度大于虚拟机所允许的深度,抛出StackOverflowError错误,扩展时无法申请到足够的内存抛出OutOfMemoryError错误。
本地方法栈
(1)本地方法栈和虚拟机栈的区别:虚拟机栈为虚拟机执行java方法服务;本地方法栈为虚拟机使用到的Native方法服务。
(2)HotSpot虚拟机把虚拟机栈和本地方法栈合二为一。
(3)请求栈深度大于虚拟机所允许的深度,抛出StackOverflowError错误,扩展时无法申请到足够的内存抛出OutOfMemoryError错误。
java堆
(1)java虚拟机中管理内存最大的一块,被所有线程共享,在虚拟机启动时创建,存放对象实例,几乎所有对象实例和数组都在这里分配内存(JIT编译器技术发展和逃逸技术成熟,使这条也不是那么绝对)。
(2)java堆是垃圾回收器管理的主要区域,称作GC堆。从内存回收角度可划分“新生代”、“老年代”,或者细致一点“Eden空间”、“From Survivor空间”、“To Survivor空间”;从内存分配角度,可以划分多个线程私有的分配缓冲区(TLAB)。
(3)堆中没有内存完成实例分配并且堆也无法再扩展时,抛出OutOfMemoryError错误。
方法区
(1)方法区是被所有线程共享的内存区域,被用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
(2)使用永久代来实现方法区,这样hotspot垃圾收集器可以像管理java堆一样管理这部分内存。但是永久代实现方法区更容易遇到内存溢出问题;并且极少数方法会因这个原因导致不同虚拟机下有不同表现,因此jdk1.7的hotspot已把字符串常量池移除方法区。
(3)方法区的内存回收目标主要是针对常量池的回收和对类型的卸载。
(4)方法区无法满足内存分配需求时候抛出OutOfMemoryError错误。
运行时常量池
(1)运行时常量池用于存放编译期生成的各种字面量和符号引用,还会把翻译出来的直接引用存储在其中。
(2)运行时常量池相对于Class文件常量池具备动态性,运行期间也可能将新的常量放入池中。
(3)常量池无法再申请到内存时抛出OutOfMemoryError错误。
直接内存
(1)jdk1.4中加入NIO使用直接内存
(2)所有区域内存加在一起大于物理内存限制的时候抛出OutOfMemoryError错误。
二、HotSpot虚拟机对象的奥秘
对象的创建
(1)对象创建的过程:虚拟机遇到一条new指令,首先检查在常量池中能否定位到这个类的引用,并且检查这个类是否已被加载、解析、初始化过;如果没有,则执行类加载过程;然后为对象在java堆上进行内存分配,分配的方式有:指针碰撞和空闲列表;内存分配完成后,要对分配的内存空间初始化成零值;然后虚拟机对对象进行设置(包括设置一些对象头信息);最后执行init方法
(2)对象在内存分配的时候如何保证线程安全:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;每个线程在java堆上面预先分配一小块内存,称为本地线程分配缓冲区(TLAB),只有在TLAB使用完,在分配新的TLAB的时候才会使用同步锁定
对象内存布局
(1)HotSpot虚拟机中,对象在内存中的存储布局可以分为3块区域:对象头、实例数据和对其填充。
(2)对象头分为两部分信息:用于存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分在32位和64位虚拟机中数据长度分别为32bit、64bit,并且这个长度是可变分配的;类型指针,虚拟机通过这个指针来确定这个对象是哪个类实例。
(3)实例数据部分是对象真正存储的有效信息,也是程序代码中定义的各种类型字段内容。这部分存储顺序会受到虚拟机分配策略参数和字段在java源码中定义顺序影响。hotspot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops,从分配策略看相同宽度的字段被分配到一起。在满足这个前提下,父类定义的变量出现在子类之前,如CompactFields参数值为true,那么子类中较窄的变量也可能会插入到父类变量的空隙中。
(4)对象的大小必须是8字节的整数倍,对象头正好是8字节的整数倍(1倍或者2倍),因此当实例数据部分没有对齐时候,需要通过对其填充来补全。
对象的访问定位
(1)在栈中定位访问堆中的对象有两种方式:句柄式,java堆中划出一块句柄池,reference中直接存储对象的句柄地址,而句柄地址中包含对象实例数据的指针(指向实例池)和对象类型数据的指针(指向方法区)分别指各自的具体地址信息,句柄的最大的好处就是reference中存储是稳定的句柄地址,对象被移动时只改变对象中的实例数据指针,reference不变;直接指针访问式,reference中存储的直接就是对象地址,它最大的好处就是速度快,节省一次指针定位的时间开销,hotspot就是采用这种方式访问的。
三、OutOfMemoryError异常
java堆溢出
/** * VM Args: -Xms20m -Xmx20m -XX:+HeadDumpOnOutOfMemoryError * @author shier *将堆的最小值-Xms参数和最大值-Xmx参数设置为一样即可避免堆自动扩展, *-XX:+HeadDumpOnOutOfMemoryError可以让虚拟机出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析 */ public class HeapOOM { static class OOMObject{ } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while(true){ list.add(new OOMObject()); } } }
虚拟机栈和本地方法栈溢出
/** * VM Args:-Xss2M * @author shier * 如果建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少 * 最大堆和减少栈容量来换取更多的线程 */ public class JavaVMStackOOM { private void dontStop(){ while(true){ } } public void stackLeakByThread(){ while(true){ Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }
方法区和运行时常量池溢出
/** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M * @author shier * 在经常动态生成大量Class的应用中,需要特别注意类的回收状况,本例中使用类CGLIB字节码增强和动态语言 * 常见的方法区溢出异常还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、 * 基于OSGi(即使是同一个类文件,被不同的加载器加载也会视为不同的类) */ public class JavaMethodAreaOOM { public static void main(String[] args) { while(true){ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor(){ public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy){ return proxy.invokeSuper(obj,args); } }); enhancer.create(); } } static class OOMObject{ } }
本机直接内存溢出
/** * VM Args: -Xmx20m -XX:MaxDirectMemorySize=10m * @author shier * DirectMemoryOOM容量可以通过-XX:MaxDirectMemorySize=10m指定,如果不指定就和java堆最大值一样 */ public class DirectMemoryOOM { private static final int SIZE = 1024 * 1024; public static void main(String[] args) { Field field = Unsafe.class.getDeclaredFields()[0]; field.setAccessible(true); Unsafe unsafe = field.get(null); while(true){ unsafe.allocateMemory(SIZE); } } }
相关文章推荐
- Java内存区域与内存溢出异常
- java虚拟机理解 ---02 java 内存区域 与 内存溢出异常
- Java内存区域详述和内存溢出异常
- Java 内存区域与内存溢出异常
- 第二章 java内存区域与内存溢出异常
- Java内存区域与内存溢出异常
- java 内存区域与内存溢出异常
- 第2章 Java内存区域与内存溢出异常
- JVM之Java内存区域与内存溢出异常
- Java内存区域与内存溢出异常
- Java内存区域与内存溢出异常
- 读书笔记——深入理解java虚拟机第2章(java内存区域与内存溢出异常)
- Java内存区域和内存溢出异常
- java内存区域与内存溢出异常
- java内存区域与内存溢出异常(1)
- java内存区域和内存溢出异常
- Java内存区域与内存溢出异常
- Java内存区域与内存溢出异常
- 《深入理解Java虚拟机》读书笔记——Java内存区域与内存溢出异常
- Java内存区域详述内存溢出异常