JVM概述
为什么学JVM(java Virtual Machine)
对于程序在运行时的性能方面的管理的需要 架构师/高级程序员必须掌握的
对于我们写代码时,是有帮助的,例如对内存的分布管理… 不同垃圾回收器的选择…
让我们对java程序运行过程更加了解,为我们写出优质的代码做好准备
什么是虚拟机
虚拟机是一种软件,是一种模拟的计算机环境,分为系统虚拟机(VMware)和程序虚拟机(JVM 只运行java程序).
JVM的作用
java程序能够实现跨平台,前提是jvm与平台相关的.
jvm首先负责加载字节码到虚拟机中,(类加载器)
负责存储产生变量,对象,类信息…(运行时数据区)
负责将字节码编译/解释为机器码
负责调用操作系统本地方法
一次编译到处运行
自动内存管理
自动垃圾管理
是java程序运行的一个统一管理的虚拟环境.
JVM整体构成部分
虚拟机整体由4个部分构成
1.类加载器(加载字节码文件
- 类加载(从硬盘或网络中加载字节码文件到内存(运行时数据区的方法去)中再去通过这个类模板实例n的对象
- 链接
- 验证
- 准备
- 解析
- 初始化
2.运行时数据区
- 方法区(存类信息)
- 堆(存对象)
- java虚拟机栈(运行java中的方法)
- 程序计数器(记录代码运行到哪行了)
- 本地方法栈(运行本地方法)
3.本地方法接口
- 负责调用本地方法,例如:Object hashCode() 、启动线程 start()、IO 输入read
4.执行引擎
将java字节码 编译/解释为机器码
类加载
类加载子系统
类加载器子系统负责从文件系统或者网络中加载 class 文件。 classLoader只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
加载的类信息存放于一块称为方法区的内存空间。
class file 存在于硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载 JVM 当中来,根据这个模板实例化出 n 个实例. class file 加载到 JVM 中,被称为 DNA 元数据模板. 此过程就要有一个运输工具(类加载器 ClassLoader),扮演一个快递员的角色.
类加载过程
加载
- 1.通过类名(地址)获取字节流
- 2.转换成运行时格式,为每个类创建Class类的对象,存储在方法区中
链接(验证、准备、解析)
-
验证
- 对字节码文件格式进行验证,看是否被污染
- 对基本语法格式进行验证,是否符合java语言规范,例如是否有父类(元数据验证)
-
准备
负责为类的静态属性分配内存,并设置默认值;不包含final修饰的static常量,final修饰的static常量是在**编译(将.java文件编译为.class字节码文件)**时进行初始化
//例如: public static int = 123;//value 在准备阶段后的初始值是 0,而不是 123.
-
解析
- 将类的二进制数据中的符号引用替换成直接引用(符号引用是 Class 文件的逻辑符号,直接引用指向的方法区中某一个地址)
- 将字节码中的表现形式转换为内存中的表现(在内存的地址)
解析在某种情况下也分为静态和动态解析,比如当一个java类被编译为Class文件后,假设这个类为A,其中引用了B,在编译阶段A类是不知道B类有没有被编译的,而且B此时也一定没有被加载,所有A不知道B的实际地址,那么编译后的class文件中,将会使用一个字符S来代表B的地址,那么S就被称为B的符号引用,如果A发生类加载并且到了解析阶段发现B还没有被加载,那么此时会触发B的类加载,B被加载后,A中的符号引用就会转换为直接引用,也就是B实际的内存地址。A调用了一个具体的实现类B,这就称为静态解析,因为解析的目标很明确,而如果B使用了多态,比如B是一个接口,它有多个实现类,那么A在解析时就不知道使用那个具体类的直接引用来进行替换,既然不知道那就等一等吧,直到在运行过程中发生了调用,此时虚拟机调用栈中将会得到具体的类信息,这时候再进行解析,就能用明确的直接引用,来替换符号引用,这也就解释了为什么解析阶段有时候会发生在初始化阶段之后,这就是动态解析,用它来实现后期绑定。java的多态是后期绑定来实现的。
初始化
-
初始化,为类的静态变量赋予正确的初始值
public static int = 123;//value 在初始化阶段后的初始值是123.
-
类什么时候初始化
JVM 规定,每个类或者接口被首次主动使用时才对其进行初始化.
- 通过 new 关键字创建对象
- 使用类中的静态属性(变量或方法)0
- 对某个类进行反射操作 反射Class.forName(“类的地址”)
- 初始化子类会导致父类被初始化
- 执行该类的main方法
其实除了上面的几种主动使用,以下两种情况为被动使用不会加载类.
-
引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致类加载,比如:
public final static int NUMBER = 5 ; //不会导致类初始化,是被动使用 public final static int RANDOM = new Random().nextInt() ; //会导致类加载
-
构造某个类的数组时不会导致该类的初始化,比如:
Student[] students = new Student[10] ;
-
类的初始化顺序
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
public class ClassInit { static { a = 20; } static int a = 10; public static void main(String[] args) { //变量a的值从准备阶段到初始化的变化过程 a-->0-->20-->10 System.out.println(ClassInit.a);//10 } }
类加载器
站在 JVM 的角度看,类加载器可以分为三种:
-
引导类加载器(启动类加载器 Bootstrap ClassLoader).
这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类库.并不继承于 java.lang.ClassLoader 没有父加载器.
-
扩展类加载器(Extension ClassLoader)
Java 语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现.
派生于 ClassLoader 类.
-
应用类加载器(系统类加载器 Application ClassLoader)
Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现.
派生于 ClassLoader 类.
加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类.
该类加载器是程序中默认的类加载器.
-
自定义类加载器
双亲为派机制
Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式.
工作原理
-
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行.
-
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器.
-
如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制.
如果均加载失败,就会抛出 ClassNotFoundException 异常。
如果我们自己创建一个名为 java.lang 的包,再创建一个名为 String 的类,当我们new String()时,会将加载创建核心类库中的 String 对象还是创建我们自己创建的 String 类对象?
package java.lang;//自己创建的包
public class String {
static{
System.out.println("自定义String类");
}
public static void main(String[] args) {
new String();
}
}
/*
这段代码其实不会加载我们伪造的String类,因为双亲委派机制会逐级向上委托父加载器去寻找该类,如果加载了我们伪造的类就会覆盖系统的String类,存在安全问题。但是也允许开发者自定义类加载器,去打破双亲为派机制。
*/
/*
运行结果
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
*/
如何打破双亲为派机制
通过继承ClassLoader去重写 loadClass 方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐) 或重写 findClass 方法 (推荐) 。
我们可以通过自定义类加载重写方法打破双亲委派机制。
再例如 tomcat 等都有自己定义的类加载器.(加载部署在tomcat中的项目时,就使用的是tomcat自己的类加载器)
运行时数据区
程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
程序计数器用来存储下一条指令的地址,即将要执行的指令代码.由执行引擎读取下一条指令
- 它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域.
- 每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致.
- 程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址.
- 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成,在线程切换的时候可以保存上一个线程的执行位置,方便恢复时快速定位。
- 它是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError(内存溢出)情况的区域,因为程序计数器存储的是字节码文件的行号,这个范围是可知的,在开始分配内的时候就会分配一个绝对不会溢出的内存空间
- 执行native方法是程序计数器值为空,因为native方法是c语言实现,没有编译成字节码指令,也就不需要去存储了
本地方法栈(Native Method Stack)
- 用来运行本地方法的区域
- 是线程私有的
- 空间大小是可以调整的
- 可能会出现栈溢出
- 是用C语言实现的
- 它的具体做法是在 Native Method Stack 中登记 native(本地) 方法,在 执行引擎执行时加载本地方法库.
Java 虚拟机栈(Java Virtual Machine Stacks)
-
描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。
-
特点:
-
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器.
JVM 直接对 java 栈的操作只有两个:调用方法入栈.执行结束后出栈.
对于栈来说不存在垃圾回收问题
-
栈中会出现异常,当线程请求的栈深度大于虚拟机所允许的深度时,会出现StackOverflowError(栈溢出).
-
-
栈的运行原理
-
JVM 直接对 java 栈的操作只有两个,就是对栈帧的入栈和出栈,遵循先进后出/后进先出的原则.
-
在一条活动的线程中,一个时间点上,只会有一个活动栈,即只有当前在正在执行方法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈(Current Frame),与当前栈帧对应的方法称为当前方法(Current Method),定义这个方法的类称为当前类(Current Class).
-
执行引擎运行的所有字节码指令只针对当前栈帧进行操作.
-
如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧
-
线程私有,不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法).
-
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧. Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常.不管哪种方式,都会导致栈帧被弹出.
-
-
栈帧的内部结构
-
局部变量表(Local Variables)
- 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。
-
操作数栈(Operand Stack)(或表达式栈)
- 栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
-
动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)
- 因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
-
方法返回地址(Retuen Address)(或方法正常退出或者异常退出的定义)
- 当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址
-
在这里插入图片描述
java堆(Java Heap)
-
基本作用特征
- 是存储空间,用来存储对象,是内存空间最大的一块区域
- 在jvm启动时就被创建,大小也就确定了
- 大小是可调整的
- 所有线程共享java堆,在这里可以划分线程私有的缓冲区
- 本区域是存在垃圾回收的
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
-
堆空间的分区
- 年轻代(youngGen)
- 伊甸园区(eden)(刚创建的对象存放在这里)
- 幸存者区(Survivor)
- 老年代(OldGen)
- 年轻代(youngGen)
-
为什么分代?
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫
描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
-
对象创建内存分配过程
-
new 的新对象先放到伊甸园区,此区大小有限制.
-
当伊甸园的空间填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区.
-
然后将伊甸园区中的剩余对象移动到幸存者 0 区.
-
如果再次出发垃圾回收,此时上次幸存下来存放到幸存者0区的对象,如果没有回收,就会被放到幸存者1区,每次会保证有一个幸存者区是空的.
-
如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区.
-
**在对象头中,它是由4位数据来对GC年龄进行保存的,所以最大值为1111,即为15。所以在对象的GC年龄达到15时,就会从新生代转到老年代。**也可以设置参数,MaxTenuringThreshold=N(N<=15)
-
在老年区,相对悠闲,当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理
-
若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常. Java.lang.OutOfMemoryError:Java heap space
public static void main(String[] args){ List<Integer> list = new ArrayList(); //集合中的对象一直在创建,但不能销毁掉就会导致堆空间溢出 while(true){ list.add(new Random().nextInt()); } }
-
-
新生区和老年区配置比例
- 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优
- 默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3
- 可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5
- 在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发人员可以通过选项**-XX:SurvivorRatio**调整这个空间比例。比如:-XX:SurvivorRatio=8
- 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优
-
分代收集思想 Minor GC、Major GC、Full GC
JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的
都是指新生区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大
类型:一种是部分收集,一种是整堆收集
- 部分收集(不是完整收集java堆的垃圾收集,其中又分为两种)
- 新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.
- 老年区收集(Major GC / Old GC):只是老年区的垃圾收集.
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾
- 调用System.gc();时(不会立即执行)
- 老年区空间不足时
- 方法区空间不足时
- 部分收集(不是完整收集java堆的垃圾收集,其中又分为两种)
方法区(Methed Area)
-
方法区的基本理解
-
方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field 等元数据、static final 常量、static 变量、即时编译器编译 后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但
对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是 要和堆分开.
所以,方法区看做是一块独立于 java 堆的内存空间.
- 方法区在 JVM 启动时被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的.
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展.
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误,关闭 JVM 就会释放这个区域的内存.
-
-
堆栈方法区交互关系
局部变量person持有java堆中Person对象实例的引用,Person实例中又存有方法区中Person类的类型信息的引用
-
方法区的大小设置
java方法区的大小不必是固定的,用户可以根据需要去动态的调整。
- 元数据区的大小可以使用参数MetaspaceSize和MaxMetaspaceSize指定
- 默认值是依赖于平台的,windows下,MetaspaceSize是21MB
- MetaspaceSize 初始值是 21M,也称为高水位线,一旦触及就会触发 Full GC,所以为了减少FULL GC那么给它设置一个较高的值
-
方法区的内部结构
方法区它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存,运行常量池等。运行时常量池就是一张表,虚拟机指令根据这张表,找到要执行的类名、方法名、常量等信息,存放编译期间生成的各种字面量和符号引用。
-
方法区的垃圾回收
- 《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。
- 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。
方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型
类的卸载
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
- 该类的所有实例都已经被回收,也就是java堆中不存在该类及其任何派生子类的的实例。
- 加载该类的类加载器已经被回收,这个条件除非是自己实现的可替换类加载器的情景,否则很难达成
的代码缓存,运行常量池等。运行时常量池就是一张表,虚拟机指令根据这张表,找到要执行的类名、方法名、常量等信息,存放编译期间生成的各种字面量和符号引用。
-
方法区的垃圾回收
- 《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。
- 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。
方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型
类的卸载
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
- 该类的所有实例都已经被回收,也就是java堆中不存在该类及其任何派生子类的的实例。
- 加载该类的类加载器已经被回收,这个条件除非是自己实现的可替换类加载器的情景,否则很难达成
- 该类的class对象没有在任何地方被引用,无法在任何地方通过反射访问该类