Java虚拟机在执行java程序的过程中,会把它的内存划分为若干个不同的运行时数据区域,如图所示:
程序计数器是一块较小的内存空间,字节码解释器工作时,就是通过改变这个计数器的值来选取下一条要执行的字节码指令。
Java虚拟机栈也是线程私有的。它的生命周期与线程相同。
虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时,会创建一个栈帧,该栈帧用于存储局部变量表、操作数栈、动态链接方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧入栈到出栈的过程。
经常有人说“栈内存”,就是指虚拟机栈。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlow异常。
如果虚拟机栈的大小可以动态扩展,但是虚拟机无法申请到足够的内存,就会抛出OutOfMemory异常。
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
存放编译器可知的基本数据类型、对象引用、返回地址类型。
其中,64位长度的long和double会占用2个局部变量空间(slot),其余的数据类型只占用1个。
局部变量表所占用的内存空间在编译器完成分配。当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的(因为过了编译期。。),在方法运行期间不会改变局部变量表的大小。
如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向 方法调用指令 后面的一条指令地址。
概念:与Java堆一样,各个线程共享的区域。用于存储Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。有一个别名“Non-Heap” (非堆)
在JDK1.6及之前,运行时常量池是方法区的一个部分,同时方法区里面存储了类的元数据信息、静态变量、即时编译器编译后的代码(比如spring 使用IOC或者AOP创建bean时,或者使用cglib,反射的形式动态生成class信息)等。
在JDK1.7及以后,JVM已经将运行时常量池从方法区中移了出来,在JVM堆开辟了一块区域存放常量池。
在HotSpot中,设计者将方法区纳入GC分代收集,像对待堆一样来管理这部分内存,能够省去编写管理这块内存的工作,所以HotSpot虚拟机使用者更愿意将方法区称为老年代。
但是把方法区纳入永久代,更容易造成永久代的内存溢出。
方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。
直接内存(堆外内存)并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。
在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制,JVM中有参数可以限制直接内存的大小(-XX MaxDirectMemorySize)。
配置虚拟机参数时,不要忽略直接内存,防止出现OutOfMemoryError异常。
Ecache就是使用堆外内存实现的。
JDK中使用DirectByteBuffer
对象来表示堆外内存,每个DirectByteBuffer
对象在初始化时,都会创建一个对用的Cleaner
对象,这个Cleaner
对象会在合适的时候执行unsafe.freeMemory(address)
,从而回收这块堆外内存。
当初始化一块堆外内存时,对象的引用关系如下:
ReferenceQueue
是用来保存需要回收的Cleaner
对象。
如果该DirectByteBuffer
对象在一次GC中被回收了
在GC时,把该Cleaner
对象放入到ReferenceQueue
中,并触发clean
方法。
Cleaner
对象的clean
方法主要有两个作用:
1、把自身从Clener
链表删除,从而在下次GC时能够被回收
2、释放堆外内存
如果JVM一直没有执行FGC的话,无效的Cleaner
对象就无法放入到ReferenceQueue中,从而堆外内存也一直得不到释放,内存岂不是会爆?
其实在初始化DirectByteBuffer
对象时,如果当前堆外内存的条件很苛刻时,会主动调用System.gc()
强制执行Full GC。
直接内存满了之后,不会主动通知垃圾收集器进行回收,而是等到老年代满了之后,触发Full GC,然后顺便回收直接内存中的废弃对象。
###元空间
HotSpot虚拟机在1.8之后已经取消了永久代,改为元空间,类的元信息被存储在元空间中。元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。
这项改造也是有必要的:
JVM的启动流程大致分为几个步骤:
Java代码执行时,需要一个JVM环境,JVM环境的创建包括两部分,JVM.dll文件的查找和装载。
装载完JVM以后,需要对启动参数进行解析,其实在装载JVM环境的过程中,已经解析了部分参数。
Java Main函数的执行流程大致如下:
JAVA OOM(Out Of Memory Error,内存溢出):
1、概念:堆内存没有足够空间分配给对象,并且垃圾收集器也没有空间回收时,就会抛出这个错误。
2、造成OOM的原因:
(1)在初始化JVM的阶段,设置给JVM可用的内存太少了
(2)用完的对象没有释放,导致内存泄漏。
3、OOM有哪几种类型:
(1)堆空间不够大造成溢出
(2)虚拟机栈的深度不能够扩展
(3)方法区溢出
九、JVM退出的几种情况:
1、执行了System.exit(int status)方法
2、程序执行结束
3、程序在执行过程中遇到了异常或者错误而终止运行(main方法里面throws抛出的异常,将会被JVM捕获,然后JVM就会异常退出了)
4、操作系统出现错误导致Java 虚拟机进程终止
十、直接内存:
1、直接内存不是Java虚拟机运行时数据区的一部分,而是计算机本身的内存。利用NIO可以使用Native函数库直接分配堆外内存,然后通Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能提升性能,因为避免了在Java堆和Native堆中来回复制数据的开销。
2、本机直接内存的分配不会受到Java堆大小的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现Out of Memory异常。
##HotSpot虚拟机
在HotSop虚拟机中,对象在内存中存储的布局可以分为:对象头、实例数据、对其填充。
对象头包括两部分信息:
实例数据部分 是对象真正存储的有效信息,也是程序代码中定义的各类型的字段内容。无论是父类继承下来的,还是子类中定义的,都需要记录起来。这部分的存储顺序会收到虚拟机分配策略参数和 字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略是longs/doubles、ints,shorts/chars,bytes/booleans,oops(对象指针)。从分配策略可以看出,相同字宽的可以放在一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
对齐填充并不是必然存在的,它仅仅起着占位符的作用。由于HotSpot要求对象起始地址必须是8字节的整数倍,换句话说,对象的大小必须是8字节的整数倍 。而对象头正好是8的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要对齐填充来补全。
Java程序需要通过栈上的引用来操作堆上的具体对象。目前主要的访问方式有句柄、直接指针。
使用句柄的方式,Java堆中将会划分出一块内存作为作为句柄池,引用中存储的就是对象的句柄的地址。而句柄中包含了对象实例数据和对象类型数据的地址。
使用直接指针的方式,引用中存储的就是对象的地址。对象头中存储了类型数据的指针,可以用来访问对象类型数据。
什么情况下使用堆外内存,需要注意什么?
当需要使用大块内存空间作为缓存的时间,如果使用堆内存,会给GC带来压力。这时候就可以使用堆外内存(直接内存)。
使用堆外内存的时候,一定要配置虚拟机参数来限制堆外内存的大小,避免内存溢出。