这些不可不知的JVM知识,我都用思维导图整理好了
先上图: 1、JVM基本概念1.1、JVM是什么JVM 的全称是 「Java Virtual Machine」,也就是我们耳熟能详的 Java 虚拟机。 JVM具备着计算机的基本运算方式,它主要负责把 Java 程序生成的字节码文件,解释成具体系统平台上的机器指令,让其在各个平台运行。 JVM是运行在操作系统上的,它与硬件没有直接的交互。
1.2、Java程序运行过程我们都知道 Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。 也就是如下: 每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。 1.3、JDK、JRE、JVM
JDK中包含JRE,也包括JDK,而JRE也包括JDK。 范围关系:JDK>JRE>JVM。 2、JVM内存区域Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域: 当然,实际上,为了更好的适应 cpu 性能提升,最大限度提升JVM 运行效率,JDK中各个版本对JVM进行了一些迭代,示意图如下: JDK1.6、JDK1.7、JDK1.8 JVM 内存模型主要有以下差异:
2.1、程序计数器一块较小的内存空间,是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。 正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。 这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。 2.2、Java虚拟机栈与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。 虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 局部变量表存放了编译期可知的各种Java虚拟机基本数据类型、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。 2.3、本地方法栈本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。 Hot-Spot虚拟机直接把本地方法栈和虚拟机栈合二为一。 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。 2.4、Java堆对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。 Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”。 从回收内存的角度看,
需要注意的是这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个Java虚拟机具体实现的固有内存布局,HotSpot里面已经出现了不采用分代设计的新垃圾收集器。 Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。 2.5、方法区(JDK1.8移除)方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。 在JDK1.8以前,HotSpot使用永久代来实现方法区,所以某些场合也认为方法区和永久代是一个概念。 在JDK 6的 时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终于完全废弃了永久代的概念,改用在本地内存中实现的元空间(Meta- space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。 如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。 2.6、运行时常量池运行时常量池(Runtime Constant Pool)是方法区的一部分——在JDK1.8已经被移到了元空间。 运行时常量池相对于Class文件常量池的一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的 intern()方法。 2.7、直接内存直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。 显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制。 元空间从虚拟机 Java 堆中转移到本地内存,默认情况下,元空间的大小仅受本地内存的限制。jdk1.8 以前版本的 class 和 JAR 包数据存储在 PermGen 下面 ,PermGen 大小是固定的,而且项目之间无法共用,公有的 class,所以比较容易出现 OOM 异常。
3、JVM中的对象上面已经了解Java虚拟机的运行时数据区域,我们接下来更进一步了解这些虚拟机内存中数据的其他细节,譬如它们是如何创建、如何布局以及如何访问的。以最常用的虚拟机HotSpot和最常用的内存区域Java堆为例,了解一下HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。 3.1、对象的创建Java对象创建的大概过程如下:
3.2、对象的内存布局在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为“Mark Word”。 3.3、对象的访问定位建?对象就是为了使?对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问?式有虚拟机实现?定,?前主流的访问?式有①使?句柄和②直接指针两种:
4、GC垃圾回收对于垃圾回收,主要考虑的就是完成三件事:
4.1、如何判断对象需要回收?4.1.1、引用计数法引用计数法的算法:
客观地说,引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。也有一些比较著名的应用案例,例如微软COM(Component Object Model)技术、使用ActionScript 3的FlashPlayer、Python语言以及在游戏脚本领域得到许多应用的Squirrel中都使用了引用计数算法进行内存管理。 但是,在Java 领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,例如在处理处理一些相互依赖、循环引用时非常复杂。 4.1.2、可达性分析算法当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。 GC Roots 包括;
4.1.3、引用无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。 Java的引用分为四种:强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。
4.2、垃圾收集算法4.2.1、标记-清除算法最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep)算法, 算法分为“ 收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。 后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的。 它的主要缺点有两个:
程的执行效率都随对象数量增长而降低;
标记-清除算法的执行过程如图: 4.2.2、标记-复制算法标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费较多。 标记-复制算法的执行过程如图所示。 4.2.3、标记-整理算法标记-整理算法的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。 “标记-整理”算法的示意图如图: 4.3、分代收集理论当前商业虚拟机的垃圾收集器,大多数都遵循了“
基于这两个假说,收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。 设计者一般至少会把Java堆划分为 时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。 基于这种分代,老年代和新生代具备不同的特点,可以采用不同的垃圾收集算法。
4.4、垃圾收集器4.4.1、Serial收集器Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。这个收集器是一个单线程工作的收集器,但它的“单线 程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。 Serial/Serial Old收 集器的运行过程如下: 4.4.2、ParNew收集器ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。 ParNew收集器的工作过程如图所示: 4.4.3、Parallel Scavenge收集器Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器 Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。 Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。 4.4.4、Serial Old收集器Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中继续讲解。 Serial Old收集器的工作过程如图所示。 4.4.5、Parallel Old收集器Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到JDK 6时才开始提供的。Parallel Old收集器的工作过程如图所示。 4.4.6、CMS收集器CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。 Concurrent Mark Sweep收集器运行过程如图: 4.4.7、Garbage First收集器G1是一款主要面向服务端应用的垃圾收集器,是目前垃圾回收器的前沿成果。HotSpot开发团队最初赋予它的期望是(在比较长期的)未来可以替换掉JDK 5中发布的CMS收集器。现在这个期望目标已经实现过半了,JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。 G1收集器运行过程如图: 5、JVM类加载Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。 5.1、类加载过程一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。 过程如下图: **加载 **: “加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,在加载阶段,Java虚拟机需要完成以下三件事情:
验证: 验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。 验证阶段大致上会完成四个阶段的检验动作: 准备: 准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。 解析: 解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。 5.2、类加载器Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。 5.2.1、类与类加载器类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 5.2.2、双亲委派模型JVM 中内置了三个重要的 ClassLoader,启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分,其他所有 的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
双亲委派模型: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。 为什么要使用双亲委派模型来组织类加载器之间的关系呢?一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的 ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。 5.2.3、破坏双亲委派模型过双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。
第二次被破坏是这个模型自身的缺陷导致的。双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API, 但没有绝对,如果基础类调用会用户的代码怎么办呢? 这不是没有可能的。一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时就放进去的rt.jar),但它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识“这些代码啊。因为这些类不在rt.jar中,但是启动类加载器又需要加载。怎么办呢? 为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认即使应用程序类加载器。 有了线程上下文加载器,JNDI服务使用这个线程上下文加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。但这无可奈何,Java中所有涉及SPI的加载动作基本胜都采用这种方式。例如JNDI,JDBC,JCE,JAXB,JBI等。
双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的。为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。 如果我们自己想定义一个类加载器,破坏双亲委派模型,只需要重写重写其中的loadClass方法,使其不进行双亲委派即可。 5.2.4、Tomcat类加载器架构Tomcat是主流的Java Web服务器之一,为了实现一些特殊的功能需求,自定义了一些类加载器。 Tomcat类加载器如下: Tomcat实际上也是破坏了双亲委派模型的。 Tomact是web容器,可能需要部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。如多个应用都要依赖hollis.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.hollis.Test.class。如果采用默认的双亲委派类加载机制,那么无法加载多个相同的类。 所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。 Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。 6、JVM故障处理6.1、基础故障处理工具6.1.1、jps:虚拟机进程状况工具jps(JVM Process Status Tool),它的功能与 ps 命令类似,可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类) 名称以及这些进程的本地虚拟机唯一 ID ( Local Virtual Machine Identifier,LVMID),类似于 ps -ef | grep java 的功能。 命令格式 jps [ options ] [ hostid ] ? options:选项、参数,不同的参数可以输出需要的信息 ? hostid:远程查看 选项列表 |
描述 |
| 选项列表 | 描述 | -class 监视类加载、卸载数量、总空间以及类装载所耗费时长 -gc 监视 Java 堆情况,包括 Eden 区、2 个 Survivor 区、老年代、永久代或者 jdk1.8 元空间等,容量、已用空间、垃圾收集时间合计等信息 -gccapacity 监视内容与-gc 基本一致,但输出主要关注 Java 堆各个区域使用到的最大、最小空间 -gcutil 监视内容与-gc 基本相同,但输出主要关注已使用空间占总空间的百分比 -gccause 与 -gcutil 功能一样,但是会额外输出导致上一次垃圾收集产生的原因 -gcnew 监视新生代垃圾收集情况 -gcnewcapacity 监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间 -gcold 监视老年代垃圾收集情况 -gcoldcapacity 监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间 -compiler 输出即时编译器编译过的方法、耗时等信息 -printcompilation 输出已经被即时编译的方法选项 | 描述 | -dump 生成 Java 堆转储快照。 -finalizerinfo 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。Linux平台 -heap 显示 Java 堆详细信息,比如:用了哪种回收器、参数配置、分代情况。Linux 平台 -histo 显示堆中对象统计信息,包括类、实例数量、合计容量 -permstat 显示永久代内存状态,jdk1.7,永久代 -F 当虚拟机进程对 -dump 选项没有响应式,可以强制生成快照。Linux平台选项 | 描述 | -F 当正常输出的请求不被响应时,强制输出线程堆栈 -l 除了堆栈外,显示关于锁的附加信息 -m 如果调用的是本地方法的话,可以显示 c/c++的堆栈
---|