如上图所示,Java 虚拟机启动时会将程序分为以下几个内存区域
寄存器 / 程序计数器
程序计数器是一块较小的内存空间,他的作用可以看做是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 《深入理解Java虚拟机》
特点
- 计数器为线程私有
- 唯一一个没有
OutOfMemoryError
的内存区域 - 随着线程的启动而产生,线程结束则销毁。
这是一段java代码反编译的结果,圈起来的就是计数器存放的内容,运行时只会改变值,所以不会因为程序的运行而改变内存区大小,所以不会溢出。
每个线程创建时都会创建一个属于当前线程的私有计数器,他用来记录每一行代码对应的偏移地址,说白了就是让程序记住自己执行到哪儿了。
因为 Java 是可以多线程的,所以线程恢复时程序计数器的存在尤为重要。在第一段话的引用文字中可以体现。
举个例子:
首先从理论上来讲
- CPU 在某一时间内只处理一个线程。因为线程切换速度很快感觉不出来,所以才称之为多线程。
- 线程是 CPU 最小的调度单元
Thread-1 和 Thread-2 两个线程都在执行 test() 方法。
Thread-1 执行 test() 执行到一半时,Thread-2 也处理到了这一行,并且优先级更高,所以这时 CPU 应让出来给Thread-2执行,Thread-1线程挂起。
CPU 是不会记录你执行到哪一步的,当Thread-1被唤醒时,如果没有计数器,它就不知道当前执行到了哪一步。
Java Stack(栈)
概念
栈是一种只能在一端进行插入和删除操作的线性表。
- 栈的操作端被称之为栈顶,另一端被称为栈底
- 栈的插入操作称为进栈、压栈、push
- 栈的删除操作称为出栈、弹栈、pop
是用来描述 Java 方法执行的内存模型。栈也是线程私有的,和线程的生命周期一样,每创建一个线程,JVM 就会为该线程创建对应的 Java 栈。
每个方法执行时会创建一个栈帧(Stack Frame)
主要存储这些信息
- 局部变量
- 操作数栈
- 动态链接
- 返回值
每个方法从调用到执行完成的过程 对应着 一个栈帧在虚拟机中入栈到出栈的过程。局部变量包括了编译器可知的各种基本数据类型、对象引用、返回地址类型。其中64位长度的long和double会占用2个局部变量空间,其他只占用1个。局部变量表所占用的内存在编译器就完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量完全是确定的,在方法运行期间不会改变局部变量表的大小。
当前活动栈A调用了另外一个方法的指令时,随之会去在栈顶创建一个栈帧B,去执行栈顶的指令,执行完成后会出栈,栈顶就由B变成了A。
由于Java栈是与线程对应起来的,Java栈数据不是线程共有的,所以不需要关心其数据一致性,也不会存在同步锁的问题。
特点
先进后出:把栈比作杯子,只可以从杯口存取,所以最先放进去的东西最后才可以取出来。
存取快:但它分配的内存是在编辑期间就固定好的,不可以动态变化的,这也就是为什么快。
存放什么
- 基本类型变量字面值
- 对象的引用 (只是引用地址,不是对象本身)
栈溢出
public class Test{
public void a(){
a();
}
}
像这段代码,当a方法被调用时,他就会不停地递归调用自己,每次调用都会有一个入栈操作。这个杯子一直被塞进去东西,当杯子装满了就会溢出。所以当达到栈限定的最大深度时,就会出现栈溢出(java.lang.StackOverflowError)。
可以通过-Xss参数来指定虚拟机的内存大小,这直接影响了栈的最大允许深度。
堆栈轨迹
java有两种方法可以获取到 StackTrace
Thread.currentThread().getStackTrace();
new Throwable().getStackTrace();
打印出来就能看到当前栈的情况。
Java Heap(堆)
存放什么
- 对象
- 数组(数组也是对象)
特点
- 存取速度慢
- 可共享
- 动态分配内存
概念
堆是 Java 虚拟机内存中占用最大的一块区域。它是被所有线程所共享的。生命周期伴随着虚拟机创建和销毁。
Java 堆的一个主要的用途就是用来存放 Java 对象实例
,这一点Java虚拟机规范中描述是:所有的对象实例以及数组都要在堆上分配。
Java 堆是垃圾管理的主要区域,因此也称之为 GC 堆。
Java 虚拟机主要采用分代回收算法。所以 Java 堆可以细分为
- 老年代(Old) 占2 / 3
- 新生代(Young) 占1 / 3
- Eden (8/10)
- From (1/10)
- To (1/10)
标记清除算法
标记清除算法是最基础的回收算法,分为标记和清除两部分。
首先标记出所有需要回收的对象,标记过后统一回收所有被标记的对象。
该算法效率比较低。
复制算法
复制算法是针对新生代的回收策略,比标记清除算法效率高。
复制算法将堆中可用的新生代内存按容量划分为两块大小相等的内存区,每次只使用其中一个区域,当其中一块内存要进行垃圾回收时,会把此区域还存活的对象复制到另外一块区域上,再把此内存区一次性清理。
每次清理存活下来的对象,都会给当前对象的“年龄”+1,当达到 JVM 默认设定的值 15 时(可由JVM MaxTenuringThreshold参数设定),会存入老年代。
现在主流的虚拟机,包括HotSpot都是采用这种回收策略进行回收。
Method Area(方法区)
存放什么
- 常量池
- 类方法信息
- 类名称、修饰符
特点
- 可共享
- 不易被回收
- 方法区超出所允许大小时会抛内存溢出
一点问题
在Java7之前,方法区位于永久代,和堆相互隔离,static变量从永久代移入堆中。
Java8,取消永久代,方法存放于元空间,仍与堆隔离,但与堆共享物理内存,理论上可以认为在堆中。
元空间和永久代
两者最大区别在于,元空间不在虚拟机中,而是使用本地内存,
为什么要把永久代变为元空间
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。
Constant Pool(常量池)
存放什么
- 常量值
- 字符串
- 方法名常量
概念
常量池是方法区中的一个数据结构,在编译时就被确定,并保存在.class文件中。
一般分为:字面值和应用值
字面值
就是字符串、final常量、方法名。
应用值
类、接口的全限定名,字段名、和修饰符、方法名和修饰符。
参考
https://blog.csdn.net/leaf_0303/article/details/78953669
https://www.cnblogs.com/lewis0077/p/5143268.html
https://www.cnblogs.com/alsf/p/9017447.html