Java进阶(4) - JVM
Java文件运行流程
源码 -> 编译 -> 加载 -> 运行 -> 卸载
1.编译流程
Javac(源码) -> 词法分析器(Token) -> 语法分析器(语法树) -> 语义分析器(注解) -> JIT -> 字节码生成器
参考:https://blog.****.net/fuzhongmin05/article/details/54880257/
2.加载流程
1、加载
简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对戏那个存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。
2、链接
链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。
1)、验证
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
格式验证:验证是否符合class文件规范
语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法视频被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过富豪引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
2)、准备
为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)
被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值
3)、解析
将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)
3、初始化
将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法。该方法的作用就是初始化一个中的变量,使用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用<clinit>方法,因为该方法只能在类加载的过程中由JVM调用。
如果父类还没有被初始化,那么优先对父类初始化,但在<clinit>方法内部不会显示调用父类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的父类<clinit>方法已经被执行。
JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。
参考:https://www.cnblogs.com/xiaoxian1369/p/5498817.html
JVM由类加载器、运行时数据区、执行引擎3部分组成
类加载器
有启动类、扩展类、系统类、自定义4种类型加载器
(1)启动类加载器 (Bootstrap ClassLoader )-> 它用来加载 Java 的核心库,比如String、System这些类
-> JRE/lib/rt.jar
(2)扩展类加载器(Ext ClassLoader由Java语言实现,父类加载器为null) -> 它用来加载 Java 的扩展库 -> JRE/lib/ext/*.jar
(3)系统类加载器(AppClassLoader由Java语言实现,父类加载器为扩展类加载器) -> 负责加载用户类路径上所指定的类库,一般来说,Java 应用的类都是由它来完成加载的 -> ClassPath 指定的所有jar或目录
(4)自定义类加载器(父类加载器肯定为AppClassLoader) -> CustomClassLoader
双亲委派机制:类加载器收到类加载请求,自己不加载,向上委托给父类加载,父类加载不了,再自己加载,优势避免Java核心API篡改和避免重复加载。
运行时数据区
由方法区、堆、虚拟机栈、程序计时器、本地方法栈5部分组成
(1)方法区(元空间 metaSpace)
1.存放类信息、静态变量 、常量池、final修饰的常量等数据,存在堆中的永生代。(Java虚拟机会回收)
2.线程共享。
3.方法区无法分配空间,就会抛出内存溢出的异常(OutOfMemoneyError)。
(2)堆
1.存放所有new的对象 ,分为新生代和老生代。(新生代空间Eden - From Survivor - To Survivor为配比8:1:1,新生代gc15次幸存后移到老生代,Copying算法)
2.线程共享。
3.堆中不存放基本类型和对象引用,只存对象本身。
(3)虚拟机栈
1.存放局部变量表、操作数栈、动态连接、方法出口等。虚拟机执行java程序的时候,每个方法都会创建一个栈帧,栈帧存放在java虚拟机栈中,通过压栈出栈的方式进行方法调用。
2.线程私有。
(4)程序计数器
1.程序计数器就是记录当前线程执行的位置,改变计数器的值来确定执行的下一条指令,比如循环、分支、方法跳转、异常处理,线程恢复都是依赖程序计数器来完成。
2.线程私有。(每条线程都有自己私有的程序计数器)
(5)本地方法栈
1.为虚拟机使用到本地方法服务(native)。本地方法栈为线程私有,功能和虚拟机栈非常类似。线程在调用本地方法时,来存储本地方法的局部变量表,本地方法的操作数栈等等信息。
备注:除了程序计数器以外,都会内存溢出异常。 各模型内存大小 可通过jvm参数动态调整 (跟踪参数 堆分配参数 栈分配参数)
参考
https://www.jianshu.com/p/a60d6ef0771b
https://blog.****.net/sinat_33087001/article/details/76977437
堆的类型
年轻代(Young Generation)
1.所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
2.新生代内存按照8:1:1的比例分为一个E区和两个S区(S0、S1),对象大部分情况在Eden区生成,回收时,先将E区存活对象复制到一个S0区,然后清空E区,当S0区也存满了,则将E区和S0区存活对象复制到S1区,然后清空E和S0区。
3.当S1区不足以存放E和S0的存活对象时,就将存活对象直接存放到老年代。如老年代也满了就会触发一次Full GC,也就是年轻、老年代都进行回收(调用 System.gc() 也将触发Full GC)。
4.新生代发生的GC叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)
年老代(Old Generation)
1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长。
持久代(Permanent Generation)
用于存放静态文件,如Java类信息、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。
垃圾回收机制
第一步:判定对象需要回收
1.引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。然而在主流的Java虚拟机里未选用引用计数算法来管理内存,主要原因是它难以解决对象之间相互循环引用的问题,所以出现了另一种对象存活判定算法。
2.可达性分析法
通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
其中可作为GC Roots的对象:虚拟机栈中引用的对象,主要是
1、虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)
2、方法区中静态属性引用的对象(static 可以理解为:引用方法区该静态属性的所有对象)
3、方法区中常量引用的对象(final 可以理解为:引用方法区中常量的所有对象)
4、本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)
参考:https://www.jianshu.com/p/8f5fa8288d9b
https://blog.****.net/luzhensmart/article/details/81431212
第二步:判断对象是否已重写或执行过finalize方法
二次标记流程见上图
第三步: 垃圾回收器回收
1.标记-清除算法,首先标记出所有需要回收的对象,然后进行统一的回收,不足之处有两个:效率低、碎片多。
2.复制算法,将可用内存划分成大小相等的两块,每次只使用一块,当一块用完了,就将还存活的对象复制到另外一块上,然后把已使用的内存空间清理掉。不足之处是将内存缩小到一半,利用率不高。
3.标记-整理算法,与标记-清除类似,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的区域
4.分代收集算法,分代收集是目前jvm普遍采用的算法,即新生代采用复制算法,因为有大量新生对象死去,只有少量存活;老年代采用标记-整理,因为老年代中对象存活率高,没有额外的空间对它进行担保。
参考:https://www.jianshu.com/p/355ae3bcec41