- JVM的作用:解释运行字节码程序,消除平台相关性。JVM将Java字节码解释为具体平台的具体指令。一般的高级语言如要在不同的平台上运行,至少需要编译成不同的目标代码。而引入JVM后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。
- JVM常见问题:https://mp.weixin.qq.com/s/Xo3_ZTVruhFSTMSrmJLvrw
- JVM知识结构图
- 程序计数器:内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
- 虚拟机栈:线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型,每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
- 本地方法栈:Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
- 堆:对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
- 方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 运行时常量池:属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
- 直接内存:非虚拟机运行时数据区的部分
- DR 数据寄存器、IR 指令寄存器、PC 程序计数器
- 一条指令分为:操作+地址,IR保存一条指令,用于发后续信号以便于进行真正的执行、PC用于保存下一条指令的地址
- 在执行一条指令的时候,先把指令存内存取到DR,然后再取到IR,然后再交给指令译码器来转换指令,再向操作控制器发出对应的信号。为了保证程序的顺利执行,所以才需要PC来保存下一条指令的地址,由于大部分指令地址都是连续的,所以+1即可
本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。其关系模型图如下图所示:
- jps -v 可以查看 jvm 进程显示指定的参数
- 使用 -XX:+PrintFlagsFinal 可以看到 JVM 所有参数的值
- jinfo 可以实时查看和调整虚拟机各项参数
- jstat -gc 12538 5000即会每5秒一次显示进程号为12538的java进程的GC情况
当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出 java.lang.OutOfMemoryError。
- 加载了大量的Class(类)
- 采用cglib等反射机制
- 过多的常量也会导致方法区溢出,尤其是字符串
- 在单一的Tomcat实例下运行多个Web应用程序(大量jsp页面)
- 在运行的Tomcat实例中反复"热部署"Web应用程序
- 先查看应用进程号pid:ps -ef | grep 应用名
- 查看pid垃圾回收情况:jstat -gc pid 5000(时间间隔)
- dump 查看方法栈信息:jstack -l pid
- dump 查看JVM内存分配以及使用情况:jmap -heap pid
- dump jvm二进制的内存详细使用情况
一个对象本身的内在结构需要一种描述方式,这个描述信息是以字节码的方法存储在方法区中的。在 HotSpot 虚拟机中,对象在内存中存储布局分为 3 块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
- 对象头
- 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄,这部分信息称为"Mark Word";Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。
- 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
- 如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。
在 64 位系统及 64 位 JVM 下,开启指针压缩,那么头部存放 Class 指针的空间大小还是4字节,而 Mark Word 区域会变大,变成 8 字节,也就是头部最少为 12 字节,如下表所示:
- 实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义顺序的影响。
- 对齐填充
对齐填充不是必然存在的,没有特别的含义,它仅起到占位符的作用。由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,也就是说对象的大小必须是 8 字节的整数倍。对象头部分是 8 字节的倍数,所以当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
JVM逃逸分析
逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用。正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象;故由于无法回收,即称为逃逸。
String test2;
/**
* 逃逸
*/
void test02() {
test2 = "test2";
}
逃逸分析参数设置:
-XX:+DoEscapeAnalysis//使用
-XX:-DoEscapeAnalysis//不用
类加载
- 类加载器:
- 类加载过程:加载,验证,准备,解析,初始化
- 加载:加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。
- 验证:确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 准备:准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
- 解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。
- 初始化:初始化阶段是执行类构造器<client>方法的过程。到了初始阶段,才开始真正执行类中定义的Java程序代码。
-
双亲委派模型
- 定义:除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
- 工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。
- 优点:使用双亲委派模型来组织类加载器之间的关系,使得Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放再rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
- 打破双亲委派机制的方法:重写loadclass()方法
-
打破双亲委派机制的例子:
- Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。打破的目的是为了完成应用间的类隔离。
- JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。
- 沙箱安全机制:防止恶意代码污染java源代码。
符号引用与直接引用
- 符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。个人理解为:在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
- 直接引用 :直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中。直接引用可以是:
- 直接指向目标的指针。(个人理解为:指向对象,类变量和类方法的指针)
- 相对偏移量。(指向实例的变量,方法的指针)
- 一个间接定位到对象的句柄。
JVM加载class文件的原理
JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件的类。
- Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
- 类装载方式,有两种
- 隐式装载,程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中
- 显式装载,通过class.forname()等方法,显式加载需要的类,隐式加载与显式加载的区别:两者本质是一样的。
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
Java程序的运行过程
Java 程序的运行必须经过编写、编译和运行 3 个步骤。
- 编写:是指在 Java 开发环境中进行程序代码的输入,最终形成后缀名为 .java 的 Java 源文件。
- 编译:是指使用 Java 编译器对源文件进行错误排査的过程,编译后将生成后缀名为 .class 的字节码文件,不像C语言那样生成可执行文件。
- 运行:是指使用 Java 解释器将字节码文件翻译成机器代码,执行并显示结果。
GC Roots
作为GCRoots的对象包括下面几种:
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
对象可达性分析
- 如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程去处理。
- 对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从"即将回收"的集合中移除,如果对象还是没有拯救自己,那就会被回收。
垃圾回收机制
堆分为新生代和老年代,新生代默认占总空间的 1/3,老年代默认占 2/3。新生代使用复制算法,有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。当新生代中的 Eden 区内存不足时,就会触发 Minor GC,过程如下:
- 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
- Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
- 移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代
-
Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过
-XX:TargetSurvivorRatio 指定,默认为 50%
- Survivor 区内存不足会发生担保分配
- 年龄超过指定大小的对象可以直接进入老年代
- Major GC,指的是老年代的垃圾清理,但并未找到明确说明何时在进行Major GC
- FullGC,整个堆的垃圾收集,触发条件:
- 每次晋升到老年代的对象平均大小>老年代剩余空间
- MinorGC后存活的对象超过了老年代剩余空间
- 元空间不足
- System.gc():并不是立即执行* finalize方法,而是通知系统进行垃圾回收,但具体什么时候执行还是要由系统自行决定。
- CMS GC异常,promotion failed:MinorGC时,survivor空间放不下,对象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC时,同时有对象要放入老年代,而老年代空间不足造成
- 堆内存分配很大的对象
垃圾回收算法
-
引用计数法:给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任何时刻计数值为0的对象就是不可能再被使用的。
- 缺点:(1)每次给对象赋值时都要维护引用计数器,且计数器本身也有一定的消耗;(2)较难处理循环引用
-
复制算法:(年轻代)
- 优点:(1)不产生内存碎片;(2)速度快
- 缺点:浪费10%的内存空间
-
标记清除算法(老年代):先标记出要回收的对象,然后统一回收这些对象
- 优点:节省空间
- 缺点:产生内存碎片;
-
标记整理算法(老年代):先标记清除,再次扫描并往一端滑动存活对象。
- 优点:不产生内存碎片;
- 缺点:需要移动对象的成本,耗时严重
垃圾收集器
- Serial收集器:新生代收集器,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)
- Serial Old收集器:老年代收集器,单线程收集器,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标 记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。
- ParNew收集器:新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
- Parallel Scavenge 收集器:新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用 Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)
- Parallel Old收集器:老年代收集器,多线程,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清 理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。
- CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间,使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS,当用户线程内存不足时,采用备用方案Serial Old收集。解决内存碎片问题:让CMS在进行一定次数的Full GC(标记清除)的时候进行一次标记整理算法。
- 初始标记(阻塞):标记GC roots直接关联的对象,速度快
- 并发标记:GC Roots Tracing过程
- 重新标记:修正并发标记期间用户进程继续运行而产生变化的标记,耗时比初始标记长,但远小于并发标记
- 并发清除:清除标记的对象
-
G1(Garbage First)收集器:区域化垃圾收集器,物理上不区分新生代和老年代。采用标记整理的算法,与CMS相比:(1)不会产生内存碎片;(2)用户可以指定垃圾回收时间(错峰垃圾回收)。
- 初始标记(InitingMark):标记GC Roots,会STW(Stop The World),一般会复用YoungGC的暂停时间。初始标记会设置好所有分区的NTAMS值。
- 根分区扫描(RootRegionScan):根据初始标记阶段确定的GC根元素,扫描这些元素所在region,获取对老年代的引用,并标记被引用的对象。该阶段与应用线程并发执行,也就是说没有STW停顿,必须在下一次年轻代GC开始之前完成。
- 并发标记(ConcurrentMark):遍历整个堆,查找所有可达的存活对象。若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。此阶段与应用线程并发执行,也允许被年轻代GC打断。
- 最终标记(Remark):此阶段有一次STW暂停,以完成标记周期。G1会清空SATB缓冲区,跟踪未访问到的存活对象,并进行引用处理。
- 清除阶段(Clean UP):这是最后的子阶段,G1在执行统计和清理RSet时会有一次STW停顿。在统计过程中,会把完全空闲的region标记出来,也会标记出适合于进行混合模式GC的候选region。清理阶段有一部分是并发执行的,比如在重置空闲region并将其加入空闲列表时。
- G1收集器相关配置如下:
-Xmx12g
-Xms12g
-XX:+UseG1GC
-XX:InitiatingHeapOccupancyPercent=45
-XX:MaxGCPauseMillis=200
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
- G1将新生代,老年代的物理空间划分取消了。
- G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。H区域可以是连续的用来分配比较大的对象
- 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
- ZGC
ZGC给Hotspot Garbage Collectors增加了两种新技术:着色指针和读屏障。ZGC的标记分为三个阶段。
- 第一阶段是STW,其中GC roots被标记为活对象。
- 第二阶段,同时遍历对象图并标记所有可访问的对象。在此阶段期间,读屏障针使用掩码测试所有已加载的引用,该掩码确定它们是否已标记或尚未标记,如果尚未标记引用,则将其添加到队列以进行标记。
- 在遍历完成之后,有一个最终的,时间很短的的Stop The World阶段,这个阶段处理一些边缘情况(我们现在将它忽略),该阶段完成之后标记阶段就完成了。