1.介绍下Java的内存区域(运行时数据区)
线程私有的:程序计数器;虚拟机栈;本地方法栈。
线程共享的:堆;方法区;直接内存(非运行数据区的一部分)
1.1 程序计数器PC寄存器用来存储指向下一条指令的地址,也即是刚要执行的指令代码。由执行引擎读取下一条指令。分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
为了切换后线程能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有的内存”。
程序计数器的两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿。
PS:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
1.2 Java虚拟机栈Java虚拟机是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
局部变量表主要存放了编辑期可知的各种数据类型、对象引用
Java虚拟机栈会出现两种错误:StackOverFlowError和OutOfMemoryError
- StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当前线程请求栈的深度超过当前Java虚拟机栈的最大什深度的时候,就抛出StackOverFlowError异常
- OutOfMemoryError:若虚拟机栈中没有空闲内存,并且垃圾回收器也无法提供更多的内存的话,就会抛出OutOfMemoryError异常
1.3 本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
当某个线程调用本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界。
本地方法可以直接通过本地方法接口来访问虚拟机内部的运行时数据区。甚至可以直接使用本地处理器中的寄存器。直接从本地内存的堆中分配任意数量的内存。
1.4 堆Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
1.4.1 堆内存细分
Java7及之前堆内存逻辑上分为三个部分:新生区+养老区+永久区
- Young Generation Space新生区 Young/New又被分为Eden区和Survivor区
- Tenure generation space 养老区 Old/Tenure
- Permanent Space 永久区Perm
Java8及之后堆内存逻辑上分为三个部分:新生区+养老区+元空间
- Young Generation Space新生区 Young/New又被分为Eden区和Survivor区
- Tenure generation space 养老区 Old/Tenure
- meta Space 元空间meta
1.4.2堆空间大小的设置
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过“-Xmx”和“-Xms”来进行设置
- “-Xms”用于表示堆区的起始内存
- “-Xmx”用于表示堆区的最大内存
通常会将这两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
1.4.3 图解对象分配过程,为新对象分配内存
1.4.4 Minor GC, Major GC、Full GC
部分收集:不是完整收集整个Java堆的垃圾收集。其中分为
- 新生代收集(Minor GC/ Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/ Old GC):只是老年代的垃圾收集
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前只有G1 GC会有这种行为。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
1.4.5 堆内存是分配对象的唯一选择吗?
在Java虚拟机中,对象是在Java堆中分配内存的,但是有一种特殊情况,那就是经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收,这也是最常见的堆外存储技术。
使用逃逸分析,编译器可以对代码做如下优化:
- 栈上分配:将堆分配转化为栈分配。
- 同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 分离对象或标量替换



