jvm在执行java程序时会把它所管理的内存划分为若干个不同的数据区域.这些区域有各自的用途,以及和创建和销毁时间,有的区域随着虚拟机进程的启动而一直存在,有的区域则是依赖用户线程的启动和结束而创建和销毁.
是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器.
由于java虚拟机的多线程是通过线程轮换,分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(一个内核)都只会执行一条线程中的指令.因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,称这类区域为"线程私有".
与程序计数器一样,java虚拟机栈也是线程私有的,生命周期与线程相同.虚拟机栈描述的是java方法执行的线程内存模型:每个方法被执行的时候,java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息.每一个方法在被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程.
局部变量表存放了编译期可知的各种jvm基本数据类型,对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址).
这个内存区域有两类异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果java虚拟机栈容量可以动态扩展,当展扩展时无法申请到足够的内存会抛出OOM.
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用的本地方法服务.
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OOM.
java堆是虚拟机所管理内存中最大的一块.java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建.此内存区域的唯一目的就是存放对象实例.
在十年之前(以G1收集器的出现为分界),作为业界绝对主流的HotSpot虚拟机,
它内部的垃圾收集器全部都基于"经典分代"来设计,需要新生代,老年代收集器搭配才能工作,到了今天,垃圾收集技术与十年前已不可同日而语,HotSpot里面也出现了不采用分代设计的新垃圾收集器.
java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放.
当前主流的java虚拟机都是按照可扩展来实现的,如果在java堆中没有内存完成实例分配,并且堆也无法再扩展时,jvm会抛出OOM.
方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据.
永久代,当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot的垃圾收集器能够像管理java堆一样管理这部分内存,但这种设计导致了java应用更容易遇到内存溢出的问题(永久代内存有上限,即使不设置也有默认大小),到了JDK8,完全废弃了永久代的概念,改用在本地内存中实现的元空间来代替.
如果方法区无法满足新的内存分配需求时,将抛出OOM.
运行时常量池是方法区的一部分.String类的intern()方法联系到常量池的使用.
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OOM.
直接内存并不是虚拟机运行时数据区的一部分,
在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数直接分配堆外内存,然后通过存储在java堆里面的DirectByteBuffer对象,作为这块内存的引用进行操作.这样能在一些场景中显著提高性能,因为避免了在java堆和Native堆中来回复制数据.
在配置虚拟机参数时,若忽略掉直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OOM.
java创建对象通常(例外:复制,反序列化)仅仅是一个new关键字而已,而虚拟机中对象(不包括数组和Class对象等)的创建过程:
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头,实例数据和对齐填充.
对象头部分包含两类信息,
第一部分是用于存储对象自身的运行时数据,如哈希吗,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
第二部分是类型指针(确定该对象是那个类的实例),此外,如果对象是一个java数组,那在对象头中还必须有一块用于记录数组长度的数据.
实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在之类中定义的字段都必须记录起来.
对齐填充,仅仅起到占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说任何对象的大小都必须是8的整数倍.对象头已经被设置为8字节的倍数.
我们的java程序会通过栈上的reference数据来操作堆上的具体对象.reference类型只是一个指向对象的引用,引用主要通过句柄和直接指针的方式进行访问,
这两种对象访问方式各有优势,使用句柄访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时,只会改变句柄中的实例数据指针,而reference本身不需要被修改.
使用直接指针来访问的最大好处就是速度快,它节省了一次指针定位的时间开销(因为对象访问在java中非常频繁).
除了程序计数器,虚拟机的其他几个运行时区域都有可能发生OOM
不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清楚这些对象,创建对象所需的内存触及最大堆的容量限制后就会产生内存溢出异常.
出现OOM后可通过内存堆转储快照进行分析,分清楚到底是出现了内存泄漏还是内存溢出
若是内存泄漏,在使用Eclipse时可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径,与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以准确定位到这些对象创建的位置,进而找出产生内存泄露的代码的具体位置.
若不是内存泄漏,那就看jvm堆参数是否还有向上调整的空间.再从代码上检查是否存在某些对象生命周期过长,持有状态时间过长,存储结构设计不合理等情况,尽量减少程序运行期的内存消耗.
无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配时,HotSpot虚拟机抛出的都是StackOverflowError
String:intern()是一个本地方法,如果字符串常量池中已经包含一个等于此String对象的字符串,则返回这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此对象的引用.
在JDK6之前的虚拟机,常量池被分配在永久代中,从JDK7起,常量池被分配在了java堆中
方法区的主要职责是用于存放类型的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等.若要对方法区内存做溢出测试,就可以在运行时产生大量的类去填满方法区,直到溢出,具体可借助CGLib直接操作字节码运行时生成大量的类,当前的很多主流框架,如Spring,Hibernate对类进行增强时,都会使用到CGLib这类字节码技术.
由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用是NIO),那就可以考虑重点检查一下直接内存方面的原因了.