1.关于jvm
-
jvm是程序虚拟机(区别于系统虚拟机)
-
运行字节码(不管源文件什么语言)
-
jvm主要功能:内存管理 和 垃圾回收
-
目前主要使用的JVM为HotSpot,它采用解释器和即时编译器并存的架构
翻译字节码(解释执行):一行行执行,响应快,速度慢,类似走路
Jit编译器(翻译执行):像一些重复的代码会保存下来,下次执行就很快,响应慢,速度快,类似等公交车。
-
jvm采用的是基于栈的指令架构
基于栈的指令架构:零地址指令,指令集小,完成一项操作需要更多指令,不与硬件直接打交道,可移植,跨平台。
基于寄存器指令架构:多地址指令,花费更少指令完成一项操作,性能高,指令集架构依赖硬件,可移植性差,设计实现也较复杂。
-
jvm主要考虑类加载器 和 执行引擎
2. 类加载器
2.1 分类
引导类加载器(BootstrapClassLoader):又称启动类加载器,使用c/c++实现,加载Java核心类库的类
自定义类加载器:所有BootstrapClassLoader派生出的子类
继承关系:
2.2 用户自定义加载类用途及实现方式:
用途:
- 隔离加载类(如使用不同中间件时,防止加载的类重名冲突)
- 除了引导类加载器,其他加载器都不是必须的,可以使用自定义在需要的时候动态加载。
- 扩展加载源
- 防止源码篡改(对字节码加密,加载到内存时自定义类加载器解密)
实现方式:
继承ClassLoader或者URLClassLoader()
2.3 类加载三过程(加载-链接-初始化)
- 加载:在内存中生成一个该类的Class对象
- 链接:
- 验证:确保Class文件合法。
- 准备:为类变量分配内存,设置默认初始值,如0,false…(类变量为static修饰的,这里不包含final static ,因为final static变量在编译时已经分配)
- 解析:将常量池的符号引用转换为直接引用
- 初始化:执行类构造器方法 clinit() 的过程,该方法由编译器收集类中所有类变量赋值动作和静态代码块中的语句合并而来
2.4 双亲委派机制
- 原理:类加载器收到加载请求后会把请求委托给父类加载器去加载,父类加载器不能加载,再由子类加载。
- 过程:
- 类加载器收到类加载请求。
- 将类加载请求一直向上委托,直到启动类加载器。
- 启动类加载器看是否能加载这个类(在rt.jar中找是否有这个类,有就加载然后结束),不能夹子就抛出异常,通知子类加载器加载,也就是看扩展类加载器是否能加载这个类(在ext目录下是否有这个类,有就加载结束),不能加载就继续通知子类加载器加载不断重复。
- 优点:
- 避免类的重复加载
- 防止核心API被篡改(沙箱安全机制)
注意:
-
若class对象的加载器不同,就算来源于同一个Class文件,那么这两个类对象也是不相等的
-
若一个类是由用户类加载器加载 的,那么jvm会将类加载器的一个引用作为类型信息的一部分保存在方法区。
-
java程序对类的使用分为主动使用(一般情况)和被动使用,被动使用不会进行类的初始化
3. 运行时数据区
3.1 结构
注:
-
一个进程对应一个方法区和堆
一个线程对应一个程序计数器,虚拟机栈,本地方法栈。 -
**GC:**代表垃圾回收,主要在堆和方法区中
**OOM:**代表内存溢出,在程序计数器中不会发生 -
栈:解决运行问题
堆:解决数据存储问题
3.2 程序计数器
1. 定义:pc寄存器是很小的一块内存空间,每个线程都有,用于存储下一条执行指令的地址,由执行引擎读取。
2. 为什么需要pc寄存器:因为cpu执行多个线程,当切换回某个线程的时候,就需要通过pc寄存器知道当前线程执行的位置。
3.3 虚拟机栈
3.3.1介绍
1. 定义和结构
每个线程创建时会创建一个虚拟机栈,虚拟机栈中保存的是栈帧(内存区块),一个栈帧对应一个方法。
2. 作用:保存局部变量和部分结果,参与方法调用和返回
3. 优点:不存在垃圾回收,速度很快,仅次于程序计数器。
4. 可能异常:java虚拟机栈大小可以固定或者动态变化。(通过-Xss 设置)
固定时:线程请求的栈容量超过固定值,抛出StackOverFlowError。
动态变化时:扩展大小时,内存不足会抛出OutOfMemoryError。
3.3.2 组成
1. 局部变量表
介绍:为一个数子数组(定义的所有类型都能转换为数字),其存储单元为变量槽(Slot),32位以内类型变量占一个槽,64位(long,double)栈两个槽,每个槽都有一个访问索引(0开始),两个槽的变量通过第一个索引访问。
当前栈帧由构造方法或实例方法(非静态方法)创建的话,那么该对象的引用this将作为第一个变量存在索引位为0的变量槽中。
若变量作用域失效,变量槽便可以回收重用,如:
注意:局部变量表中直接或间接引用的对象不会被回收。局部变量表是性能调优的重要部分。
2. 操作数栈
介绍:用于保存计算的中间结果(根据字节码指令,向栈中写入数据或读出数据)。
为什么要用操作数栈:比如下一条字节码为加操作,通过局部变量表是不知道哪两个数相加。但通过操作数栈弹出两个栈顶数就可以。
栈顶缓存技术:虚拟机基于栈式架构,指令集密集,频繁内存读写影响执行速度,栈顶缓存技术通过将栈顶元素全部缓存在物理cpu寄存器中,提高执行引擎的效率。
3. 动态链接
介绍:每个栈帧都保存了 一个 可以指向当前方法所在类的 运行时常量池, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接
两类方法调用:
静态链接:目标方法在编译期可知(对应方法早期绑定)
动态链接:目标方法在运行时才可知(对应方法晚期绑定)
animal类可能时狗猫,所以编译时不知道调用狗的eat()还是猫的eat();所以为晚期绑定。
上图为早期绑定,调用父类的构造方法,编译时就可以确定,将符号引用转换为直接引用。
虚方法和非虚方法
非虚方法就是目标方法在编译期可知,对应早期绑定方法。
调用指令:
注意:invokestatic和invokespecial调用非虚方法,invokevirtual(除去final修饰的) 和invokeinterface 掉虚方法
invokedynamic:java7出现,java8lambda表达式出现,才能直接生成。invokedynamic是为了实现 动态类型语言支持而做的一种改进。
上图可知,java就是静态语言,js和python为动态语言(java8之后,java也支持动态语言的一些特性)
虚方法表:调用虚方法时,会一直去找父类,如果每次这样性能就不好,所以引入虚方法表。每个类中都有一个虚方法表,存储各方法实际入口,如下图,son和father类方法中,蓝色是继承object,白色是自身的,所以调用时就会从虚方法表中获取,如箭头,而不会从父类中依次去找。
4. 方法返回地址
作用:用于存放pc寄存器的值 ,在该方法结束后返回到被调用的位置。如果正常退出就到调用位置的下一条语句,异常退出根据异常表确定(比如try catch捕获了就会接着执行,未捕获就会向上抛 )
5. 一些附加信息(了解)
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如对程序调试提供支持的信息。
3.4 本地方法栈
- java 虚拟机栈用于管理java方法调用,本地方法栈用于管理本地方法调用。
- 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。(因为本地方法主要是c,在本地方法中可以直接调用处理器中的寄存器,分配本地内存堆中的内存等)
- 并不是所有的jvm都支持本地方法。
注意:线程间的栈帧不能相互引用
4. 本地方法接口和本地方法库
4.1 本地方法
本地方法是由native修饰的方法,是java 调用非java代码的接口。用于与java外部环境打交道和提升效率。
4.2 本地方法库
java提供的一些本地方法。
5. 堆
5.1介绍
- 堆是java内存管理的核心区域,一个java进程对应一个jvm实例,存在一个堆内存。堆内存在创建时确定(可设置)。
- 堆可以处于物理上不连续的空间,但在逻辑上需要是连续的。
- 堆中可划分线程私有的缓冲区供线程使用,如TLAB。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
5.2 堆空间细分
Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
5.3 设置堆空间参数
- 设置
堆大小设置
-Xms 用来设置堆空间(年轻代+老年代)的初始内存大小(默认电脑内存大小1/64)
-Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小(默认电脑内存大小1/4)- -X是jvm的运行参数
- ms是memory start
- mx是memory max
注:开发时初始和最大值建议设置成一样的,这样可以避免堆频繁扩充和释放。
新生代和老年代比例设置
新生代中Eden和两个survivor比例设置
- 查看设置的参数
-
方式一: cmd命令:
jps jstat -gc进程id
-
方式二: jvm设置启动参数 (同-Xms):
-XX :+printGcDetails
5.3 对象在堆中的分配过程
新对象先放Eden , YGC后将存活的放to区,再将from区的未超阈值的放to区且值加1,超阈值的放old区,若to区放不下也放old区。具体如下图:
5.4 GC分类
- 部分收集 Partial GC
新生代收集:minor GC/ Young GC(YGC)
老年代收集:major GC/ Old GC
混合收集:Mixed GC(收集整个新生代和部分老年代) - 整堆收集 Full GC (FGC) :收集整个堆区和方法区
注:有的人将major GC和Full GC混用。
5.5 GC触发条件
5.6 TLAB
堆区中的新生代区划分出一部分空间,为每个线程分配一个私有缓存区。这个缓存区被称为TLAB(Thread Local Allocation Buffer)。
因为共享区的数据为了安全会使用加锁等机制,从而影响效率,所以产生TLAB,当线程数据在私有缓存区放不下才放到共享数据区。
5.7堆空间常用的jvm参数
关于-XX:HandlePromotionFailure
解释:-XX:HandlePromotionFailure :
在发生MinorGC前,先检测老年代最大连续空间是否大于新生代所有对象的总空间,如果大于,此次MinorGC就是安全的,如果小于就看XX:HandlePromotionFailure 参数,如果:
- 设置false时:直接进行Full GC。
- 设置true时:先检测老年代最大连续可用空间是否大于历次晋身到老年代对象的平均大小,如果大于,就先MinorGC。如果小于就FullGC。
它是设置空间分配担保,默认是true,在java7以后不使用该参数,直接判断:如果老年代最大连续空间大于新生代所有对象的总空间 或者 老年代最大连续可用空间大于历次晋身到老年代对象的平均大小就进行MinorGC ,否则进行FullGC。
5.8 栈上分配对象
除了堆中分配对象外,在栈上也可以分配对象,不过有条件,经过逃逸分析后发现一个对象如果没有发生逃逸,就可以在栈上分配,如果发生逃逸就不能栈上分配。(逃逸主要是指new的对象是否在方法外使用,因为栈中是栈帧,每个栈帧对应一个方法,所以如果在方法外使用的话,一旦栈帧弹出栈就会出现问题)
栈上分配对象优点:栈帧弹出栈,变量就自动被回收,就不用GC。
java7之后,默认开启了逃逸分析,java7之前需要设置参数开启。
开启逃逸分析后,编译器可以对代码做以下优化:
-
栈上分配(上述介绍)
-
同步省略或锁消除(JIT编译器可以借助逃逸分析来判断一个对象是否只被一个线程访问,如果是,JIT在编译这个同步块的时候,会取消这部分代码的同步,这样能提高并发性和性能)如:
-
分离对象或标量替换
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
解释:
标量:无法分解成更小数据的数据,如java的基本数据类型
聚合量:可以分解的数据。
好处:为栈上分配提供了很好的基础。
注:只有在server模式下,才可以开启逃逸分析。