运行时数据区
运行时共有数据区:
堆:对象
方法区:方法区存储的是从Class文件加载进来的静态变量、类信息、常量池以及编译器编译后的代码。
私有数据区:
虚拟机栈:
线程私有区域,每一个线程都有独享一个虚拟机栈,因此这是线程安全的区域。
虚拟机栈存放基本数据类型以及对象的引用。
每一个方法执行的时候会在虚拟机栈中创建一个相应栈帧,方法执行完毕后该栈帧就会被销毁。方法栈帧是以先进后出的方式虚拟机栈的。
每一个栈帧又可以划分为局部变量表、操作数栈、动态链接、方法出口以及额外的附加信息。
这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(通常是递归导致的);JVM动态扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
本地方法栈:
本地方法栈是Java程序在调用本地方法的时候创建栈帧的地方
程序计数器:
它的作用就是记录当前线程所执行的位置。 这样,当线程重新获得CPU的执行权的时候,就直接从记录的位置开始执行,分支、循环、跳转、异常处理也都依赖这个程序计数器来完成。此外,程序计数器还具有以下特点:
(1)线程私有,每一个线程都有一个程序计数器,因此它是线程安全的。
(2)唯一一块不存在OutOfMemoryError的区域,可能设计者觉得没必要。
类加载器
Class文件被类加载器主动加载到内存中有以下两种情况:
(1)调用类的构造器方法
(2)调用类的静态变量或者静态方法
类加载步骤:
加载:
(1)通过类的限定名获取其定义的二进制字节流
(2)将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
(3)在堆中生成代表这个类的class对象,作为方法区的访问入口
验证:
(1)文件格式的验证:验证字节流是否符合 Class 文件格式的规范,并且被当前的虚拟机版本处理。例如验证文件是否以魔数0xCAFEBABE开头;主、次版本号是否在当前的Java虚拟机接受范围之内等等。
(2)元数据验证:对字节码描述的信息进行语意分析,以保证其描述的信息是否符合《Java语言规范》的要求。比如验证这个类是否有父类;如果这个类不是抽象类,那么它是否实现了父类或者接口中要求实现的所有方法。
(3)字节码验证:通过数据流分析和控制流分析,确定语意是否合法、符合逻辑。
(4)符号引用验证:最后一个阶段的校验行为发生在虚拟机把符号引用转为直接引用的时候,这个过程在解析阶段发生。符号引用验证可以看作是对类自身以外(常量池子中的各种符号引用)的各类信息进行匹配校验。
准备:
为类分配内存并设置初始值
解析:
将常量池中的符号引用转化为直接引用
符号引用:用符号表示所引用的目标,符号可以是任何形式的字面量。
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄,直接引用和虚拟机实现的内存布局直接相关。
初始化:
为类的静态变量赋予正确的初始值,jvm负责对类进行初始化
双亲委派模式:
Java有三层类加载器:
(1)启动类加载器(Bootstrap Class Loader)
(2)扩展类加载器 (Extension Class Loader)
(3)应用程序类加载器 (Application Class Loader)
如果一个类加载器收到了一个类加载的请求,那么它会把这个请求交给父类加载器去执行,每一个层次的类加载器都是这样。通常只有父类无法完成这个类加载请求后,子类才会自己去完成类加载。这使得Java中的类随它的类加载器一起,具备了一种带有优先级的层次关系。据一个例子,无论哪一个类加载器加载java.lang.Object这个类,都委托给处于模型最顶端的启动类加载器。因此,就能保证Object类在程序的各个类加载器的环境中是同一个类。
双亲委派模型意义:
(1)系统类防止内存中出现多份同样的字节码
(2)保证Java程序安全稳定运行
怎么打破双亲委派:
(1)重写loadClass()
(2)上下文切换类加载器
GC
评判 GC 的两个核心指标:
延迟(Latency):也可以理解为最大停顿时间,即垃圾收集过程中一次 STW 的最长时间,越短越好,一定程度上可以接受频次的增大,GC 技术的主要发展方向。
吞吐量(Throughput):应用系统的生命周期内,由于 GC 线程会占用 Mutator 当前可用的 CPU 时钟周期,吞吐量即为 Mutator 有效花费的时间占系统总运行时间的百分比,例如系统运行了 100 min,GC 耗时 1 min,则系统吞吐量为 99%,吞吐量优先的收集器可以接受较长的停顿。
GC的分类:
(1)Minor GC/Young GC:针对新生代的垃圾收集。
(2)Major GC/Old GC:针对老年代的垃圾收集。
(3)Full GC:针对整个Java堆以及方法区的垃圾收集。
判断对象是否能回收:
(1)引用计数算法:
对每个对象的引用进行计数,每当有一个地方引用它时计数器 +1、引用失效则 -1,引用的计数放到对象头中,大于 0 的对象被认为是存活对象。虽然循环引用的问题可通过 Recycler 算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。
(2)可达性分析算法:
从 GC Root 开始进行对象搜索,可以被搜索到的对象即为可达对象,此时还不足以判断对象是否存活/死亡,需要经过多次标记才能更加准确地确定,整个连通图之外的对象便可以作为垃圾被回收掉。目前 Java 中主流的虚拟机均采用此算法。
垃圾收集算法:
(1)标记-清除:标记出可回收的对象,直接进行回收,会产生内存碎片
(2)复制算法:将不可回收的对象复制到单独的一片空间,然后删除当前空间的垃圾(一般用于新生代,s区移动)
(3)标记整理(old):将存活对象压缩到一片区域
垃圾收集器
Serial 收集器:新生代采用复制算法,老年代采用标记-整理算法(单线程)
ParNew收集器:新生代用复制算法,老年代采用标记-整理算法(多线程)
Parallel Scavenge 收集器:可控制吞吐量,多线程
cms收集器:前面说到过Parallel Scavenge 收集器(java9弃用,14删除),它是一个可以控制吞吐量的垃圾收集器。现在要说的CMS 收集器,它是一个追求最短停顿时间的垃圾收集器,基于标记–清除算法实现的。CMS 垃圾收集器的运作过程相对前面几个垃圾收集器来说比较复杂,整个过程可以分为四个部分:
(1)初始标记: 需要Stop The World,这里仅仅标记GC Roots能够直接关联的对象,所以速度很快。
(2)并发标记: 从关联对象遍历整个GC Roots的引用链,这个过程耗时最长,但是却可以和用户线程并发运行。
(3)重新标记: 修正并发时间,因为用户线程可能会导致标记产生变动,同样需要Stop The World。
(4)并发清除: 清除已经死亡的对象。
cms缺点:
(1)CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
(2)CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。
浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”。
(3)CMS是一款“标记–清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
G1收集器:一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。
ZGC:JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。
oom排查
可以在GC时用jmap dump下内存空间,也可以在启动参数添加 -XX:+HeapDumpBeforeFullGC -XX:+HeapDumpAfterFullGC -XX:HeapDumpPath=e:\dump 自动dump。后面用jhat分析,可以看到那些内存占用内存高。
(1)-Xms设置初始堆的大小,-Xmx设置最大堆的大小
(2)-XX:NewSize年轻代大小,-XX:MaxNewSize年轻代最大值,-Xmn则是相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值
(3)-XX:NewRatio设置年轻代和年老代的比值,如果为3,表示年轻代与老年代比值为1:3,默认值为2
(4)-XX:SurvivorRatio年轻代和两个Survivor的比值,默认8,代表比值为8:1:1
(5)-XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。
(6)-XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代
(7)-XX:MaxDirectMemorySize当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC