JDK1.7 & JDK1.8的不同
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
JDK1.7
JDK1.8
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 堆外内存(永久代或元空间、代码缓存)
下面我们按顺序梳理下这些都是作咩的
1 程序计数器
程序计数器是一块较小的内存空间,可以看做是当前线程执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选去下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
为了线程切换后能够恢复到正确的执行位置,每条线程都要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存为”线程私有“的内存。
总结一下,程序计数器的作用
- 字节码解释器通过改变程序计数器来一次读取指令,实现代码的流程控制:如顺序执行、选择、循环、异常处理
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪了
为何程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域?
因为它仅仅只是一个运行指示器,它需要存储的内容是下一个需要待执行的命令的地址。
无论代码有多少,最坏情况下死循环也不会让这块内存区域超限,因为程序计数器所维护的就是下一条待执行的命令的地址,所以不存在OutOfMemoryError。
2 虚拟机栈
每个线程在创建的时候都会创建一个Java虚拟机栈,其内部保存一个个的栈帧,对应着一次次Java方法调用,是线程私有的,生命周期和线程保持一致。
方法调用的数据需要通过Java虚拟机栈进行传递,每一次方法调用都会有一个对应的栈帧压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
栈由一个个栈帧组成,每个栈帧中都拥有局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。
2.1 局部变量表
局部变量表主要存放了编译期可见的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
2.2 操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
2.3 动态链接
主要服务于一个方法需要调用其他方法的场景。Class文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向其他方法的符号转化为其在内存地址中的直接饮用。也叫动态连接。
动态链接的作用:将符号引用转化为调用方法的直接饮用。
栈空间正常调用情况下不会出现问题,如果多层递归出不来的话,就会导致栈中压入太多栈帧占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就会抛出StackOverflow错误。
Java方法有两种返回方式,一种是return语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说,栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算方法结束。
除了StackOverflow错误之外,栈还可能会出现OutOfMemoryError
错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError错误。
3 本地方法栈
和虚拟机栈发挥作用类似,惟一的区别是:虚拟机栈为虚拟机执行Java方法服务,本地方法栈则为虚拟机使用到的Naive方法服务。
在HotSpot虚拟机中和Java虚拟机栈合二为一。
也会出现 StackOverFlowError
和 OutOfMemoryError
两种错误。
4 堆
Java虚拟机所管理的内存中最大的一块,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
什么对象不在堆上分配内存呢?
在Java中,典型的对象不在堆上分配的情况有两种:TLAB(Thread Local Allocation Buffer)和栈上分配(严格来说TLAB也是属于堆,只是在TLAB比较特殊)。JVM在Server模式下的逃逸分析可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配。由于该对象一定是局部的,所以栈上分配不会有问题。
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(==分代的唯一理由就是优化 GC 性能==):
- 新生带(年轻代):新对象和没达到一定年龄的对象都在新生代
- 老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大
- 元空间(JDK1.8 之前叫永久代):像一些方法中的操作临时对象等,JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。
下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间区域属于老年代,右边的区域属于永久代。
4.1 年轻代 (Young Generation)
年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为 Minor GC。年轻一代被分为三个部分——伊甸园(Eden Memory)和两个幸存区(Survivor Memory,被称为from/to或s0/s1),默认比例是8:1:1
- 大多数新创建的对象都位于 Eden 内存空间中
- 当 Eden 空间被对象填充时,执行Minor GC,并将所有幸存者对象移动到一个幸存者空间中
- Minor GC 检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的
- 经过多次 GC 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代
4.2 老年代(Old Generation)
旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为 主GC(Major GC),通常需要更长的时间。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝
4.3 方法区
元空间放在后边的方法区再说~
4.0 字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp
,StringTable
可以简单理解为一个固定大小的HashTable
,容量为 StringTableSize
(可以通过 -XX:StringTableSize
参数来设置),保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
5 方法区
不管是 JDK8 之前的永久代,还是 JDK8 及以后的元空间,都可以看作是 Java 虚拟机规范中方法区的实现。
虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开。
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
5.0 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。
Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table) 。
-
字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。
-
常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。
常量池表会在类加载后存放到方法区的运行时常量池中。
运行时常量池的功能类似于传统编程语言的符号表,尽管它包含了比典型符号表更广泛的数据。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError
错误。
5.1 类型信息
对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation),JVM 必须在方法区中存储以下类型信息
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于 interface或是 java.lang.Object,都没有父类)
- 这个类型的修饰符(public,abstract,final 的某个子集)
- 这个类型直接接口的一个有序列表
5.2 域(Field)信息
- JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
- 域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient 的某个子集)
5.3 方法(Method)信息
JVM 必须保存所有方法的
-
方法名称
-
方法的返回类型
-
方法参数的数量和类型
-
方法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)
-
方法的字符码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 方法除外)
-
异常表(abstract 和 native 方法除外)
-
- 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引