JVM 之内存结构
一、类的加载器和加载过程

- 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的标识
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Enginee(执行引擎)决定
- 加载的类信息存放在方法区(元空间)的内存空间。除了类的信息外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
1 类的加载过程

加载(Loading)
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
链接(Linking)
验证(Verify)
目的在于确保Class文件的字节流中包含信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身的安全。
准备(Prepare)
- 为类变量分配内存并且设置该类变量的默认初始值(基本数据类型为零值,引用类型为null)
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化。
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量时会随着对象一起分配到java堆中
解析(Reslove)
将常量池中的符号引用转换为直接引用的过程。
初始化
- 初始化阶段就是执行类构造器方法<cinint>()的过程
- cinit方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
- 构造器方法中指令语句按源文件中出现的顺序执行
- <cinit>()不同于类构造器(<init>())
- JVM会保证子类的<cinit>()执行前,父类的<cinit>()已执行完毕
- 虚拟机必须保证一个类的<cinit>()方法在多线程下被同步加锁
注意:
- 类加载的过程是不涉及到堆内存的,只有创建对象时,才会把创建的对象放在堆内存里面。
- 主类在加载过程中需要用到哪些类才会逐步去加载这些类。jar或war包的类不是一次性全部加载的,使用到时才会加载。
2 类加载器的分类
虚拟机自带加载器
JVM支持两种类型的类加载器,分别是引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
Java虚拟机规范中将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
在程序中我们最常见的类加载器始终只有3个。除了Boosstrap是C/C++实现,其他的都是Java实现
Boostrap ClassLoader 引导类加载器
使用C/C++实现,嵌套在JVM内部
Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sum.boot.class.path路径下的内容)都是使用引导类加载器进行加载的
获取Bootstrap加载器加载的路径
URL[] urLs = Launcher.getBootstrapClassPath().getURLs(); for (URL urL : urLs) { System.out.println(urL.toExternalForm()); }
加载扩展类和应用类加载器,并指定为他们的父加载器(并不是继承关系)
出于安全考虑,Boostrap启动类加载器指加载包名为Java、Javax、sun开头的类
Extenstion ClassLoader 扩展类加载器
从Java.ext.dirs系统属性所指定的目录中加载类库,或从/JAVA_HOME/jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
获取扩展类加载器加载的目录
String property = System.getProperty("java.ext.dirs"); for (String s : property.split(";")) { System.out.println(s); }
Application ClassLoader 应用类加载器
对于程序员自己定义的类,默认都是使用应用类加载器进行加载
用户自定义加载器
1 为什么要自定义类加载器?
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏(加密的class文件,在加载器中进行解密后加载)
2 自定义类加载器实现步骤
继承ClassLoader并重写findClass()方法(jdk1.2不建议用户覆盖loadClass()方法)
public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] result = getClassFromCustomPath(name); if (result == null) { throw new FileNotFoundException(name); } return defineClass(name, result, 0, result.length); } catch (FileNotFoundException e) { e.printStackTrace(); throw new ClassNotFoundException(name); } } private byte[] getClassFromCustomPath(String name) { // 在此处获取到字节码文件二进制流 // 如果需要进行解密操作,则在此进行 return null; } }
如果没有太复杂的需求,可以直接继承UrlClassLoader类,避免自己去编写findClass()方法及其获取字节码流的方式。
3 双亲委派机制
类加载器在接收到加载类的请求时,会将加载任务委托给父加载器,依次委托,直到最上级的加载器,如果上级加载器无法完成加载,就会交给下级加载器自己来加载。

优势:
- 类的全局唯一性,避免类的重复加载
- 保证java代码的安全,防止核心API被改写。比如自己写一个java.lang.String。
tomcat会打破双亲委派机制,因为tomcat会加载多个war包,不同的war可能会存在路径和名称都相同的类。
二、运行时数据区
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。
不同的JVM对于内存的划分方式和管理机制存在着部分差异。

线程独有:程序计数器、栈、本地栈
线程共享:堆、堆外内存(方法区)
1 程序计数器(PC寄存器)
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令
- 程序计数器是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域
- 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法(当前栈帧)。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined)

- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等继承功能都需要依赖这个计数器来完成
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 是唯一一个在Java虚拟机规范中没有规范任何OutOfMemoryError情况的区域
例:使用idea jclasslib插件查看指令地址和操作指令

2 Java 虚拟机栈
2.1 Java虚拟机栈是什么
Java虚拟机栈(Java Virtual Machine Stack),每个线程在创建时都会创建一个虚拟机栈,其内部保持一个个的栈帧,对应着一次次的Java方法调用。
生命周期
与线程的生命周期保持一致
作用
主管Java程序的运行,保存方法的局部变量(8种基本数据类型,对象的引用地址)、部分结果、并参与方法的调用和返回
优点
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
- 栈的操作只有两个:入栈、出栈
- 对于栈来说,不存在垃圾回收的问题
栈中可能出现的异常
Java虚拟机规范允许Java栈的大小时动态的或者固定不变的。
我们可以使用参数-Xss
选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
- SrackOverFlowError:如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,Java虚拟机将会抛出StackOverFlowError异常。
- OutOfMemoryError:如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时,没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机会抛出OutOfMemoryError异常。
栈运行原理
- JVM直接对Java栈的操作只有两个,
压栈
、出栈
,遵循先进后出
/后进先出
的原则 - 在一条活动的线程中,一个时间点上,只有一个活动的栈帧。
- 与当前栈帧对应的就是当前方法,定义这个方法的类就是当前类
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
- 如果该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈。

2.2 Java虚拟机栈中存储什么
- 每个线程都有自己的栈,栈中的数据都是以
栈帧
的格式存在 - 在这个线程上正在执行的每个方法都有对应一个栈帧
- 栈帧是一个内存区块、是一个数据集,维系着方法执行过程中的各种数据信息
每个栈帧中存储着:
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
- 一些附加信息

2.3 局部变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用、以及返回地址类型
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全的问题
局部变量表所需的容量大小在编译器确定下来的,并保存在方法的code数据的maximum_local_variables数据项中。在方法运行期间是不会改变局部变量表的大小的,方法嵌套的次数由栈的大小决定。
局部变量表的变量只能在当前方法调用中有效,当方法调用结束后,会随着方法栈帧的销毁、局部变量表也会随之销毁。
关于Slot的理解
参数值的存放总是在局部变量表数组的索引0开始,到数组长度-1的索引结束
局部变量表,最基本的存储单元是Slot(变量槽)
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型,returnAddress(返回地址)类型的变量
在局部变量表里,32位以内的类型只占用一个slot,64位(long、double)的类型占用两个slot
JVM会为局部变量表中的每一个Slot都分配一个访问索引、通过索引即可成功访问到变量值
当一个实例方法被调用的时候、它的方法参数和方法体内部定义的局部变量将会按照顺序被赋值到局部变量表种的每一个Slot上。
如果当前帧是由构造方法或者实例方法创建的、那么该对象引用this将会存放在index为0的slot处。
补充说明:
- 在栈帧中,与性能调优关系最为密切的部分就是局部变量表,在方法执行时,虚拟机使用局部变量表完成方法的传递
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象不会被回收
2.4 操作数栈
操作数栈,在方法执行过程中,根据字节码指令,往栈帧中写入数据或提取数据,即入栈和出栈
- 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。
- 栈中的任何一个元素都是可以任意的Java数据类型
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度
- 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
通过字节码指令角度查看操作数栈

0 bipush 10 # 压入操作数栈
2 istore_1 # 从操作数栈取出栈顶数据,放入局部变量表、索引位置为1
3 bipush 20 # 入栈
5 istore_2 # 从操作数栈取出栈顶数据,放入局部变量表、索引位置为2
6 iload_1 # 取出局部变量表索引位置为1的数据、压入操作数栈
7 iload_2 # 取出局部变量表索引位置为2的数据、压入操作数栈
8 iadd # 求和操作,取出操作数栈的数据,进行运算后再次压入操作数栈
9 istore_3 # 从操作数栈取出栈顶数据放入局部变量表、索引位置为3
10 return

2.5 动态链接(指向运行时常量池的方法引用)
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。动态链接的作用就是为了将这些符号引用转换为调用方法的应用。
通过代码理解:
public class DynamicLinkingTest {
public void methodA() {
}
public void methodB() {
methodA();
}
}
// 通过javap命令获取到class文件的字节码指令
javap -v DynamicLinkingTest.class
这一部分就是常量池的信息,Java虚拟机会将这些信息加载到方法区的运行时常量池中
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."<init>":()V
#2 = Methodref #3.#17 // com/cqsiri/jucstudy/jvm/DynamicLinkingTest.methodA:()V
#3 = Class #18 // com/cqsiri/jucstudy/jvm/DynamicLinkingTest
#4 = Class #19 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 Lcom/cqsiri/jucstudy/jvm/DynamicLinkingTest;
#12 = Utf8 methodA
#13 = Utf8 methodB
#14 = Utf8 SourceFile
#15 = Utf8 DynamicLinkingTest.java
#16 = NameAndType #5:#6 // "<init>":()V
#17 = NameAndType #12:#6 // methodA:()V
#18 = Utf8 com/cqsiri/jucstudy/jvm/DynamicLinkingTest
#19 = Utf8 java/lang/Object
invokevirtual #2,这里的#2就是指向常量池的符号引用
public void methodB();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #2 // Method methodA:()V
4: return
LineNumberTable:
line 10: 0
line 11: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/cqsiri/jucstudy/jvm/DynamicLinkingTest;
总结:
- methodB()方法里调用methodA()方法
- 调用methodA()方法指向#2
- #2最终会指向#6、#12
- 这些"#"号后面带数字的就是符号引用
- 动态链接就会将这些符号引用转换为指向运行时常量池的引用
补充说明:
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标在编译器可知且运行期保持不变时,这种情况下将调用方法的符号引用直接转换为直接引用的过程称之为静态链接
动态链接:
如果被调用方法在编译器无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接
2.6 方法返回地址
存放调用该方法的PC寄存器的值。
一个方法的结束,有两种方式:
- 正常执行完成
- 出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息
2.7 一些附加信息(不一定有)
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如堆程序提供支持的信息。
3 本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈(Native Method Stack)用于管理本地方法的调用。
- 本地方法栈,也是线程私有的
- 本地方法是使用C语言实现的
- 具体做法是Native Method Stack中登记Native方法,在Execution Enginee执行时加载本地方法库
- 当某个线程调用一个本地方法时,它就进入了一个全新的并且不受虚拟机限制的世界。它和虚拟机拥有同样的权限
4 堆区
4.1 堆的核心概述
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
Java堆区在JVM启动时被创建,其空间大小也确定了,通常是JVM管理的最大一块内存区域。堆的内存大小是可以调节的
JVM虚拟机规范规定,堆可以处于物理上不连续的内存空间,但它在逻辑上的连续的
所有线程共享堆内存的数据。在这里还可以划分线程私有的缓冲区(TLAB)
在方法结束后,堆中的对象不会马上被回收,仅仅在进行垃圾收集的时候才会被移除。堆是GC执行垃圾回收的重点区域
堆空间内部结构
4.2 设置堆的内存大小与OOM
堆区用于存储Java对象实例,那么堆区的大小在JVM启动时就设置好了,可以通过
-Xmx
设置最大内存,-Xms
设置初始内存。例:-Xmx2g -Xms2g-Xms:初始化内存大小,等价于
-XX:InitialiHeapSize
-Xmx:堆区扩展的最大内存,等价于
-XX:MaxHeapSize
一旦堆区中的内存大小超过
-Xmx
设置的值、将会抛出OutOfMemoryError异常通常会将
-Xmx
、-Xms
设置为相同的值,目的在于垃圾回收机制清理完堆区的内存后不需要重新分割计算堆区的大小,进而提高性能默认情况下,-Xmx=物理电脑内存/4,-Xms=物理电脑内存/64
4.3 新生代与老年代
4.3.1 存储在JVM中的java对象分为两类:
- 一类是生命周期较短的对象、这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致
4.3.2 新生代与老年代在堆结构的占比(一般采用默认的,不会修改)
- 默认 -XX:NewRatio=2,表示新生代占比1,老年代占比2。新生代占堆内存的1/3,老年代占2/3
- -XX:NewRatio=4,表示新生代占比1,老年代占比4。新生代占堆内存的1/5,老年代占4/5
4.3.3 新生代
- 在HotSpot中,伊甸园(Eden)空间和另外两个Survivor空间所占的比例是8:1:1。
- 可以通过-xx:SurvivorRatio调整这个空间比例。例-XX:SurvivorRatio=8
- 8:1:1的默认占比并不是固定的,虚拟机会根据对象存活的情况自适应调整占比。可以通过参数来关闭自适应调整(-XX:-UseAdaptiveSizePolicy)
- 几乎所有的对象都是在Eden中创建出来的。(Eden空间不够会创建到老年代)
- 绝大部分的对象创建和销毁都在新生代进行了
- 设置新生代的内存大小。通过
-Xmn
设置。例:-Xmn256m,优先级高于比例设置
4.4 图解对象分配过程

- 创建的对象先放在伊甸园区
- 当伊甸园的空间填满时、程序又需要创建对象,JVM垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中不再被其他对象所引用的对象进行销毁。再加载新的对象到伊甸园区
- 伊甸园对象在经过15次垃圾回收时,还存活的对象将会晋升到老年代。通过参数
-XX:MaxTenuringThreahold
进行晋升阈值的设置。 - 伊甸园和幸存者区进行垃圾回收后,存活的对象放到另外一个幸存者区。两个幸存者区谁空谁就是to区。每次进行垃圾回收都会进行交换。
4.4.1 第1次Minor GC的情况

4.4.2 第2次Minor GC的情况

4.4.3 第n次Minor GC的情况
新生代对象每次经过垃圾回收时,如果存活,都会在年龄上进行+1操作,如果在经历15次的回收时,任然存活,则会晋升到老年代中。

4.5 Minor GC、Major GC、Full Gc
JVM在进行GC时,并非每次都对三个内存(新生代、老年代;方法区)区域一起回收的,大部分回收的都是在新生代。
GC按照回收区域又分为两大类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
- 部分收集:
- 新生代收集(Minor GC / Young GC)
- 老年代收集(Major GC / Old GC):CMS GC 会有单独收集老年代的行为
- 混合收集(Mixed GC):收集整个新生代以及部分老年代
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾回收
4.5.1 新生代GC触发机制(Minor GC / Young GC)
- 当伊甸园区空间不足时,会触发Minor GC。Survivor区域满时,不会触发Minor GC
- Java对象大多数都具备朝生夕灭的特性,所有Minor GC非常频繁,一般回收速度也比较快。
- Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
4.5.2 老年代GC触发机制(Major GC / Old GC)
- 出现了Major GC,经常会伴随至少一次的Minor GC(但并非绝对的)。就是说在老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC。
- Major GC的速度一般会比Minor GC慢10倍以上,SWT的时间更长
- 如果Major GC后,内存还不足,就报OOM。
4.5.3 Full GC 触发机制
- 调用System.gc(),系统建议执行Full GC,但是不必然执行。
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的对象大小大于老年代的可用内存
- 新生代回收对象放入to区时,to区的可用内存不足,则将对象转存到老年代,且老年代的可用内存小于对象大小
Full GC是开发或调优中需要尽量避免的
4.6 动态对象年龄判断
如果Survivor 区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreashold中要求的年龄。
4.7 TLAB
TLAB:Thread Local Allocation Buffer 线程本地分配缓冲区
为什么有TLAB?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 为了避免多个线程操作同一地址,需要使用加锁机制,进而影响分配速度
什么是TLAB?
- 从内存模型而不是从垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
- 多线程同时分配内存时,使用TLAB可用避免一系列的非线程安全问题,同时还能提升内存分配的效率,因此这种内存分配方式称为快速分配策略
- JVM将TLAB作为内存分配的首选,如果TLAB空间分配内存失败,JVM会尝试通过加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存
- TLAB内存非常小,默认占Eden区的1%。通过
-XX:UseTLAB
设置是否开启TLAB空间;通过-XX:TLABWasteTargetPercent
自定义TLAB所占Eden空间的百分比大小
4.8 空间担保机制
在发生Minor GC之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象的总空间
- 如果大于,则此次Minor GC是安全的
- 如果小于,则虚拟机会查看
-XX:HandlerPromotionFailure
设置值是否运行担保失败- HandlerPromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
- 如果大于,尝试进行Minor GC
- 如果小于,改为进行一次Full GC
- HandlerPromotionFailure=false,则改为进行Full GC
- HandlerPromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
JDK7之后,参数失效。默认走=true的逻辑
4.9 逃逸分析
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊的情况,就是如果通过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化为栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收。这也是最常见的堆外存储技术。(目前Hot Spot虚拟机并未采用对象的栈上存储,但应用了标量替换)
逃逸分析示例:
public class EscapeAnalysisTest {
public EscapeAnalysisTest obj;
/**
* 方法返回EscapeAnalysisTest对象,发生逃逸
*/
public EscapeAnalysisTest getInstance() {
return obj == null ? new EscapeAnalysisTest() : obj;
}
/**
* 为成员属性赋值,发生逃逸
*/
public void setObj(){
this.obj = new EscapeAnalysisTest();
}
/**
* 方法内部创建的对象,对象的作用域只在方法内部。没有发生逃逸
*/
public static void createObj(){
EscapeAnalysisTest test = new EscapeAnalysisTest();
}
}
结论:能使用局部变量的就不要在方法外部定义
标量替换
标量:指一个无法再分解成更小的数据的数据。Java中的基本数据类型就是标量。相对的,可以再分解的叫做聚合量。
如果经过逃逸分析,对象未被发生逃逸行为,那么对象可能从聚合量分解为标量。
代码演示:
public static void main(String[] args) {
Point point = new Point(1,2);
// 对象没有发生逃逸,那么此时可能会被优化为:
// int x = 1;
// int y = 2;
// 存储在栈中的局部变量表中
}
static class Point{
int x;
int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
4.10 堆区常用JVM参数
参数 | 含义 | 默认值 |
---|---|---|
-Xmx | 堆最大内存 | 1/4 |
-Xms | 堆初始化内存 | 1/64 |
-Xmn | 设置新生代大小 | |
-XX:NewRatio | 新生代、老年代占比 | 2 |
-XX:SurvivorRatio | Eden和Survivor区占比 | 8:1:1 |
-XX:MaxTenuringThreshold | 晋升老年代阈值 | 15 |
-XX:+PrintGCDetails | 输出详细的GC处理日志 | |
-XX:+DoEscapeAnalysis | 开启逃逸分析 | true |
-XX:+EliminateAllocations | 开启标量替换 | true |
5 方法区(Method Area)
5.1 方法区的基本理解
《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做No-Heap(非堆),目的就是要和堆分开,所以方法区看作是独立于Java堆的内存空间
- 方法区与Java堆一样,是各个线程共享的内存区域
- 方法区在JVM启动该的时候被创建,并且它的实际的物理内存空间和Java堆区一样都可以是不连续的(但逻辑上视为连续的)。随着JVM的关闭就是释放这个区域的内存。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
- 方法区的大小决定了系统可以保持多少个类。如果系统中定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:MetaSpace。(JDK7为PermGen space)
- 方法区在jdk1.8后,称作元空间(MateSpace),jdk1.7及以前称为永久代(PermGen Space)
5.2 设置方法区内存的大小(1.8)
- 元数据区大小可以使用参数
-XX:MetaSpaceSize
指定初始化大小;-XX:MaxMatespaceSize
指定最大值 - 默认值依赖于平台。windows下,
-XX:MatespaceSize
是21m,-XX:MaxMatespaceSize
的值是-1,即没有限制 - 如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。
5.3 方法区的内部结构
方法区存储内容主要有:类型信息、运行时常量池、常量、静态变量、即时编译器(JIT)编译后的代码缓存等。
5.3.1 类型信息
对每个加载的类型(class、interface、enum、annation),JVM必须在方法区中存储以下信息:
- 完成有效的名称(包名.类名)
- 直接父类的有效名(interface或者java.lang.Object,没有父类)
- 修饰符(public、final、abstract)
- 实现的接口列表
5.3.2 域(Field)信息
JVM必须在方法区中保持类型的所有域的相关信息以及域的声明顺序,包括域名称、域类型、域修饰符(public、static、final、volatile、transient)
5.3.3 方法(Method)信息
- 方法名称
- 返回类型
- 修饰符
- 字节码、操作数栈、局部变量表大小
- 异常表(每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获异常的异常类的常量池索引)
5.3.4 运行时常量池
- 会将字节码文件中常量池内容放到运行时常量池中
- 在加载类和接口到虚拟机后,就会创建对应的运行时常量池
- 包括编译器以及明确的数值字面量,也包括运行期解析才能够获得的方法或者字段的引用
5.4 方法区演进细节
HotSpot方法区的变化
版本 | 内容 |
---|---|
jdk1.6及之前 | 有永久代,静态变量存放在永久代上 |
jdk1.7 | 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量保存在堆中 |
jdk1.8及以后 | 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍然堆中 |
注意:对象的实例仍然都是放在堆中,只是对象的引用放在了不同的地方
5.5 方法区的垃圾回收
方法区的垃圾回收主要回收两部分内容:常量池中废弃的常量和不在使用的类型。
判断一个类不再使用(可以卸载),需要满足以下三个条件:
- 该类所有的实例都已经被回收,以及任何派生此类的子类的实例。
- 加载该类的类加载器已经被回收
- 对应的Class对象没有在任何地方被使用,无法在任何地方通过反射访问该类的方法。
三、本地方法接口
1 什么是本地方法
简单的讲,一个Native Method就是一个Java调用非Java代码的接口。该方法的实现由非Java语言实现,比如C、C++。例如Thread类的start0()方法,就是native关键字修饰的。
2 为什么要使用Native Method
- 与Java环境外的交互
- 与操作系统的交互
四、执行引擎
1 执行引擎概述
JVM的主要任务就是负责装载字节码文件到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它仅仅包含一些能够被JVM识别的字节码指令、符号表、以及其他辅助信息
那么,如果想让一个Java程序运行起来,执行引擎(Execute Enginee)的任务就是将字节码指令解释/编译为对应平台上机器能够执行的指令。简单来说,JVM的执行引擎充当了将Java语言翻译为机器语言的翻译官
- 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
- 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息
2 什么是解释器(Interpreter)、什么是JIT编译器?

- 解释器:当Java虚拟机启动时会根据预定义规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行
- JIT(Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成本地机器平台相关的机器语言
通过-XX:CompileThreshold
来人为设置方法调用多少次触发JIT热点编译的阈值
五、对象的创建和内存布局
1 对象创建的步骤
1.1 判断对象对应的类是否加载、链接、初始化
- 虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。(即判断元信息是否存在)
- 如果没有,在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为Key进行查找对应的.class文件。
- 如果没有找到文件,抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类对象
1.2 为对象分配内存
计算对象占用空间大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用类型,仅分配引用变量空间即可,即4个字节大小。
- 内存规整:指针碰撞 (所有用过的内存在一边、空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存就把指针向空闲处挪动一段与对象大小相等的距离)
- 内存不规整: 虚拟机维护了一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容
内存是否规整取决于采用哪种垃圾收集器
1.3 处理并发安全问题
- 采用CAS失败重试、区域加锁保证更新的原子性
- 每个线程采用TLAB预先分配一块内存
1.4 属性的默认初始化(零值初始化)
对象的所有属性设置默认值,保证对象实例属性在不赋值时可以直接使用。
1.5 设置对象的对象头
将对象的所属类(即类的元信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中
1.6 执行init方法进行初始化
在Java程序的视角来看,初始化才正式开始。初始化成员变量、执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量
2 对象的内存布局

2.1 对象头
- 运行时元数据
- 哈希值(HashCode)
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 类型指针:指向类元数据,确定该对象所属类型
2.2 实例数据
对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的)
规则:
- 相同宽度的字段总是被分配在一起
- 父类中定义的变量会出现在子类之前
- 如果CompactFields=true,子类的窄变量会插入到父类变量的空隙
2.3 对齐填充
不是必须的、仅仅起到占位符的作用
六、String
1 String的基本特性
- String类声明是final的,不可继承
- String类实现了Serializable接口(支持序列化)、Comparable接口(比较大小)、CharSequence接口
- jdk8及以前内部定义了final char[] 存储字符串数据,jdk9时改为byte[]。
- String:代表不可变的字符序列。简称:不可变性。
- 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
- 当对现有的字符串进行连接操作时,也需要重写指定内存区域赋值
- 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值
- 通过字面量的方式(区别于new)给一个字符串赋值,此时字符串值声明在字符串常量池中。字符串常量池不会存放相同内容的字符串的
- String Pool(字符串常量池)是一个固定大小的HashTable,默认值大小长度是60013(jdk1.6是1009)。如果放进String Pool的String非常多,就会造成Hash冲突严重、从而导致链表很长,性能下降。
- 使用
-XX:StringTableSize
设置StringTable的长度。jdk8时,要求设置的最小值是1009
2 String的内存分配
- 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
- 8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。主要使用方法有两种:
- 双引号声明出来的String对象会直接存储在常量池种
- new关键字创建的String对象可以使用intern()方法
- 所有的字符串都保存在堆中
3 字符串的拼接操作
- 常量与常量的拼接结果在常量池,原理是编译器优化
- 只要其中有一个是变量,结果就在堆中(对象存放区域)。变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象的地址
String a = "a";
String b = "b";
// str1 在编译器进行优化 等价于"ab",存放在字符串常量池
String str1 = "a" + "b";
String str2 = "ab";
// str3 涉及到变量的操作,采用了StringBuilder进行拼接
String str3 = a + "b";
String str4 = "a" + b;
// 字符串调用intern()方法后,如果字符串常量池存在,则直接返回常量池中的地址
// 如果字符串常量池不存在,则创建新的(调用inter方法对象的地址),返回调用inter方法的对象的地址
String str5 = str4.intern();
/*
* 相当于:
* StringBuilder s = new StringBuilder();
* s.append(a);
* s.append(b);
* s.toString(); // 约等于 new String("ab");
* */
String str6 = a + b;
System.out.println(str1 == str2); // true
System.out.println(str2 == str3); // false
System.out.println(str3 == str4); // false
System.out.println(str2 == str5); // true
System.out.println(str5 == str6); // false
4 intern()的使用
如果不是用双引号声明的String对象,可以使用String提供的intern方法:
intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。 也就是说,如果在任意字符串上调用string.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。 通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)。