JVM学习
1、类加载器
类加载器子系统:从文件系统或网络中加载class文件,class文件在文件开头有特定的文件标识 CAFEBABE
类加载器加载的类信息,会放在方法区的内存空间。
1.1、类加载的过程
加载阶段:
1、通过类的全限定类名获取此类的二进制流
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
链接阶段
-
验证(Verify):
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,确保被加载类的正确性。不会危害虚拟机自身安全
- 四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
-
准备
- 为类变量分配内存并设置该类的默认初始化值。
- 不包含用final修饰的static ,因为final在编译的时候就会分配了,准备阶段会显示初始化;
- 不会为实例变量分配初始化,类变量会分配在方法区,实例变量会随着对象一起分配到Java堆中。
-
解析
- 将常量池内的符号引用转换为直接引用的过程
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。直接引用就是指向目标的指针、相对偏移量或者一个间接定位到目标的句柄
- 解析动作主要针对 类或接口、字段、类方法、方法类型等
初始化阶段
- 初始化阶段就是执行类构造方法的过程
- 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
- 构造器方法中指令是按照语句在文件中出现的顺序执行
<Clinit>( )
不同于类的构造器。(注意:类构造器是虚拟机视角下的<init>()
)- 若该类有super类,JVM会保证子类的
<clinit>()
执行前,super类的已经执行完毕 - 虚拟机必须保证一个类的
<clinit>
方法在多线程下被同步加锁
1.2、类加载器的分类
总的来说类加载器分为两大类:引导类加载器,用户自定义类加载器
如图所示:
在图中除了Bootstrap Class Loader以外,其他的类加载器都直接或间接的继承ClassLoader
1.2.1、引导类加载器
(启动类加载器 Bootstrap Class Loader)
-
c/c++实现,嵌套在JVM内部
-
用来加载JAVA核心库(jre/lib/rt.jar 、resource.jar 或者sun.boot.class.path路径下的内容,用于提供JVM自身需要的类)
-
没有继承 ClassLoader
-
加载 扩展类和应用程序类加载器,并指定他们的父类加载器
-
只加载java、javax、sun开头的类
1.2.2、扩展类加载器
(Extension ClassLoader)
-
java编写的
-
派生于ClassLoader类
-
是由引导类加载器 加载的
-
从java.ext.dirs系统属性所制定的目录下加载类库,或者从JDK的安装目录jre/lib/ext子目录下加载类库。用户创建的JAR放在此位置,也会自动由扩展类加载器加载
1.2.3、应用程序类加载器
(系统类加载器 AppClassLoader)
- java编写
- 派生于ClassLader
- 由扩展类加载器 加载
- 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载程序中默认的类加载器
- 通过ClassLoader.getSystemClassLoader()方法可以获取该类加载器
1.2.4、用户自定义类加载器
为什么要自定义类加载器?
-
隔离加载类
-
修改类加载的方式
-
扩展加载源
-
防止源码泄露
自定义类加载器实现步骤
1、可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
2、在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载器,但是在JDK1.2之后,不建议覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
3、在编写自定义类加载器时,如果没有太复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式。
1.3、ClassLoader类的常用方法
方法名称 | 描述 |
---|---|
getParent() | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载名称为name的类,返回java.lang.Class类的实例 |
findClass(String name) | 使用指定的二进制名称查找类。 |
findLoadedClass(String name) | 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例 |
defineClass(String name ,byte[] b , int off , int len) | 将一个 byte 数组转换为java.lang.Class类的实例。 |
resolveClass(Class<?> c) | 连接指定的一个java类 |
获取ClassLoader的方法
1、class.getClassLoader() :获取当前类的类加载器
2、Thread.currentThread().getContextClassLoader(); 获取当前线程上下文的ClassLoader
3、ClassLoader.getSystemClassLoader();获取当前系统的ClassLoader
4、DriverManager.getCallerClassLoader() 获取调用者的ClassLoager000
1.4、双亲委派机制
工作原理
1、如果一个类加载器收到了类加载得请求,他并不会自己去加载,而是把这个请求委托给父类加载器去执行
2、如果父类加载器还存在父类加载器,则进一步向上委托,依次递归请求,最终将到达引导类加载器
3、如果父类加载器可以完成加载任务,就成功返回,若父类加载器无法完成加载任务,子类加载器才会尝试自己去加载
==注意:==这里说的父类加载器,指的是加载该类加载器的类加载器
优势
1、避免类的重复加载
2、保护程序安全,防止核心api被篡改。
1.5、类的使用方式
类的使用方式分为主动使用和被动使用
主动使用
1、创建类的实例
2、访问某个类或借口的静态变量,或者对静态变量赋值
3、调用类的静态方法
4、反射
5、初始化一个类的子类
6、Java虚拟机启动时被标明为启动类的类
7、JDK7开始提供的动态语言支持
其他的使用JAVA类的方式都是类的被动使用,都不会进行类的初始化
2、运行时数据区
Java虚拟机定义了部分程序运行期间会使用运行时数据区,其中一部分随虚拟机的创建销毁而创建销毁,一部分是跟线程对应的
运行时数据区内存模型
2.1、程序计数器
(program Counter Register) 程序计数寄存器 又称PC寄存器
作用:PC寄存器用来存储当前线程指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。
- 是一块较小的内存空间
- 每一条Java虚拟机线程都有自己的pc寄存器。
- 可看作当前线程所执行的字节码的行号指示器
- 任意时刻,一条Java虚拟机线程只会运行一个方法,该方法被称为当前方法
- 若该方法不是native的,那pc寄存器就保存Java虚拟机正在执行的字节码指令的地址
若是native的,那么pc寄存器的值是undefined。
- pc寄存器的容量,至少可以存一个returnAddress类型的数据或者一个与平台相关的
本地指针的值
- 此区域是唯一一个在Java虚拟机规范中没有规定任何OurOfMemoryError情况的区域
2.2、Java虚拟机栈
Java虚拟机栈是什么?
java虚拟机栈(Java Virtual Machine Stack),早期也叫java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(Stack Frame),对应着一次次的Java方法调用。
一个栈帧对应着一个方法
生命周期
随着线程的创建而创建,消失而消失。
作用
主管Java程序的运行,他保存方法的局部变量、部分结果、并参与方法的调用和返回。
异常
Java虚拟机规范允许栈的大小是动态的或者固定不变的
- 如果线程请求的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机抛出StackOverflowError异常
- 如果Java虚拟机栈可以动态扩展,在尝试扩展时无法申请足够内存,或创建新的线程时没有足够内存去创建对应的虚拟机栈,那么Java虚拟机将会抛出OutOfMemoryError异常
2.2.1、栈的存储单位
每个线程都有私有的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在的
在线程中正在执行的每个方法都各自对应一个栈帧(Frame)
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中各种数据信息
注意:不同线程中所包含的栈帧是不允许存在相互引用的,既不可能 在一个栈帧之中引用另外一个线程的栈帧。
Java方法的两种返回函数的方式
一种是通过return指令进行正常的函数返回,另外一种是抛出异常,不管使用哪种方式,都会导致栈帧被弹出。
2.2.2、栈帧的存储结构
在栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或异常退出的定义)
- 一些附加信息
1、局部变量表
-
定义为一个数组,用于存储方法参数和定义在方法体内的局部变量,包括基本数据类型、对象引用、returnAddress类型
-
局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
-
**局部变量表的大小是在编译期确定下来的。**方法运行期间不会改变局部变量表的大小
-
局部变量表随着方法栈帧的销毁而销毁
局部变量表的基本存储单位:Slot(变量槽)
在局部变量表里,32位以内的类型只占用一个Slot(包括returnAddress类型),64位的类型(long或double)占用两个Slot。
非静态方法栈帧的局部变量表中索引为零的位置上会多一个this(对当前对象的引用)
局部变量与成员变量在赋值时的区别
变量的分类:按照数据类型分:1、基本数据类型;2、引用数据类型
按照在类中声明的位置分:1、成员变量(类变量,实例变量);2、局部变量
成员变量:在使用前,都默认的初始化赋值
类变量 :lingking的prepare阶段:给类变量默认赋值 —> inital阶段:给类变量显示赋值;
实例变量:随着对象的创建,在堆空间中分配实例变量空间,并进行默认赋值
局部变量:在使用前,必须显示赋值,否则编译不通过。
2、操作数栈
在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,及入栈(push)、出栈(pop)。
主要保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
操作数栈的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。
在操作数栈里,32位以内的类型只占用一个栈单位深度,64位的类型(long或double)占用两个栈单位深度。
通过i++和++i理解 操作数栈
3、动态链接
指向运行时常量池的方法引用
每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了就是为了支持当前方法的代码能够实现动态链接。
动态链接的作用
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用被保存在class文件的常量池中。
描述一个方法调用另外其他方法时,就是通过常量池中指向方法的符号引用来表示的,动态链接就是为了将这些符号引用转换为调用方法的直接引用。
4、方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
(以下内容理解时想想多态)
静态链接:
当一个字节码文件被转载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
动态链接
如果被调用的方法在编译期无法被确定下来,即只能在程序运行期将调用方法的符号引用转换为直接引用。由于这种引用转换的过程具备动态性,因此也就被称之为动态链接
方法的绑定机制
对应的方法的绑定机制:早期绑定和晚期绑定。绑定是一个字段,方法或者类在符号引用被替换为直接引用弄个的过程,这仅仅发生一次
虚方法和非虚方法
- 普通调用指令
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用方法,私有及父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
- 动态调用指令
- invokedynamic:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法
方法重写的本质
1、找到操作数栈顶的第一个元素所执行的对象的实际类型,记作 C。
2、如果在类型C中找到与常量池中的描述符和简单名称都符合的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
3、否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
4、如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
虚方法表
在面向对象的编程中,会频繁的使用动态分派,如果每次动态分派的过程都要重新在类的方法元数据中搜索合适的目标的话就会影响到执行效率。
为了提高性能,JVM采用在类的方法区建立一个虚方法表来实现。使用索引表来替代查找。
每个类都有一个虚方法表,表中存放着各种方法的实际入口。
虚方法表的创建时间
虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
5、方法返回地址
存放调用该方法的pc寄存器的值。
方法的结束方式
1、正常执行完成
2、出现未处理的异常,非正常退出
正常退出,调用者的PC寄存器的值作为返回地址,
异常退出,返回地址是通过异常表来确定的,栈帧中不会存储这部分信息
6、附加信息
栈帧中还允许携带一些与Java虚拟机实现相关的一些附加信息。不一定有
2.3、本地方法栈
Native Method 是一个Java调用非Java代码的接口。
- 用于支持native方法(指使用java以外的其他语言写的方法)的执行。
- 如果虚拟机支持该栈,则该栈在线程创建时创建
- 可固定大小,也可根据计算动态扩展和收缩
- 可能的异常与Java虚拟机栈一样
当某个线程调用一个本地方法时,他就不在受虚拟机限制,和虚拟机有相同的权限
2.4、堆
2.4.1、概述
- 一个JVM实例只存在一个堆内存,在JVM启动的时候即被创建,其空间大小也被确定。是JVM管理的最大的一块内存空间
- 堆内存大小可调节
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上视为连续的
- 所有线程都共享Java堆,还可以划分线程私有的缓冲区。
- 《Java虚拟机规范》描述:所有的对象实例以及数组都应当在运行时分配在堆上。
- “几乎”所有的对象实例都在这里分配内存。
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
- 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
栈、堆、方法区的联系
2.4.2、堆内存细分
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
- JDK7及之前:堆内存逻辑上分为新生区,养老区,永久区。
- JDK8及以后:堆内存逻辑上分为新生区,养老区,元空间。
约定:
新生区<=>新生代<=>年轻代;
养老区<=>老年区<=>老年代
永久区<=>永久代
堆内存结构示意图
2.4.3、设置堆空间大小
可以通过-Xmx
,-Xms
来进行设置堆空间大小
-Xms
:用于表示堆区的起始内存,等价于-XX:InitialHeapSize
-Xmx
:用于表示堆区的最大内存,等价于-XX:MaxHeapSize
一旦堆区中的内存大小超过-Xmx
所指定的最大内存时,将会抛出OutOfMemoryError异常
通常将 -Xms
和Xmx
两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分割计算堆区的大小,从而提高性能
默认情况下,初始内存大小:物理内存大小的 1/64
最大内存大小:物理内存大小的 1/4
查看设置的参数:
方式一:cmd中jps
, jstat -gc 进程id
方式二:加参数-XX:+PrintGCDeTails
2.4.4、新生代与老年代
存储在JVM中的Java对象分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常快
- 一类是生命周期非常长的对象
堆可细分为年轻代、老年代
年轻代又可以划分为 Eden空间,Survivor0(from区),Survivor1空间(to区)
配置新生代与老年代在堆结构的占比
默认 -XX:NewRatio=2
,表示新生代占1,老年代占2,新生代占整个堆得1/3
修改为-XX:NewRatio=4
,表示新生代占1,老年代占4,新生代占整个堆得1/5
设置新生代中Eden区与Survivor区比例
HotSpot中,新生代的Eden空间和另外两个Survivor空间所占比例,默认为8:1:1(官网)
但是事实上通过JVisualVM发现,并不是8:1:1,而是6:1:1;这是因为开启了自适应的内存分配策略。
可以通过-XX:SurvivorRatio=8
来调整这个空间比例为8:1:1。
几乎所有的对象都是从“Eden”区new出来的,绝大部分的Java对象的销毁都在新生代进行
-Xmn
:设置新生代的空间大小,一般使用默认值
2.4.5、图解对象分配过程
总结:
- 针对幸存者S0,S1区的总结:复制之后有交换,谁空谁是to。
- 关于垃圾回收:频繁在新生代收集,很少在老年代收集,机会不在永久代/元空间收集
对象分配的特殊情况
2.4.6、区分Minor GC(Y GC) 、Major GC、Full 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堆和方法区的垃圾收集。
2.4.7、内存分配策略
针对不同年龄段的对象分配原则:
- 优先分配到Eden
- 大对象直接分配老年代
- 尽量避免程序中出现大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold 中要求的年龄。
- 空间分配担保 :
-XX:HandlePromotionFailure
2.4.8、对象分配过程 TLAB
为什么有TLAB(Thread Local Allocation Buffer)?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 为可避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
什么是TLAB?
- 从内存模型的角度,对Eden区进行划分,JVM为每个线程分配一个私有缓存区域,它包含在Eden空间
- 多线程同时分配时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此可以将这种内存分配方式称之为快速分配策略
- 由OpenJDK衍生出来的JVM都提供TLAB。
详细说明TLAB
-
尽管不是所有的对象实力都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
-
在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间,默认开启。
-
默认情况下,TLAB空间内存非常小,仅占“Eden”区的1%;
图解
2.4.9、堆空间常用参数
-
-Xms
:用于表示堆区的起始内存,等价于-XX:InitialHeapSize
-
-Xmx
:用于表示堆区的最大内存,等价于-XX:MaxHeapSize
-
-Xmn
:设置新生代的空间大小,一般使用默认值 -
-XX:+PrintFlagsInitial
:查看所有的参数的默认初始值 -
-XX:+PrintFlagsFinal
:查看所有的参数的最终值(可能存在修改,不再是初始值) -
具体查看某进程中某个参数的指令:
- 步骤1、
jps
:查看当前运行的java程序进程 - 步骤2、
jinfo -flag 参数名称 进程id
- 步骤1、