导图社区 JVM相关
java虚拟机相关知识,主要内容有通过问题理解JVM、主要组成部分、类加载、JVM内存结构、垃圾回收、java内存模型、调优和拍错。
编辑于2022-01-03 12:10:54JVM相关
通过问题理解JVM
说一下 JVM 的主要组成部分?及其作用? 说一下 JVM 运行时数据区?详细介绍下每个区域的作用? java中都有哪些类加载器? 哪些情况会触发类加载机制? 什么是双亲委派模型? 说一下类装载的执行过程? 哪些变量可以作为GC Roots? Java 中都有哪些引用类型? 说一下 JVM 有哪些垃圾回收算法? 说一下 JVM 有哪些垃圾回收器? 新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别? 简述分代垃圾回收器是怎么工作的? Minor GC与Full GC分别在什么时候发生? 有没有在生产环境下排过错,说说过程? 说一下 你知道的JVM 性能监控的工具? 常用的 JVM 调优的参数都有哪些?
主要组成部分
说一下 JVM 的主要组成部分?及其作用? 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码, 运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的 本地库接口(Native Interface)来实现整个程序的功能 JRE,JDK,JVM,JIT之间由什么不同? JRE代表Java运行时(Java Runtime Environment) , 是运行Java引用所必须的。 jDK 代表Java开发工具(Java development kit) ,是Java程序的开发工具,如Java编译器,它也包含JRE。 JVM代表Java虚拟机( Java virtual machine ) ,它的责任是运行Java应用。 JIT 代表即时编译(Just In Time compilation) , 当代码执行的次数超过一定的阈值时,会将Java字节码转换为本地代码,如,主要的热点代码会被准换为本地代码,这样有利大幅度提高Java应用的性能。
类加载器
java中都有哪些类加载器? 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库; 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库; 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。 自定义类加载器:通过继承ClassLoader抽象类,实现loadClass方法 描述一下JVM加载class文件的原理机制?
运行时数据区
执行引擎
本地库接口
类加载
类子节码
类文件结构
常量池
访问表中
类和接口索引
字段表属性
方法表属性
属性表属性
理解字节码
javac,javap
其他语言:如kotlin
类加载机制
类加载执行
加载
根据查找路径找到相应的 class 文件然后导入;
链接
检查
检查加载的 class 文件的正确性;
准备
给类中的静态变量分配内存空间;
解析
虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化
对静态变量和静态代码块执行初始化工作。
类的加载
使用
卸载
类的加载机制
类加载器的层次
寻找类加载器
类的加载
JVM类加载机制
JVM内存结构

线程私有
程序计数器
程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
作用
一块较小的内存空间,是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数 器,这类内存也称为“线程私有”的内存。
概述
正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native方法,则为空。 这个内存区域是唯一个 在虚拟机中没有 规定任何OutOfMemoryError 情况的区域。
java虚拟机栈
Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息; 1.什么情况下会发生栈内存溢出? 栈为每个线程私有,在不断的调用太多方法时
概述
主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。 是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧( Stack Frame )用于存储 局部变量表、操作数栈、动态链接、方法出口等信息。每- 一个方法从调用直至执行完成的过程,就对应 着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈的存储单位
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在 在这个线程上正在执行的每个方法都各自有对应的一个栈帧 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息 栈帧( Frame )是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束 而销毁一一无论方法是正常完成还是异常完成 (抛出了在方法内未被捕获的异常)都算作方法结束。
栈运行原理
JVM 直接对 Java 栈的操作只有两个,对栈帧的压栈和出栈,遵循“先进后出/后进先出”原则 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class) 执行引擎运行的所有字节码指令只针对当前栈帧进行操作 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧 Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常,不管用哪种方式,都会导致栈帧被弹出
栈帧的内部结构
局部变量表(Local Variables) 操作数栈(Operand Stack)(或称为表达式栈) 动态链接(Dynamic Linking):指向运行时常量池的方法引用 方法返回地址(Return Address):方法正常退出或异常退出的地址 一些附加信息 
局部变量表
操作数栈
动态链接
方法返回地址
一些附加信息
本地方法栈
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的; JVM中栈和堆的区别? 栈是运行时的单位,而堆是存储的单位。 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪。
本地方法接口
简单的讲,一个 Native Method 就是一个 Java 调用非 Java 代码的接口。我们知道的 Unsafe 类就有很多本地方法 为什么要使用本地方法(Native Method)? 与 Java 环境外交互:有时 Java 应用需要与 Java 外面的环境交互,这就是本地方法存在的原因。 与操作系统交互:JVM 支持 Java 语言本身和运行时库,但是有时仍需要依赖一些底层系统的支持。通过本地方法,我们可以实现用 Java 与实现了 jre 的底层系统交互, JVM 的一些部分就是 C 语言写的。 Sun's Java:Sun的解释器就是C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分都是用 Java 实现的,它也通过一些本地方法与外界交互。比如,类 java.lang.Thread 的 setPriority() 的方法是用Java 实现的,但它实现调用的是该类的本地方法 setPrioruty(),该方法是C实现的,并被植入 JVM 内部
本地方法栈
Java 虚拟机栈用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用 本地方法栈也是线程私有的 允许线程固定或者可动态扩展的内存大小 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么 Java虚拟机将会抛出一个OutofMemoryError异常 本地方法是使用 C 语言实现的 它的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区,它甚至可以直接使用本地处理器中的寄存器,直接从本地内存的堆中分配任意数量的内存 并不是所有 JVM 都支持本地方法。因为 Java 虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果 JVM 产品不打算支持 native 方法,也可以无需实现本地方法栈 在 Hotspot JVM 中,直接将本地方法栈和虚拟机栈合二为一
线程共享
堆内存
Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存; java会内存泄露吗?(内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。) 理论上Java因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收也会发生内存泄露。 对象分配原则 java对象创建过程 简述java对象结构
内存划分
为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC 性能):
新生代
新生带(年轻代):新对象和没达到一定年龄的对象都在新生代
Eden
From Survivor(幸存者)
To Survivor
老年代
老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大
元空间
元空间(JDK1.8 之前叫永久代):像一些方法中的操作临时对象等,JDK1.8 之前是占用 JVM 内存,JDK1.8 之后直接使用物理内存
设置堆内存大小和OOM
对象在堆中的生命周期
对象的分配过程
GC垃圾回收简介
TLAB
堆是分配对象存储的唯一选择吗?
方法区
方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。 方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。 虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开。 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的是 String.intern()方法。受方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。 方法区的大小和堆空间一样,可以选择固定大小也可选择可扩展,方法区的大小决定了系统可以放多少个类,如果系统类太多,导致方法区溢出,虚拟机同样会抛出内存溢出错误 JVM 关闭后方法区即被释放 JVM永久代中会发生垃圾回收吗?
解惑
你是否也有看不同的参考资料,有的内存结构图有方法区,有的又是永久代,元数据区,一脸懵逼的时候? 方法区(method area)只是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,并没有规定如何去实现它,不同的厂商有不同的实现。而永久代(PermGen)是 Hotspot 虚拟机特有的概念, Java8 的时候又被元空间取代了,永久代和元空间都可以理解为方法区的落地实现。 永久代物理是堆的一部分,和新生代,老年代地址是连续的(受垃圾回收器管理),而元空间存在于本地内存(我们常说的堆外内存,不受垃圾回收器管理),这样就不受 JVM 限制了,也比较难发生OOM(都会有溢出异常) Java7 中我们通过-XX:PermSize 和 -xx:MaxPermSize 来设置永久代参数,Java8 之后,随着永久代的取消,这些参数也就随之失效了,改为通过-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 用来设置元空间参数 存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中 如果方法区域中的内存不能用于满足分配请求,则 Java 虚拟机抛出 OutOfMemoryError VM 规范说方法区在逻辑上是堆的一部分,但目前实际上是与 Java 堆分开的(Non-Heap)
设置方法区内存的大小
方法区内部结构
类型信息
域信息
方法信息
运行时常量池
常量池
运行时常量池
方法区在JDK 6,7,8中的演进细节
jdk1.6及之前 有永久代,静态变量存放在永久代上 jdk1.7 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中 jdk1.8及之后 取消永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中
移除永久带的原因
为永久代设置空间大小是很难确定的。 在某些场景下,如果动态加载类过多,容易产生 Perm 区的 OOM。如果某个实际 Web 工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现 OOM。而元空间和永久代最大的区别在于,元空间不在虚拟机中,而是使用本地内存,所以默认情况下,元空间的大小仅受本地内存限制 对永久代进行调优较困难
方法区的垃圾回收
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。 先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近 Java 语言层次的常量概念,如文本字符串、被声明为 final 的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量: 类和接口的全限定名 字段的名称和描述符 方法的名称和描述符 HotSpot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收 判定一个类型是否属于“不再被使用的类”,需要同时满足三个条件: 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常很难达成 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 Java 虚拟机被允许堆满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading 、-XX:+TraceClassUnLoading 查看类加载和卸载信息。 在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
直接内存
直接内存并不是JM运行时数据区的一部分, 但也会被频繁的使用:在JDK 1.4引入的NIO提供了基于 Channel与Buffer的IO方式,它可以使用Native函数库直接分配堆外内存,然后使用 DirectByteBuffer对象作为这块内存的引用进行操作(详见: Java I/0扩展),这样就避免了在Java堆和 Native堆中来回复制数据,因此在一些场景中可以显著提高性能。
垃圾回收
判断一个对象是否可以被回收
引用计数算法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。 两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
可达性分析算法
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。 主要是对常量池的回收和对类的卸载。 在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。 类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载: 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。 加载该类的 ClassLoader 已经被回收。 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
Fianlize()
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。 当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。
引用类型
强引用
软引用
弱引用
虚引用
垃圾回收算法
常见的垃圾回收算法有哪些? 下级的所有子标题 怎么判断对象是否可以被回收? 引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题; 可达性分析:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
引用计数法-废弃
引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候, 计数器-1,当计数器为 0 的时候,JVM 就认为对象不再被使用,是“垃圾”了。 缺点:会有循环引用的问题
根搜索算法
根搜索算法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链 (Reference Chain),当一个对象没有被 GC Roots 的引用链连接的时候,说明这个对象是不可用的 哪些变量可以作为GC Roots GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。栈,方法区和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。 1.虚拟机栈(栈帧中的本地变量表)中引用的对象; 2.方法区中的类静态属性引用的对象; 3.方法区中常量引用的对象; 4.本地方法栈中JNI(即一般说的Native方法)中引用的对象
标记-清除
基于根搜索算法: 1.从根进行搜索,从根不可达的对象被认为是垃圾,进行标记 2.对标记过的对象进行清除 缺点:标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间
标记-整理
标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对 象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象 存活时间较长的老年代。
复制
复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上, 然后把这块内存整个清理掉。复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率 不高 现在的 JVM 用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例 不是 1:1(大概是 8:1)
分代收集
分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各个代对象的存活特点,每个代采用不同的垃 圾回收算法。新生代采用复制算法,老年代采用标记—整理算法。垃圾算法的实现涉及大量的程序细节,而且不同的 虚拟机平台实现的方法也各不相同
JVM使用的垃圾回收算法
垃圾回收器
说一下 JVM 有哪些垃圾回收器? 新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别? - 新生代回收器:Serial、ParNew、Parallel Scavenge - 老年代回收器:Serial Old、Parallel Old、CMS - 整堆回收器:G1 新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。 简述分代垃圾回收器是怎么工作的? 分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下: 把 Eden + From Survivor 存活的对象放入 To Survivor 区; 清空 Eden 和 From Survivor 分区; From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
新生代
Serial 与Parallel GC之间的不同之处 Serial与Parallel在GC执行的时候都会引|起stop-the-world.它们之间主要不同serial收集器是默认 的复制收集器,执行GC的时候只有一个线程,而parallel收集器使用多个GC线程来执行。
Serial搜集器
最早的单线程串行垃圾回收器。 Serial 翻译为串行,也就是说它以串行的方式执行。 它是单线程的收集器,只会使用一个线程进行垃圾收集工作。 它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
ParNew收集器
是 Serial 的多线程版本 它是 Serial 收集器的多线程版本。 是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。 默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
Paralle Scavenge搜集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器……看上去和ParNew都一样,那它有什么特别之处呢? Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
老年代
Serial Old收集器
Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案。 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途: 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
Parallel Old搜集器
是 Parallel 老生代版本,Parallel 使用的是复制的内存回收算法,Parallel Old 使用的是标记-整理的内存回收算法
CMS收集器
一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统。 CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 分为以下四个流程: 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 并发清除: 不需要停顿。 在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。 具有以下缺点: 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
G1收集器
一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。 G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。 G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。 通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。 每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。 如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤: 初始标记 并发标记 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 筛选回收: 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 具备如下特点: 空间整合: 整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 可预测的停顿: 能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
ZGC
使用场景:巨大的堆和较小的停顿时间 Z Garbage Collector,即ZGC,基于Region内存布局,不设置分代, 使用了读屏障, 染色指针和内存多重映射等技术实现的可并发标记-整理算法的,是一个可伸缩的、低延迟的垃圾收集器,主要为了满足如下目标进行设计: 停顿时间不会超过10ms 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在10ms以下) 可支持几百M,甚至几T的堆大小(最大支持4T)
内存分配与回收策略
Minor GC和Full GC
Minor GC与Full GC分别在什么时候发生? 新生代内存不够用时候发生MGC也叫YGC,JVM堆内存不够的时候发生FGC在
Minor GC
发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 当无法为新对象分配内存的时候会触发Minor GC
Full GC
堆整个堆进行回收,耗时主要在老年代上,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
内存分配策略
1. 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC
2. 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。 经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。 -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制
3. 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。 -XX:MaxTenuringThreshold 用来定义年龄的阈值
4. 动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
5. 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。 如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。
Full GC的触发条件
1. 旧生代空间不足 旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 2. Permanet Generation空间满 PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 3. CMS GC时出现promotion failed和concurrent mode failure 对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。 4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间 这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
1. 调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
2. 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。 为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
3. 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节
4. JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。 当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。 为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
5. Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
java内存模型
基础
产生的原因
内存模型概念
重排序与顺序一致性
内存模式组成
主内存
工作内存
JVM并发问题
工作内存数据一致性
指令重排序优化
内存间的交互操作
交互操作流程
8种内存基本原子操作
运行规则
内存交互基本操作的3个特性
先行发生原则(Happens-before)
内存屏障
8种操作同步规则
Volatile型变量
Final型变量的特殊规则
synchronized的特殊规则
调优和拍错
有没有在生产环境下排过错,说说过程? 可以说案例分析里面的案例,也可以说内存飙高的排查
调优思路
jvm调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。
性能指标
调优的时候需要关注的东西
吞吐量
CPU在用户应用程序运行的时间 / (CPU在用户应用程序运行的时间 + CPU垃圾回收的时间)
延迟
其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。
内存占用
垃圾收集器流畅运行所需要 的内存大小
性能调优原则
1MinorGC回收原则
每次minor GC 都要尽可能多的收集垃圾对象。以减少应用程序发生Full GC的频率
2GC内存最大化原则
处理吞吐量和延迟问题时候,垃圾处理器能使用的内存越大,垃圾收集的效果越好,应用程序也会越来越流畅。
3GC调优3选2原则
在性能属性里面,吞吐量、延迟、内存占用,我们只能选择其中两个进行调优,不可三者兼得。
调优类型
系统延迟需求
在调优之前,我们需要知道系统的延迟需求是那些,以及对应的延迟可调优指标是那些。 应用程序可接受的平均停滞时间: 此时间与测量的Minor GC持续时间进行比较。 可接受的Minor GC频率:Minor GC的频率与可容忍的值进行比较。 可接受的最大停顿时间: 最大停顿时间与最差情况下FullGC的持续时间进行比较。 可接受的最大停顿发生的频率:基本就是FullGC的频率。 以上中,平均停滞时间和最大停顿时间,对用户体验最为重要,可以多关注。 基于以上的要求,我们需要统计以下数据: MinorGC的持续时间; 统计MinorGC的次数; FullGC的最差持续时间; 最差情况下,FullGC的频率;
优化新生代的大小
比如如上的gc日志中,我们可以看到Minor GC的平均持续时间=0.069秒,MinorGC 的频率为0.389秒一次。 如果,我们系统的设置的平均停滞时间为50ms,当前的69ms明显是太长了,就需要调整。 我们知道新生代空间越大,Minor GC的GC时间越长,频率越低。 如果想减少其持续时长,就需要减少其空间大小。 如果想减小其频率,就需要加大其空间大小。 为了降低改变新生代的大小对其他区域的最小影响。在改变新生代空间大小的时候,尽量保持老年代空间的大小。 比如此次减少了新生代空间10%的大小,应该保持老年代和持代的大小不变化,第一步调优后的参数如下变化: java -Xms359m -Xmx359m -Xmn126m -XX:PermSize=5m -XX:MaxPermSize=5m 新生代的大小有140m变为126,堆大小顺应变化,此时老年代是没有变化的。
优化老年代的大小
同上一步一样,在优化之前,也需要采集gc日志的数据。此次我们关注的是FullGC的持续时间和频率。 可以调整老年代的大小来调整FullGC的频率
jvm参数
常用的 JVM 调优的参数都有哪些? -Xms2g:初始化推大小为 2g; -Xmx2g:堆最大内存为 2g; -Xmn500M 设置年轻代为500m; -XX:PermSize=500M ; 1.8之后采用 MetaspaceSize -XX:MaxPermSize=500M ; 1.8之后采用 MaxMetaspaceSize -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4; -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2; –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合; -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合; -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合; -XX:+PrintGC:开启打印 gc 信息; -XX:+PrintGCDetails:打印 gc 详细信息。 -XX:MaxDirectMemorySize : 设置直接内存的大小
OOM分析
Java堆内存溢出
在 Java 堆中只要不断的创建对象,并且 GC-Roots 到对象之间存在引用链,这样 JVM 就不会回收对象。 只要将-Xms(最小堆),-Xmx(最大堆) 设置为一样禁止自动扩展堆内存。 当使用一个 while(true) 循环来不断创建对象就会发生 OutOfMemory,还可以使用 -XX:+HeapDumpOutofMemoryErorr 当发生 OOM 时会自动 dump 堆栈到文件中。 public static void main(String[] args) { List<String> list = new ArrayList<>(10) ; while (true){ list.add("1") ; } }
MetaSpace(元数据)内存溢出
JDK8 中将永久代移除,使用 MetaSpace 来保存类加载之后的类信息,字符串常量池也被移动到 Java 堆。 PermSize 和 MaxPermSize 已经不能使用了,在 JDK8 中配置这两个参数将会发出警告。 JDK 8 中将类信息移到到了本地堆内存(Native Heap)中,将原有的永久代移动到了本地堆中成为 MetaSpace ,如果不指定该区域的大小,JVM 将会动态的调整。 可以使用 -XX:MaxMetaspaceSize=10M 来限制最大元数据。这样当不停的创建类时将会占满该区域并出现 OOM。 public static void main(String[] args) { while (true){ Enhancer enhancer = new Enhancer() ; enhancer.setSuperclass(HeapOOM.class); enhancer.setUseCache(false) ; enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invoke(o,objects) ; } }); enhancer.create() ; } }
调优工具
说一下 你知道的JVM 性能监控的工具? JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。 jps jinfo jstat jmap jstack jconsole:用于对 JVM 中的内存、线程和类等进行监控; jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等 idea + jprofiler内存分析 调优命令有哪些 Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo 1. jps , JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。 2. jstat , JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机 进程中的类装载、内存、垃圾收集、JIT编译等运行数据。 3. jmap , JVM Memory Map命令用于生成heap dump文件 4. jhat , JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump , jhat内置了 -一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看 5. jstack ,用于生成java虚拟机当前时刻的线程快照。 6. jinfo , jVM Configuration info这个命令作用是实时查看和调整虚拟机运行参数
在线调试
字节码增强技术
动态调试原理
动态调试工具
对象模型
执行引擎