JVM内存管理
JVM所管理的内存包括以下几个运行时数据区域:
图片来源:http://ju.outofmemory.cn/entry/371070
PC Register: Program Counter Register
程序计数器:它是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成,
为了切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,它为线程私有的内存。
它是JVM规范中唯一一个没有规定会导致OutOfMemory(内存泄露,下文简称OOM)的区域。
JVM Stack:
虚拟机栈也就是我们常说的栈,和程序计数器一样,它也是线程私有的内存。或者说是虚拟机中的局部变量表部分。
局部变量表存储了编译期间可知的各种基本数据类型:boolean, byte, char, short,int,float,long,double,对象引用。
Native Method Stack:
本地方法栈:与栈所发挥的作用相似,区别是虚拟机栈为虚拟机执行java方法的服务,而本地方法栈则为虚拟机使用到的Native方法服务。(Native方法:一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C)。部分虚拟机直接就把虚拟机把虚拟机栈和本地方法栈合二为一。
Heap:
堆:JVM管理内存中最大的一块,也是所有线程共享的内存区域,此内存的唯一目的就是存放对象实例,所有对象的实例以及数组都要在堆上分配。
java堆是垃圾收集器管理的主要区域,因此也被称做:GC堆。
从内存回收的角度看,现在的收集器基本都采用分代收集算法,所以java堆还可以细分为:新生代和老年代
再细致一点的有:eden空间,From survivor空间,to survivor空间,
注:Java 8 之后,没有了永久代(Permanent Generation),取而代之的是元空间(Metaspace)。元空间不在 Java 堆上,而在本地内存上。
图片来源:http://ju.outofmemory.cn/entry/371070
从内存分配的角度看,线程共享的堆可能划分为多个纯种私有的分配缓冲区(Thread local allocation buffer, TLAB)
Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。
Method Area:
方法区:与堆一样,是线程共享的内存区域,用于存储已被JVM加载的类信息,常量,静态变量,即时编译后的代码等数据,
HotSpot虚拟机使用永久代来收集方法区,但方法区不是永久代,垃圾收集在这个区域很少出现,收集的效果不太理想,主要是对常量池的回收和类型的卸载(条件相当苛刻)
Runtime Constant Pool:
运行时常量池:是方法区的一部分,class文件中除了有类的版本,字段,方法,接口的描述信息外,还有一项就是常量池,用于存放编译期生成的各种字面量和符号的引用。
运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,java并不要求常量一定只有编译才能产生,运行期也可能将常量放在池中。
Direct Memory:
直接内存:并不是JVM运行时的数据区。JDK1.4引用的NIO,引入了一种基于通道与缓冲区的IO方式,它可以使用Natvie函数库直接分配堆外内存,然后通过一个存储在java堆中的对象作为这块内存的引用进行操作,这样在一些场景中显著提高性能,因为避免了在堆和native堆中来回复制数据。本机直接内存的分配不会受到jva堆大小的限制,所以要注意它的配置。
HotSot虚拟机:JDK采用的虚拟机:
对象的创建:虚拟机遇到一个new指令时,首先去检查这个指令的参数,是否能在常量池中定位到一个符号的引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过。如没有,那必须先执行相应的类加载过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需内存大小在类加载完成后便可完全确定。
根据堆是地规整,内存分配方式有
1.指针碰撞:规整时,所有用过的内存放在一边,空间的内存放在另一边,中间放着一个指针作分界点的指示器。
2.空间列表:不规整时,虚拟机要维护一个列表,记录哪些空间内存,分配时从列表中找到空给对象的实例。
堆是否规整,由所采用的垃圾收集器是否带有压缩整理功能决定:
使用serial,parnew等带有compact过程的收集器,采用指针碰撞的分配方式
使用CMS这种基于Mark-sweep(标记-整理)算法的通常采用空间列表。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,如果使用TLAB,可由它完成,这一步操作保证了对象的实例字段在java代码中不赋初始值就可直接使用,
对象的内存布局:
在hotspot中对象在内存的存储的布局分为3个区域:对象头,实例数据,对齐填充。
对象头包括:
一部分:存储自身的运行时数据:如hashcode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳。
另一部分:类型指针,即对象指向它的类无数据的指针,虚拟机通过这个指针,来确定这个对象是哪个类的实例。
hotspot默认的分配策略为:long/double,ints shorts/chars,bytes/boolean, oops, 从分配策略可以看出,相同字段总是被分配到一起,(父类定义的变量会出现在子类之前)
对象的访问定位:
主流的访问方式有使用句柄和直接指针:
如果使用句柄,那堆中会划分出一赽,内存来作为句柄,引用中存储的就是对象的句柄地址,句柄中饮食了对象实例数据和类型数据各自的地址信息:
优点:对象移动(很频繁)后,只需要修改句柄中的数据而reference不用修改。
使用直接内存:那么堆对象的布局中就必须考虑如何放置访问类型数据的信息,而reference中存放的直接就是对象地址:
优点:速度更快,节省了一次指针定位的时间开销。hotspot采用这种方式 。
图片来源:https://blog.csdn.net/clover_lily/article/details/80095580