专题四:JVM原理:

参考:https://www.cnblogs.com/enjiex/p/5079338.html
https://segmentfault.com/a/1190000014395186

Jre,jdk,jvm:
JRE就是Java平台所有的Java程序都是在JRE环境下运行的。
JDK是用来编译、调试程序的开发包。JDK也是Java程序需要在JRE上运行。
JVM是jre的一部分是一个虚拟的计算机,是仿真模拟计算机的各种功能实现的。

什么是jvm?Jvm原理:
Jvm是一种用于计算机设备的规范,他它是虚构出来的机器,是通过在计算机上模拟各种功能实现的。它包含了一套字节码指令集,一组寄存器,一个栈,一个垃圾回收堆和一个存储方法域。
Jvm是Java的核心基础,Java编译器只需要面向jvm,生成jvm能理解的字节码文件,将每一条字节码指令翻译成不同的机器码在特定平台运行。
Jvm屏蔽了与具体操作系统平台相关的信息,使Java程序只需要在虚拟机上运行代码就可以在多种平台上不加修改的运行。Jvm在执行字节码时,实际上还是把字节码解释成具体平台上的机器指令去执行,实现一次编辑、多处运行。

JVM执行过程:
1、加载.class 文件
2、管理分配内存
3、执行垃圾收集
JVM是jdk的底层负责操作系统的交互操作,提供完整的Java运行环境。jdk通过四个步骤装入JVM:
1、创建JVM 环境和配置
2、装载JVM.dll
3、初始化并挂界到实例上
4、调用实例装载并处理class类

类加载器:
执行 main() 方法(静态方法)就需要先加载main方法所在类 HelloLoader
加载成功,则进行链接、初始化等操作。完成后调用 HelloLoader 类中的静态方法 main
加载失败则抛出异常

加载阶段:
通过一个类的全限定名获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的 .Class 对象,作为方法区这个类的各种数据的访问入口

链接阶段:
验证:
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。
准备:
为类变量(static变量)分配内存并且设置该类变量的默认初始值,即零值。
这里不包含用final修饰的static,因为final在编译的时候就会分配好了默认值,准备阶段会显示初始化。这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析:
将常量池内的符号引用转换为直接引用的过程
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行;
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄;解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。

初始化阶段:

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(比如:Class.forName(“com.atguigu.Test”))
  5. 初始化一个类的子类
  6. Java虚拟机启动时被标明为启动类的类
  7. JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
  8. 除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,即不会执行初始化阶段(不会调用 clinit() 方法和 init() 方法)

clinit() 方法:
初始化阶段就是执行类构造器方法 clinit() 的过程
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法
clinit() 方法中的指令按语句在源文件中出现的顺序执行
clinit() 不同于类的构造器。(关联:构造器是虚拟机视角下的 init())
若该类具有父类,JVM会保证子类的 clinit() 执行前,父类的 clinit() 已经执行完毕
虚拟机必须保证一个类的clinit() 方法在多线程下被同步加锁

JVM生命周期:
1)启动,Java虚拟机通过引导类加载器创建一个初始类来完成的,这个类由虚拟机的具体实现指定的
2)执行,程序开始执行时才会运行,程序结束时就会停止。执行Java程序的时候真正执行的实质上是一个叫做Java虚拟机的进程。
3)消亡,
a. 正常结束
b. 遇到异常或错误而异常终止
c. 操作系统错误导致Java虚拟机终止
d. 在Java安全管理器允许的情况下,线程可以调用runtime类或system类的exit方法,或runtime的halt方法。
e. JNI规范描述的加载或卸载Java虚拟机时出现的退出情况。

JVM的区域划分:
大致分为五个部分:
程序计数器
虚拟机栈

方法区
本地方法栈

程序计数器:
我们写的java代码存放在 .Java源文件中,需要先编译成 .class 字节码文件才能JVM执行;在执行字节码指令时,JVM里的程序计数器就是用来记录每个线程当前执行的字节码指令位置;因为会有多个线程执行不同代码,所以每个线程都会由自己的一个程序计算器专门记录当前线程执行的字节码指令位置,各个线程之间的计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。工作时就是通过改变计数器的值来选取下一个要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都依赖于这个计数器来完成。
如果线程正在执行native方法,计数器则为空,这个内存区域是唯一一个在规范中没有规定任何OutOfMemoryError情况的区域。

虚拟机栈:
Java虚拟机栈是一块用来保存每个方法内的局部变量等数据的区域;它和计数器一样每个线程都有自己的Java虚拟机栈,它的生命周期和线程是形同的;线程执行一个方法,Java虚拟机栈就会为这个方法调用创建对应的栈帧,栈帧里有这个方法的局部表量表、操作数栈、动态链接、方法出口等数据,包括这个方法执行的其他相关信息。局部变量就存放了编译期各种基本数据类型、对象引用等;局部变量所需的空间在编译期间分配,方法运行期间局部表量表的大小是完全固定的不会改变的。
也就是说,JVM中的Java虚拟机栈就是调用执行方法时都会给方法创建栈帧然后入栈,方法执行完后就会出栈。规范中对这个区域规定了两种情况:①线程请求深度大于虚拟机允许的深度时,抛出StackOverflowError 异常;②虚拟机是可动态扩展的,当扩展无法申请到足够的内存时会抛出OutOfMemoryError 异常。

Java堆内存:
Java 堆是存放代码中创建的各种对象的关键区域,是Java虚拟机管理中被所有线程共享的内存最大的一块内存区域。这个区域会存放对象实例,所有的对象实例以及数组都要在堆上分配;随着不断优化,栈上分配就变得不那么绝对,栈上分配就有逃逸分析和标量替换:逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体;标量替换允许将对象打散后分配到栈上。
Java堆还是垃圾收集器管理的主要区域,也被称为GC堆。然后方法的栈帧的局部变量表存放这个对象引用类型的局部变量,即存放对象的地址。

方法区:
方法区和Java堆一样是各个线程共享的区域;它主要存放类似于对象实例的自己信息、平时用到的各类信息,还有一些类似常量池的东西也存放在这个区域里。运行时常量池是每个类或接口的常量池在运行时的表示形式,在类或接口被加载后对应的运行时常量池就会被创建出来,运行期间也会有新的常量放进运行时常量池中。
在JDK1.8之前方法区代表JVM的一块区域,但是在JDK1.8之后名字改为“MetaSpace”,但主要还是存放我们自己写的各种类的相关信息。JVM规范并没有强制对这个区实现垃圾回收,所以也称为永久代,也不需要专门设计垃圾回收机制。JDK1.7之后HotsPot虚拟机就将运行时常量池从永久代中移除了。

本地方法栈:
很多地方都会走native方法去调用本地操作系统中的方法,可能调用的是c语言写的方法,或者是一些底层类库。在调用这种方法时,就会有线程对应的本地方法栈,这里面也是跟Java虚拟机栈类似的,存放各种native方法的局部变量表之类的信息。JVM规范中没有对本地方法的具体实现和数据结构做强制规定,虚拟机可以自由实现。

运行时常量池:
运行时常量池是方法区的一部分,用于存放编译期间生成的各种字面量和符号引用,内存有限,无法申请抛出OutOfMemoryError 异常。

直接内存:
是非虚拟机运行时数据区的部分,它是线程共享的。使用native函数库直接分配堆外内存,不经过JVM内存直接访问系统物理内存的类。

JVM垃圾回收:
Java中的引用类型: 强引用、软引用、弱引用、虚引用

强引用:
就是通过new创建一个对象,赋值给一个变量就是强引用,是虚拟机生成的引用;在强引用环境下,垃圾回收要严格判断当前对象是否有被强引用,强引用状态下就不会被垃圾回收。强引用可以直接访问目标对象。强引用可能会导致内存泄漏

软引用:
一般被作为缓存使用,在垃圾回收时虚拟机根据当前系统的剩余内存(紧张或富裕)来判断是否进行回收,也就是说在虚拟机发生OutOfMemory时肯定不存在软引用。

弱引用:
与软引用类似,作为缓存使用;但是在进行垃圾回收时一定会被回收。也就是说弱引用的生命周期只存在于一个垃圾回收周期内。

虚引用:
也被称作幽灵引用,强度最低的引用;虚引用是否存在完全不会对生存时间构成影响。虚引用无法获取实例,垃圾回收随时都有可能回收。唯一作用就是用于追踪。

垃圾回收算法:
按基本回收策略:
①、引用计数:
古老的回收算法。原理是对象有一个引用就增加一个计数,删除一个引用就减少一个计数,回收时就回收计数为0的对象,但是这个算法无法处理循环引用问题。
②、可达性分析清理:
可达性分析法:
通过一系列“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD(死亡)状态的线程还可以恢复,GC不会回收它的内存。(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的)。

标记-清除:两个阶段,第一从引用根节点开始标记所有被引用的对象;第二遍历整个堆把未标记的对象清除;需要暂停整个应用,还会产生内存碎片。
复制:把内存分为两个相等的区域,只使用一个区域,垃圾回收时遍历使用区,把正在使用的对象复制到两一个区域,只处理正在使用的对象。成本小不会出现碎片问题,但是需要两倍的内存空间。
标记-整理:结合前两个算法,分两个阶段;第一从根节点开始标记所有被引用的对象,第二遍历整个堆清除标记对象,把存活对象压缩到堆的其中一块按顺序排放。避免了碎片问题和内存空间问题 。

按分区对待:
①、增量收集:
实时垃圾回收算法,在应用进行时进行垃圾回收。
②、分代回收:
把对象分为年轻代、年老代、持久代,对不同生命周期的对象进行分析后使用不同算法进行回收。

按系统线程:
①、串行收集:
使用单线程处理所有垃圾回收工作,实现容易效率较高。但是无法使用多处理器的优势;所以适合单处理器机器,或小数据情况下的多处理器机器。
②、并行收集:
使用多线程处理垃圾回收工作,速度快效率高;理论上cpu越多越能体现出优势。
③、并发收集:

分代处理:年轻代、年老代、持久代

年轻代:
所有新生成的对象都是先放在年轻代,年轻代的目标就是尽可能快速的收集生命周期短的对象;分为三个区:一个Eden区和两个Survivor区。大部分对象都是在Eden区,进行垃圾回收时:
①Eden区满时,将还活这的对象复制到S0区中的一个,再清空Eden区。
②S0满时,将S0和Eden区的存活对象复制到S1中去,再清空Eden区和S0。
③S1满时,把S0复制过来的还存活的对象复制到老年代,再清空S
垃圾回收触发是根据内存大小和具体算法来决定的,不一定满了才会触发。

老年代:
存放这年轻代复制过来的对象(经过多次垃圾回收还存活),老年代里的对象生存时间比较长;当老年代满时,会对整个堆进行回收,这样回收就会更加彻底,但时消耗的时间也会更长。

持久代:
用于存放一些无效的类、静态变量、静态方法等,对垃圾回收没有明显的影响,但是有些应用会动态生成或调用一些class。当它存满时会直接触发垃圾回收。

垃圾回收的类型:Scavenge GC和Full GC

Scavenge GC:
新生成的对象再Eden区申请空间失败时就会触发,对它进行垃圾回收;这个类型是对年轻代的Eden区进行的,不会影响到年老代。是速度快,效率高的算法,使Eden区能尽快空出空间。

Full GC:
对整个堆进行整理。包括年轻代,年老代和持久代。速度较慢,应尽量减少使用。