JVM 运行时数据区域
Java运行时数据区域包括5部分,分别为方法区,堆,虚拟机栈,本地方法栈,程序计数器,如下图
1.,7以下 版本
1.8 版本
1.8同1.7比,最大的差别就是:元数据区取代了方法区(永久代)。元空间的本质和方法区(永久代)类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
1.程序计数器
简单来说 ,是用于存放指令的地址 ,线程私有的。PC的初值为程序第一条指令的地址,在顺序执行程序时,控制器首先按程序计数器所指出的指令地址从内存中取出一条指令,然后分析和执行该 指令,同时将PC的值加1指向下一条要执行的指令。
2. 虚拟机栈
- 线程私有的,生命周期和线程相同
- 每个方法执行的同时会创建一个帧栈,用于保存局部变量表(包括 基本数据类型和对象引用类型),操作数栈,方法出口,动态链接等信息
- 局部变量表所需的内存空间是在编译期间完成分配,在运行期间不会改变局部变量表的大小
- 这个区域有两种异常状况:
第一种,StackOverFlowError ,如果线程请求的栈深度大于虚拟机所允许的深度就会出现异常;
第二种,OutOfMemoryError , 虚拟机无法申请到足够的内存时,导致内存溢出
3. 本地方法栈
类似虚拟机栈,区别就是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法(类似C,C++)服务
4. 方法区(永久代)
线程共有的,各个线程共享内存区域。用来存放虚拟机加载的类,常量,静态变量,1.7版本很多人把方法区称为永久代,本质上两者不等价。仅仅是因为HotSpot虚拟机的设计团队选择
把GC分代收集扩展至方法区,或者说用永久代来实现方法区而已,这样HotSpot 的垃圾收集器可以像管理Java堆一样管理这部分内存,省去了专门为方法区编写管理内存的工作。对于其
他虚拟机来说是不存在永久代这种说法。
使用永久代来实现方法区,现在看来并不是个好办法,因为这样更容易导致内存溢出(永久代有 -XX:MaxPernSize 的上限),所以现在的版本放弃永久代(方法区),并逐步改为本地内存(Native Memory)来实现方法区,并将方法区的常量池放在堆中
5. 元数据区
元数据区的本质和方法区(永久代)类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
6. 直接内存
直接内存不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用 进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
7. 堆
其唯一的用途就是存放对象实例:几乎所有的对象实例及数组都在对上进行分配。1.7后,字符串常量池从永久代中剥离出来,存放在堆中。堆有自己进一步的内存分块划分,按照GC分代收集角度的划分请参见下图。
- 老年代 : 三分之二的堆空间,主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用标记—清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
- 年轻代 : 三分之一的堆空间,又分为 Eden区、ServivorFrom、ServivorTo三个区
- eden区: 8/10 的年轻代空间,Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对年轻代区进行一 次垃圾回收。
- survivorTo: 1/10 的年轻代空间,保留了一次MinorGC过程中的幸存者
- survivorFrom : 1/10 的年轻代空间,上一次GC的幸存者,作为这一次GC的被扫描者。
MinorGC的过程:MinorGC采用复制算法。在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
- 永久代
指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域. 它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制.
采用元空间而不用永久代的几点原因:
1、为了解决永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出(因为堆空间有限,此消彼长)。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。
简单说,我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。