一、概述
1.1 JVM结构
- 方法区/堆是多线程共享的
- 虚拟机栈/本地方法栈/程序计数器是每个线程独有一份的,即线程私有
1.2 虚拟机历史
1.3 结构分析
1.3.1 类加载器
类的生命周期
加载、验证、准备、解析、初始化、使用、卸载
1.3.2 运行时数据区
(1)程序计数器
程序计数器Program Counter Register是一小块内存空间,它表示的是执行字节码指令的行号,字节码解释器工作时就是通过改变程序计数器来选取下一条需要执行的字节码指令
程序计数器有以下特点:
- 线程私有
- 线程执行的是Java方法,则程序计数器记录的是字节码指令的地址
- 线程执行的是本地(Native)方法,那么程序计数器的值为空(Undefined)
(2)虚拟机栈
一个程序的运行都要维护一个堆栈,这里的堆用于存放数据,而栈用于处理数据是如何执行的,JVM也包含这样的设计思想,这里所要介绍的虚拟机栈就是用来存放方法执行的线程内存模型。
它有以下几个特点:
- 每个线程创建时,都会创建一个虚拟机栈
- 当线程中的方法被调用时,都会将其作为一个栈帧压入到虚拟机栈中
- 虚拟机栈是线程私有的
虚拟机栈存在以下异常:
- StackOverflowError:线程请求的栈的深度大于虚拟机所允许的深度,则抛出此异常
- OutOfMemoryError:虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存是,则抛出此异常
(3)本地方法栈
本地方法栈是服务于本地(Native)方法的,虚拟机栈是服务于Java方法的,这两个本质上没什么区别,在HotSpot虚拟机中直接将两者合二为一了
(4)Java堆
Java堆的作用是存放对象的实例,它有以下特点:
- 线程共享
- Java堆是被垃圾收集器管理的内存区域
存在的异常:
- OutOfMemoryError:当Java堆无法再为实例分配足够的内存,且堆无法再扩展时,抛出此异常
(5)方法区
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
方法区还包含一个运行时常量池(Runtime Constant Pool),Java编译为Class文件后,Class文件除了有类的版本、字段、方法、接口等描述信息外,还包含常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
存在的异常:
- OutOfMemoryError:当方法区无法满足新的内存分配需求时,抛出此异常
1.3.4 执行引擎
执行引擎包含三个部分:解释器、JIT即时编译器、垃圾回收器
JVM执行引擎的主要任务就是将字节码指令解释为对应平台上的本地机器指令
二、类加载器
2.1 类加载过程
2.2 类加载器分类
2.2.1 启动(引导)类加载器
- 这个类加载器使用C/C++实现,嵌套在JVM内部
- 用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 没有父加载器,并不继承java.lang.ClassLoader
- 负责加载扩展类加载器和系统类加载器,并指定未他们的父类加载器
- 出于安全考虑,Bootstrap启动类加载器只加载包名未java、javax、sun等开头的类
2.2.2 扩展类加载器
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
2.2.3 系统(应用程序)类加载器
- Java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器
2.2.4 用户自定义类加载器
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏
2.3 双亲委派机制
双亲委派机制是指子加载器加载时会向上传递依靠父加载器进行加载,如果父加载器无法进行处理才会一步步的交给子加载器进行类的加载操作
三、程序寄存器(PC寄存器)
3.1 概念
程序计数器(Program Counter Register),寄存器Register的命名来源于CPU的寄存器,寄存器存储指令相关的信息,CPU只有把数据装载到寄存器中才能运行
JVM的PC寄存器的概念跟物理上CPU的寄存器有些区别,PC寄存器是一种对物理寄存器的抽象模拟
3.2 作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令
3.3 特点
- 所占空间内容小
- 线程私有,生命周期与线程一致
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前程序正在执行的Java方法的JVM指令地址;如果是在执行native方法,则是未指定值(undefined)
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
- 此区域不会出现OurOfMemoryError(OOM)
四、虚拟机栈
4.1 概念
Java虚拟机栈也叫Java栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用
特点:线程私有
生命周期:与线程一致
作用:主管Java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回
4.2 栈帧的内部结构
- 局部变量表Local Variables
- 操作数栈Operate Stack:
- 动态链接Dynamic Linking:指向运行时常量池的方法引用
- 方法返回地址Return Address:
- 一些附加信息
4.3 局部变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据包括各类基本数据类型、对象引用(reference),以及returnAddress类型
局部变量表所需的容量大小是在编译器期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
boolean、byte、short、char在存储前会被转换位int
4.4 slot的理解
Slot(变量槽)是局部变量表的最基础的存储单元
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
JVM会为局部变量表中的每个Slot都分配一个访问索引,通过这个索即可成功访问到局部变量表中指定的局部变量值
4.5 操作数栈
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
32位的类型占用一个栈单位深度
64位类型占用两个栈单位深度
4.6 动态链接(方法的调用)
动态链接:栈帧内部指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接,比如invokedynamic指令
动态链接的作用:在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
4.6.1 方法的绑定机制
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
早期绑定:早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。这里就体现出了多态的特性
4.6.2 虚方法、非虚方法和虚方法表
非虚方法:如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。如静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。其他方法称为虚方法。
子类多态性的前提:1、类的继承关系;2、方法的重写
虚方法表:由于Java中会经常使用到动态分派,如果在每次动态分派过程中都要重新在类的方法元数据中搜索合适的目标就容易影响效率,所以JVM在每个类的方法区中都建立一个虚方法表,使用索引表来查找虚方法
虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕
如果Animal中重写了toString()方法,则toString()的就会指向Animal,否则就会指向Object
4.6.3 方法调用指令
虚拟机中提供了以下几条方法调用指令:·普通调用指令:
invokestatic:调用静态方法,解析阶段确定唯一方法版本
invokespecial:调用
<init>
方法、私有及父类方法,解析阶段确定唯一方法版本invokevirtual:调用所有虚方法
invokeinterface:调用接口方法
动态调用指令:
- invokedynamic:动态解析出需要调用的方法,然后执行前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
4.6.4 invokedynamic指令
动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征,比如js、python。
Java仍然属于静态语言,但拥有动态特性,动态性体现在lambda表达式上
4.6.5 方法重写的本质
找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C
先从C开始查找,如果从类型c中找到与常量池中的描述与名称都相符的方法则进行访问权限校验(即public/protected/private访问权限的设置或者不同包之间的访问权限),如果通过则返回这个方法的直接引用,如果不通过,则返回java.lang.IllegalAccessError异常。
否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
4.7 方法返回地址return address
方法返回地址存放该方法的PC寄存器的值,即调用该方法的指令的下一条指令的地址
一个方法的结束,有两种方式:
- 正常执行完成,方法退出后返回到该方法被调用的位置
- 出现未处理的异常,非正常退出,返回地址要通过异常表来确定,栈帧不会保存这部分信息
方法正常调用后使用哪一条返回指令需要根据方法返回值的实际数据类型确定,方法返回指令:ireturn(byte、char、short、int、boolearn)、lreturn、freturn、dreturn、areturn(引用类型),最后return指令供void、实例初始化方法、类和接口的初始化方法使用
方法的退出就是当前栈帧出栈的过程,此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器等,让调用者方法继续执行下去
4.8、本地方法接口
Native Method就是一个Java调用非Java代码的接口,比如C/C++
Java在定义一个native method时,并不提供实现体(有些像定义一个Java接口),因为其实现体是由非java语言在外面实现的
Java中的本地方法用native进行修饰,native可以与所有其它的java标识符连用,但是abstract除外
4.8.1 使用本地方法的原因
4.8.2 本地方法栈
五、堆
5.1 概述
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域,Java堆区在JVM启动时候即被创建,其空间大小也就确定了。堆是JVM管理的最大一块内存空间,堆的大小是可以调节的
堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
- 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
- 从实际使用角度看:“几乎”所有的对象实例都在堆分配内存,但并非全部。因为还有一些对象是在栈上分配的(逃逸分析,标量替换)
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
5.2 堆空间结构
- JDK7把堆空间分为新生代+老年代+永久代
- JDK8把堆空间分为新生代+老年代+元空间
- 年轻代中的S0和S1分别表示幸存者一区(from区)和幸存者二区(to区),程序运行时只会选择其中一个存放数据
- 新生代与老年代的比例为1:2
5.2.1 新生代
新生成的对象优先存放在新生区中,新生区对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高,默认的新生代与老年代的比例为1:2
HotSpot将新生区划分为三块,一块较大的Eden(伊甸)空间和两块较小的Survivor(幸存者)空间,分别称为From区和To区,默认比例为8:1:1
划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC
5.2.2 老年代
在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
- 大对象直接进入老年代:JVM中有这样一个参数 -XX: PretenureSizeThreshold ,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区以及2个Survivor区之间来回复制,产生大量的内存复制操作
- 对象年龄:对象通常在Eden区诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor中,并且将其对象设为1岁,对象在Survivor区中每熬过一次Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认15),就会被晋升到老年代中,对象晋升老年代的年龄阈值, 可以通过参数-XX:MaxTenuringThreshold设置
5.2.3 永久代/元空间
永久区存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
- JDK1.6之前,永久代,常量池在方法区
- JDK1.7,永久代,但是慢慢退化了,去永久代,常量池在堆中
- JDK1.8之后,无永久代,常量池在元空间
这个区域常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,存储的是Java运行的一些环境或者类信息,这个区域不存在垃圾回收。关闭VM虚拟机就会释放这个区域的内存。
5.3 对象内存分配过程
- 对象优先在Eden区分配,当进行YGC时,会将存活的对象放到From区,To区空着不用。
- 当第二次进行YGC时,会将From区和Eden区存活的对象复制到To区,此时Eden区和From区就为空。
- 当第三次进行YGC时,会将To区和Eden区存活的对象复制到From区,此时Eden区和To就为空。
- 按照此规律,循环往复下去
5.3.1 TLAB
TLAB全称为Thread Local Allocation Buffer,由于以下原因,从而导致TLAB的产生
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内
存空间是线程不安全的 - 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
现在new一个对象,然后TLAB分配,如果TLAB空间够用,那么就对象实例化,如果不够用就只能用Eden公共的部分,如果还不够用,那么就触发GC
5.4 GC垃圾回收
JVM在进行Gc时,并非每次都对新生代、老年代、方法区一起回收的,大部分时候回收的都是指新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC / Young Gc):只是新生代的垃圾收集
- 老年代收集(Major Gc / old GC):只是老年代的垃圾收集。
目前,只有CMS GC会有单独收集老年代的行为。注意,很多时候Major Gc会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。 - 混合收集(Mixed Gc):收集整个新生代以及部分老年代的垃圾收集。
目前,只有G1 Gc会有这种行为
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
5.4.1 触发条件
- 年轻代Minor GC:当年轻代空间不足时,就会触发Minor Gc,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次 Minor GC会清理年轻代的内存)。
- 因为Java对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。Minor GC会引发STW,即暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
- 老年代Major Gc:指发生在老年代的Gc,对象从老年代消失时,我们说“Major GC”或“Full GC”发生了
- 在老年代空间不足时,会先尝试触发Minor Gc。如果之后空间还不足,则触发Major GC
- Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
- 如果Major GC后,内存还不足,就报OOM了
- 整堆收集(Full GC)
- 调用System.gc()时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 有Edan区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把对象转存到老年代,且老年代的可用内存小于该对象大小
- Full GC是开发或调优中尽量避免的
5.5 栈上分配
5.6 代码优化
六、方法区
6.1 概述
《Java虚拟机规范》中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
方法区具有以下特点:
- 方法区((Method Area)与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java.lang.outOfMemoryError
- java.lang. outOfMemoryError: PermGen space或者java.lang. outOfMemoryError: Metaspace
- 加载大量的第三方的Jar包
- Tomcat部署的工程太多
- 大量动态的生成反射类
- 关闭JVM就会释放这个区域的内存。
本质上,永久代、元空间都是对方法区的实现,不能直接对其画等号,因为还有其他的方法区实现方式,永久代和永久代存在本质区别:
- 元空间不在虚拟机设置的内存中,而是使用本地内存
6.2 方法区大小设置
- jdk7及以前:
- 通过-XX:PermSize来设置永久代初始分配空间。默认值是20.75M
- -XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
- 当JV8M加载的类信息容量超过了这个值,会报异常outOfMemoryError:PermGen space。
- jdk8及以后:
- 元数据区大小可以使用参数-XX:Metaspacesize和-XX:MaxMetaspaceSize指定,替代上述原有的两个参数。
- 默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspacesize 的值是-1,即没有限制。
- 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常outOfMemoryError: Metaspace
- -XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-XX:Metaspacesize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于Gc后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspacesize时,适当提高该值。如果释放空间过多,则适当降低该值。
- 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC ,建议将-XX:Metaspacesize设置为一个相对较高的值。
6.3 内部结构
6.3.1 存放内容
- 类型信息
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java.lang.0bject,都没有父类)
- 这个类型的修饰符(public, abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
- 域信息(成员变量)
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
- 域的相关信息包括:域名称、域类型、域修饰符(public, private,protected,static,final, volatile, transient的某个子集)
- 方法信息
- 方法名称
- 方法的返回类型(或void)·方法参数的数量和类型(按顺序)
- 方法的修饰符(public, private,protected,static, final,synchronized,native,abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表(abstract和native方法除外)
- 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
- 静态变量、常量、即时编译器编译后的代码缓存
6.4 运行时常量池
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(constant pool Table),包括各种字面量和对类型、域和方法的符号引用。
常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。简单来说就是存放字面量和符号引用
常量池是 *.class
文件中的。当类的字节码被加载到内存中后,他的常量池信息就会集中放入到一块内存,这块内存就称为运行时常量池,并且把里面的符号地址
变为真实地址
。
以下为java代码编译后的*.class
文件的常量池内容
1 | // ===========================================常量池=============================================== |