Java虚拟机管理的内存分为五大区域,程序计数器、虚拟机栈、本地方法栈、堆以及方法区。程序计数器、虚拟机栈和本地方法栈都是线程私有的,即每个线程都有自己的程序计数器、虚拟机栈和本地方法栈;堆和方法区是线程共享的,即所有线程共享堆和方法区。
一、程序计数器
程序计数器是一块很小的内存空间,它是线程私有的,可以看作是当前线程正在执行的字节码的行号指示器。
对于一个处理器(如果是多核CPU那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有很多指令,为了线程切换可以恢复到正确执行位置,每个线程都需有一个独立的程序计数器,来记录当前线程指令执行的行号。不同线程之间的程序计数器互不影响,独立存储。
如果当前线程正在执行的是
Java方法:
程序计数器记录的就是当前线程正在执行的字节码指令的地址。
本地方法(C或C++的方法):
程序计数器值为undefined。
程序计数器有两个作用:
1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如顺序执行、选择、循环、异常处理。
2.在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
程序计数器的生命周期是随着线程的创建而创建,随着线程的结束和销毁。
二、虚拟机栈(Java栈) — 栈
同程序计数器一样也为线程私有,生命周期也与之相同。
就是我们平时说的栈,栈描述的是Java方法执行的内存模型(栈空间是为java方法的运行提供的空间,换言之就是java方法是在栈中运行的)。每个方法被执行的时候都会创建一个栈帧(每个java方法在栈空间中都对应一个栈帧,即一个栈帧就是一个方法的运行空间),用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程【栈是先进后出后进先出】。
平时说的栈一般主要指局部变量表部分。局部变量表是一片连续的内存空间,用来存放方法参数以及方法内定义的局部变量,存放着编译期间已知的各种基本数据类型、对象引用类型(reference类型)和returnAddress类型
基本数据类型:
byte、short、int、long、char、float、double、boolean。
对象引用类型:(简单理解就是内存地址)
它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其它与此对象相关的位置。
returnAdderss类型:
指向了一条字节码指令的地址。
局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈帧中需要分配多大的局部变量表空间是完全确定的,在方法运行期间不会改变局部变量表大小。
相对于类属性变量(非局部变量)的准备阶段和初始化阶段来说(有默认值),局部变量没有准备阶段(没有默认值),必须显示初始化。
如果是非静态方法,则在index[0]位置上存储的是方法所属对象的实例引用,随后存储的是方法的参数和局部变量(静态方法的栈帧的局部变量表只存放方法的参数和局部变量,非静态方法[对象方法],怎么确定它属于哪个对象,就在非静态方法的栈帧的局部变量表的index[0]位置存储的是这个非静态方法所属的对象的地址,然后再存放这个方法的参数和局部变量)。
三、本地方法栈
同样也为线程私有,生命周期是随着线程的创建而创建,随着线程的结束和销毁。
本地方法栈与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,而底层可能调用的是C或者C++。
四、堆
被所有线程共享,在虚拟机启动时创建,用来存放对象实例,几乎所有的对象实例都在堆中分配内存(堆是为对象开辟内存空间的)。对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是垃圾回收器管理的主要区域,堆中的垃圾由垃圾回收器自动回收,因此很多时候也被称为"GC堆"。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为新生代和老年代;新生代又有Eden空间、From Survivor空间、To Survivor空间三部分。堆的内存空间既可以固定大小,也可运行时动态调整,通过-Xms参数设定初始值-Xmx参数设定最大值,例如-Xms256M、-Xmx1024M,但当堆已满且内存已无法再扩展时就抛出OutOfMemoryError异常。
五、方法区
Java虚拟机规范中定义方法区是堆的一个逻辑部分,但是别名Non-Heap(非堆),以与Java堆区分。
方法区中存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区是堆的一个逻辑部分,因为和堆一样都是线程共享的,整个虚拟机中只有一个方法区。
方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区又称为永久代;也正是因为方法区中的信息一般需要长期存在,所以Java虚拟机规范对方法区的要求比较宽松,可以不实现垃圾收集,即使内存回收,主要目标也是对常量池的回收和对类的卸载。
和堆一样,内存允许固定大小也允许可扩展大小,当方法区内存空间无法满足内存分配需求时,也将抛出OutOfMemoryError异常。
运行时常量池是方法区的一部分,方法区中存放的数据有被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码,其中常量就存储在运行时常量池中。.java文件被编译之后生成的.class文件中除了包含类的版本、字段、方法、接口等信息外,还有一项就是常量池,常量池中存放编译时期产生的各种字面量和符号引用,.class文件中的常量池中的所有的内容在类被加载后就存放在方法区的运行时常量池中(一个类中的常量在编译时是先存在于这个类的.class中的常量池的,然后在类被加载时就转存到方法区中的运行时常量池了)。运行时常量池相对于class文件常量池的另外一个特性是具备动态性,java语言并不要求常量一定只有编译才产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中(.class的常量池不具备动态性,即只能存放类被编译产生的常量;方法区的运行时常量池具备动态性,不但能够存放加载的类的常量,也能存放在运行时产生的常量)。
总结:我们需要掌握的内存区域
栈
1)是为java方法提供运行空间的
2)方法一旦被调用就会在栈中创建对应的栈帧,而方法的整个执行过程就是方法对应的栈帧从入栈到出栈的过程。换言之,就是方法被调用进栈(压栈 入栈),方法执行结束出栈(弹栈)。
3)栈是先进后出后进先出(先被调用的方法最后结束,后被调用的方法最先结束)
4)栈中的变量都是属于方法的,所以都是局部变量,且局部变量必须初始化值。
5)栈生命周期与其所属线程的生命周期一致,可以认为栈具有自动销毁机制。
堆
1)是为实体对象来开辟空间的,换言之就是实体对象的空间都在堆中开辟。凡是被new出来的都是对象。
2)堆中的变量是对象变量,因为是属于对象的,且是随着对象的创建而产生随着对象的销毁而销毁。
3)堆中的变量(对象变量)都有默认值:
整数:0 浮点型:0.0 布尔型:false char型:’ ’ 对象:null
4)堆没有自动销毁机制,它里面的垃圾由垃圾回收器负责收集(通过收集算法判断哪个对象属于垃圾然后再清理)
5)堆中的空间都有一个16进制的首地址作为地址进行区分。
方法区
方法区中存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
常量是存放在方法区中的运行时常量池中的。