1. JVM入门概念
概念: JVM是java虚拟出来的一块内存(运行java的场所),但它不是具体的产品,它只是一套规范标准,而 HotSpot 才是JDK1.8中,Oracle公司生产的一个JVM的具体实现:
- JVM采用了那种实现,可以通过 java -version 命令查看。
- 规范和实现的关系,可以对比理解成接口和实现类的关系。
- 运行时数据区:程序运行时,ClassLoader会将class文件加载到JVM中的运行时数据区(Runtime Data Area)中,该区域通常被分为五部分,如果将内存比作一个"小区",那么:
- Java栈
Stack:相当于小区的物业室,空间小,功能少,但访问方便。 - Java堆
Heap:相当于小区的住宅区,空间大,功能多,但访问麻烦。 - 方法区
Method Area:相当于小区的公告板,谁都可以用,且只存在一个。 - PC寄存器
Program Counter Register:相当于小区的保安,监视并登记每个访问者(线程)的位置。 - 本地方法栈
Native Method Stack:相当于小区的开发商,主要负责和操作系统打交道,因为使用的不是Java语言的原因,一般我们不关心,而且在JDK1.8版本中,此区域已经被整合到了Java栈中。
- Java栈
JVM内存分布图
2. PC寄存器
思考一个情景:
你在读一本非常喜欢的书,但是每过一阵子,你会受到你母亲大人的无情打断,去楼下取快递,去扫个地,去擦个玻璃等等等等...
那么每次当你被打断的时候,你最好使用一个书签,记录下你当前读到了书的哪一页,当你回来的时候,根据你的书签,可以继续阅读你的书...
线程也是一样,一个线程在做一项工作的时候,不知道会被打断多少次,因此它也需要一个“书签”,在它每次被打断的时候,能够记录当前程序运行到的“行号”,以便自己被重新唤醒时,能够准确地续接之前的工作,这个“书签”,就是PC寄存器。
概念:
- PC寄存器也叫程序计数器,它占用很小的一块内存空间,它由JVM直接管理,不需要我们管理。
- PC寄存器是线程私有的,即每个线程中都有独立的PC寄存器。
- PC寄存器记录的是字节码指令的行号位置,其所在的线程会按照它的指示进行工作。
- PC寄存器只适用于非本地方法,如果线程执行的是本地方法(Native方法),则PC寄存器始终为空。
- PC寄存器初始值为0,当线程执行任务时,详细的步骤模拟如下:
- 字节码解释器(专门操作字节码的一个家伙)开始工作...
- PC寄存器自增:0 => 1
- 所在线程执行第1行字节码指令...
- 第1行字节码指令执行完毕...
- PC寄存器自增:1 => 2
- 所在线程执行第2行字节码指令...
- 第2行字节码指令执行完毕...
- ...
- 此区域是唯一一个在JVM规范中没有规定出现
OutOfMemoryError(OOM)情况的区域。
3. 栈内存
概念: JVM栈是负责执行java方法的一块内存区域,被每一个线程所私有,随线程的创建而创建,所以也被出称为线程栈。
- 一个线程中的每个方法在执行时都会创建一个对应的栈帧
Stack Frame,一个方法开始被调用到执行完成的过程,就对应着一个栈帧在JVM栈中从入栈到出栈的过程(First-In-Last-Out)。 - 不同线程之间所包含的栈帧是不允许存在相互调用的。
- 在一条活动线程中,只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,被称为当前栈帧。
本地方法栈:负责执行本地方法的一块内存区域,使用的是
native方法,底层并不是java语言,不用我们操心,而且不同的JVM实现对栈区域的实现方式是不同的,比如HotSpot就把本地方法栈和JVM栈合二为一了。
3.1 栈帧
概念: 一个栈帧中主要包含:局部变量表,操作数栈,动态链接和方法出口。
- 局部变量表
LocalVariableTable,也叫本地变量表,是一种线性表的数据结构。- 局部变量表存放的是在编译期就可知的各种基本数据类型和对象引用,一般指的就是方法的参数或者方法内的局部变量。
- 一个方法需要在帧中分配多大的局部变量空间是在编译期就可以确定的。
- 方法在运行期间局部变量表的大小是不会改变的。
- 可以使用
-g:none或-g:vars命令来取消或生成这项信息,如果选择不生成它,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0, arg1这样的占位符。
- 操作数栈:栈帧内所有的计算过程都是在操作数栈中完成的。
- 动态链接:存放的是对象的引用。
- 帧数据区:装着访问常量池的指针和访问异常处理表的指针。
栈帧组成结构图
3.2 栈内存异常
概念:
- 栈内存中的变量在出了作用域后会自动销毁,所以不用担心它的回收问题。
- 栈的大小可以固定也可以动态拓展,或者可以通过 -Xss 参数来设定。
- 栈内存中可能会出现两种内存异常:
- 如果线程请求的栈调用深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
- 如果虚拟机栈在动态扩展时无法申请到足够的内存,抛出OutOfMemoryError异常。
- 栈的深度是由栈的内存空间决定的,请求的栈越深,也即是已使用的栈的空间占用越大,所以上面规定的两种异常是有重叠之处的,一种异常也可能会导致另外一种异常的发生。
源码: /javase-oop/
- src:
c.y.jvm.StackOverflowErrorDemo
/**
* @author yap
*/
public class StackOverflowErrorDemo {
private static int stackFrameCount = 0;
public static void main(String[] args) {
try {
method();
} catch (Throwable e) {
System.out.println("current stack depth:" + stackFrameCount);
e.printStackTrace();
}
}
private static void method() {
stackFrameCount++;
method();
}
}
- src:
c.y.jvm.StackOutOfMemoryDemo
/**
* @author yap
*/
public class StackOutOfMemoryDemo {
public static void main(String[] args) {
while (true) {
new Thread(() -> {
while (true) {
System.out.println("go!");
}
}).start();
}
}
}
在多线程环境中才可以才模拟出OutOfMemoryError异常,特别提醒:此代码运行时会导致系统假死,具有一定的风险性,请在运行前保存好其他文件。
4. 堆内存
概念: Java堆是JVM所管理的内存中最大的一块,所有线程共享,几乎99%对象实例都是在这里分配内存的,因为它也是GC管理的主要区域,所以也被称做GC堆。
- 内存碎片:Java堆是一块不连续的内存空间,且实例所需的内存大小在类加载完成后就可以确定下来,为实例分配内存空间相当于把一块确定大小的内存从Java堆中划分出来,这个划分在内存中是随机的,就可能会导致已经使用过的内存和空闲的内存相互交错,所以这就需要一个列表来维护,来记录哪些内存块是可用的,哪些内存块已经被实例占用了,在这个过程中也很可能会产生一些内存碎片,不过碎片问题就是GC该考虑的问题了。
- 内存溢出:Java堆是如果没有足够的内存空间完成对象实例的分配,并且堆也无法再扩展,将会抛出OutOfMemoryError(OOM)异常。(新实例造不出来)
- 内存泄露:程序在申请内存后,无法释放已申请的内存空间,内存泄露会导致内存资源耗光,通俗的说就是实例占着内存空间无法归还给系统。(旧实例回收不了)
Java堆是用于存储实例的,所以只要不断地创建对象,来把Java堆填满,并且保证垃圾回收机制不能清除这些对象,就可以模拟出Java堆内存的溢出。
源码: /javase-oop/
- src:
c.y.jvm.HeapOutOfMemoryDemo
/**
* 使用 -Xms20M -Xmx20M 限制堆内存大小为20M。
*
* @author yap
*/
public class HeapOutOfMemoryDemo {
/**
* 声明类内部静态类(非private),生命周期和外部类HeapOutOfMemory一样长,
* 使垃圾收集器无法回收这些对象占用的内存空间
*/
static class HeapOutOfMemoryInner {
}
public static void main(String[] args) {
List<HeapOutOfMemoryInner> list = new ArrayList<>();
while (true) {
list.add(new HeapOutOfMemoryInner());
System.out.println(list.size());
}
}
}
5. 方法区
概念: 方法区是线程共享的运行时内存区域,用于存储已经被JVM加载过的类的信息(如类的名称、类的修饰符信息、类的成员信息、常量池等)、常量、静态变量、即时编译器编译后的代码等数据等。
- 方法区和Java堆一样,也不需要连续的内存空间,在JVM的实现中,也是可以选择固定大小或者可扩展,并且还可以选择不实现垃圾回收,因为这个区域用到回收的地方很少,但这个区域同样会出现内存泄漏的问题。
- 方法区和JVM类似,只是一个规范而不是一个实现:
- JDK7之前,Hotspot使用永久代来实现方法区,永久代和堆相互隔离,永久代的大小在启动JVM的时候可以设置一个固定不变的值。
- JDK8取消了永久代的概念,使用元空间来实现方法区,仍然与堆空间不相连。
- 当方法区无法满足内存分配时,抛出OutOfMemoryError(OOM)异常。
5.1 运行时常量池
概念: 运行时常量池是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息之外,还有一个常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类被加载后,进入方法区的运行时常量池中。
- 运行时常量池用于存放编译期的常量信息、方法和属性编译期的符号引用和运行期的直接引用。
- 运行时常量池具有动态性,常量并不一定是在编译期才会被放入该常量池,在运行期间也可能有新的常量进入池中,比如调用String类的
intern(),这个方法的作用就是的作用就是将某个实例从堆内存中移动到常量池中的String池中。
5.2 String池
概念: String池,也叫StringTable,底层的数据结构是HashSet,不允许重复。
- JDK1.7中,String池从永久代移动到了堆中。
- 凡是被双引号引起来的值都会先去String池中查找,如果存在就拿出来直接使用,如果不存在就将它放入到池中,然后再使用。
5.3 代码重用池
概念: 数值在 [-128, 127] 之间,返回指向 IntegerCache.cache 中已经存在的对象的引用,即-128到127的数据,都在代码重用池中存放,在使用的时候,会直接从池中获取。
源码: /javase-oop/
- src:
c.y.jvm.MethodAreaTest.codeReuse()
/**
* @author yap
*/
public class MethodAreaTest {
@Test
public void codeReuse() {
Integer a = 100;
Integer b = 100;
Integer c = new Integer(100);
System.out.println(a == b);
System.out.println(b == c);
}
}
6. 直接内存
概念: 直接内存并不在JVM管理的内存区域内,也不是JVM规范中定义的内存区域,而是直接使用外部主机的物理内存,这在一些场景中(如文件复制)可以提高性能,但是在使用过程中,也要注意主机内存大小的限制(包括物理和系统级的限制)和垃圾回收工作,否则也会抛出OutOfMemoryError异常。