JVM 学习补充 --- 2019年4月
学习重点:
JVM 优化、垃圾回收、内存模型、双亲委派机制、对象逃逸、类的加载到卸载过程、字节码文件分析
1、JVM 内存溢出
1、堆溢出:堆要不断的创建对象,如果避免了垃圾回收来清除这些对象,就会产生JVM内存溢出。一般手段是通过内存映像分析工具对Dump出来的堆转储快照进行分析,分清楚到底是内存泄露还是内存溢出。
SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。2、虚拟机栈和本地方法栈溢出:线程请求的栈深度大于虚拟机所允许的最大深度。或者虚拟机在扩展栈时无法申请到足够的内存空间。
3、方法区和运行时常量池溢出:一个类要被垃圾回收器回收,判断条件是苛刻的。
4、本机直接内存溢出。
2、垃圾回收
首先判断对象是否已经死亡(不再被任何途径使用)
1)、引用计数算法,给对象中加一个计数器,一个地方引用该对象,计数器值就加1,当引用失效时,计数器值 就减1。为0的时候就不可能再被使用。但是有个缺点,如果两个对象互相引用对方,就会造成无限循环而不会被回收(类似死锁的机制)。
2)、可达性分析算法,通过一系列 GC Roots 对象作为起点,从这些节点往下搜索,成为链路。如果对象不再链路上,表示可回收。(例如我在方法栈的对象引用指向了堆里的对象实例,就是可达的。所有所有渠道都没有指向堆里的对象实例,就是不可达,该对象就可以被回收)
3)、方法区(永生代)垃圾回收:废弃常量和无用的类。
垃圾收集算法
1)、标记 - 清除算法(最基础),首先标记出所有需要回收的对象,标记完成后统一回收被标记的所有对象。但是有不足的地方:效率太低和清楚后会产生大量不连续的内存碎片。
2)、复制算法,将可用内存分为大小相等的两块,每次只使用其中一块。使用完一块后,就将还活着的对象复制到另一块内存,清空第一块内存。缺点是代价太高昂。
3)、标记 - 整理算法:将标记的死亡对象,都往一端移动,然后清理掉端边界以外的内存。
4)、分代收集算法:把 Java 堆分为新生代和老年代。新生代每次垃圾收集都有大批对象死去,那就用复制算法。老年代就用标记 -整理算法。
垃圾回收器
1)、CMS 收集器,获取最短回收停顿时间为目标的收集器。基础算法用的 标记 - 清除 算法来实现的。整个过程分为4个步骤:初始标记、并发标记、重新标记、并发清除。其中初始标记和重新标记,也要暂停其他工作线程。耗时最长的并发标记和并发清除可以和用户的线程同时工作。
有点:并发收集、低停顿。
适用场景:应用需要更快的响应,更短的停顿、CPU资源也比较丰富,就适合用 CMS 收集器。
2)、G1收集器,可以并行和并发收集、分代收集、空间整合、可预测的停顿。
3、虚拟机类加载机制
Class 文件格式的储存结构只有两种数据类型,无符号数和表。
无符号数:用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
表:多个无符号数或者其他表作为数据页构成的复合数据类型,所有表习惯以"info"结尾。
虚拟机运行时,从Class文件的常量池获得对应的符号引用,再类创建或运行时解析、翻译到具体的内存地址。
类加载机制:把描述类的数据,从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成被虚拟机直接使用的 Java 类型。并且,类型的加载、连接和初始化都在程序运行期间完成。(动态加载和动态连接)
加载 --- 验证 --- 准备 --- 解析 ---初始化 --- 使用 --- 卸载
其中解析可以在初始化阶段之后再开始,支持 Java 语言的运行时绑定。
初始化:遇到 new、getstatic、putstatic、invokestatic 这4条字节码指令时,如果类没有初始化过,则会出发其初始化。
使用反射调用时,如果类没有进行过初始化,也会触发初始化。
当初始化一个类时,该类的父类没有初始化,则先触发其父类的初始化。
当虚拟机启动时,用户需要先指定一个主类(含 main() 方法的类,例如 springboot 的启动类),虚拟机会先初始化该类。
加载:通过一个类的全限定名来获取此类的二进制字节流,然后将字节流代表的静态存储结构转化为方法区的运行时数据结构,然后在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
验证:
1)、文件格式验证,验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
2)、元数据验证,对字节码进行语义分析,保证其描述的信息符合 Java 语言规范。
3)、字节码验证。
4)、符号引用过验证。
准备:将类变量分配内存,并设置类变量初始值的阶段,这些变量所使用的的内存在方法区进行分配。类变量(被 static 修饰的变量),并且赋值动作是在初始化阶段才会执行。但如果用 final 修饰的话,准备阶段就会赋值。
解析:将常量池内的符号引用,替换为直接引用(指向目标的指针或者是一个能间接定位到目标的句柄)。
类加载器:每个类都有一个类加载器,组合起来成为 JVM 里面的唯一性。不同的类加载器,加载的同一个 Class 文件,得到的两个类也必定不相等。
双亲委派模型:顶层为启动类加载器,然后下一级为扩展类加载器,再下一级为应用程序加载器,再下一级为自定义类加载器。除了顶层的启动类加载器,其余的类加载器都有自己的父类加载器。不是以父子关系存在,而是以组合关系来复用父加载器的代码。
当一个类加载器收到类加载请求(自己未加载过),将求情往上委派给父类加载器,一直往上,到顶层启动类加载器。 当父类加载器无法完成这个加载请求,子加载器才会去加载。
扩展知识
系统的各种API类,用的启动类加载器加载。如果自己写相同的API类,包路径一样,双亲委派加载机制,就会让顶层父类加载器加载,但是加载的类有唯一性,所以会报错。
如果没有双亲委派机制,子类加载器自己就加载了一个API类,并且父类也加载了系统API类,在引用的时候,就会不知道引用哪个API类而报错。
new 一个对象时,在堆里面放了对象的实例,把类的元数据放入了方法区。
方法区指向一个常量池,方法区指向一个对象,对象指向该常量池(常量池数据相等)。如果用==号比较方法区的两个不同的引用,地址不同,false。如果用 equal 比较,则相等。
编译的时候,会折叠字符串,例如 String s = '"1" + "2"; 编译的时候 String s = "3"; 所以只会创建一个对象。
对象逃逸:方法内创建的对象,作为返回参数返回出去后,被其他方法引用。或者对象作为参数,传入下级方法等操作。避免了该对象被标记为死亡的(方法结束,对该对象的引用就断开,就可以视该对象死亡,可用垃圾回收器回收)。所以叫对象逃逸。
