JVM简述

概述

jvm是java中很重要的一块知识,也是面试常问的问题之一,今天笔者就带你深入了解一下jvm的知识,本文分以下几部分

  1. jvm的内存模型
  2. jvm的垃圾回收机制及回收算法
  3. jvm的常见参数
  4. jvm的监控及调优

一、jvm内存模型

JVM简述

上图即为jvm的内存模型示意图,jvm的主要分为堆、栈、方法区、程序计数器四部分。其中方法区和堆是线程共享的。栈、程序计数器是线程私有的。下面我将逐一解释这几个部分

程序计数器:在多线程情况下,线程会根据时间片来抢夺CPU的资源。即便在多线程情况下,cpu也只会在同一时间执行一条语句,程序计数器即在每一个线程执行时记录其执行位置,以便被打断后能恢复至正确的位置执行。

:栈里存储的都是局部变量,用以引用堆和方法区中的对象和常量。此处可抛出StackOverflowException和OutOfMemoryException(大多数--此处在《深入理解JVM虚拟机》中一书提到)

:堆中存放的是对象的实例。绝大部分的对象实例在这里分配内存。堆可以处在物理不连续的内存空间上,只要逻辑上连续就可以。java堆在是实现时,可以是固定的,也可以是可扩展的(-Xmx 最大)( -Xms最小)超过可扩展的最大大小时抛出OOM异常。堆的大小在理论上应不超过32G,一旦超过则jvm原有对堆的优化就会失效,在提升jvm大小时要考虑这个问题。

方法区: 方法区中存放的都是常量和静态变量、被加载的类信息以及被即时编译器编译后的代码。方法区和永久带分别为jvm的规范和HotSpot虚拟机的具体实现。运行时常量池也是方法区的一部分。

方法区的回收:主要是对常量池的回收及对类型的卸载。

回收类:1、此类的所有实例已被回收。 2、此类的ClassLoader已被回收 3、此类在任何地方没有没反射引用

二、jvm的垃圾回收机制及回收算法

垃圾回收算法(*)

垃圾回收的区域位于

引用计数法

     循环引用

可达性分析

      通过GCRoot向下查找,查找的路径称为引用链,一旦某个对象不处于引用链上,则该对象是不可用的。

可作为GCRoot的对象:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中Native方法引用的对象 

标记—清除算法

标记清除算法分为两个部分标记清除

标记即从GCRoot开始标记存活对象,之后清除其他未被标记对象

效率问题:标记和清除两步的效率都不高

空间问题:标记—清除算法清理对象时并不会移动对象,所以在清除过后易产生大量不连续的内存碎片,在以后分配大对象时,由于连续空间不足会提前触发GC

复制算法

复制法即将目标内存区域分为两块,每次只使用其中一块,一块内存用完就将此块内存存活对象复制至另一块,再将此块内存清空。新生代的收集及采用此种算法,因为在新生代的对象存活率低。在jvm中将新生代分为较大Eden和两个较小的survivor空间。每次使用其中一块Eden和survivor,回收时将存活的放入另一块survivor中,再清理掉之前的。默认比例8:1:1。也就是每次新生代中可用内存空间为整个新生代容量的90% ( 80%+10% ),只有10% 的内存会被“浪费”。survivor空间不够时,需要依赖其他内存(老年代)进行分配担保,即让对象进入老年代。

JVM简述

新生代进入老生代的情况

  • 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。
  • 大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
  • 长期存活的对象(-XX:MaxTenuringThreshold)**将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。
  • 动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

为什么分代收集

  • 不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率.

HotSpot算法实现

1.枚举根结点

GC Roots在全局性引用(常量或类静态属性)和执行上下文(栈帧的本地变量表)中,如果太多的话不可能一一进行检查,太消耗时间。同时,GC检查时会出现GC停顿,即可达性分析工作必须在一个能确保一致性的快照中进行,此时在整个分析期间整个执行系统仿佛被冻结,对象的引用关系不会出现变化,否则分析结果无法保证。即GC进行时必须停顿所有的Java执行线程。目前虚拟机使用的都是准确式GC(虚拟机自己知道内存中某个位置的具体数据是什么类型,即知道哪些地方存放着对象引用)。在类加载完成时,使用OopMap数据结构(OOP,普通对象指针)来进行查看对象的存放地址。GC扫描时就可以直接得到信息。

2.安全点

GC Roots枚举的问题:可能导致引用关系变化,或者说OopMap内容变化的指令非常多。如果每一条指定都生成OopMap,那将会需要大量的额外空间,GC的空间成本将会变很高。

安全点:没有每条指令都生成OopMap,只在特定位置记录了信息,即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。由于安全点的选定既不能太少以致于让GC等待时间太长,也不能太过于频繁以致于过分增大运行时的负荷。因此,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”来选定的。长时间运行即指令序列复用,例如方法调用、循环跳转、异常跳转等。

另一个问题是如何在GC发生时让所有线程(不包括执行JNI调用的线程)都到最近的安全点上再停顿下来。

有两种方式:抢先式中断和主动式中断。

抢先式中断即不需要线程的先执行代码主动配合,而是在GC发生时,先全部中断,然后发现有线程中断的不在安全点上,就恢复线程,让其跑到安全点上再停顿。(几乎没用这种方式。)

主动式中断: 当GC需要中断时,不直接对线程操作,仅仅简单设置一个标志,各线程主动去轮询这个标志,发现中断标志时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

3.安全区域

安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的安全点。但是对于不执行(即没有分配CPU时间)的程序,如线程处于sleep或Blocked状态,线程就无法响应JVM的中断请求,走到安全点去挂起。JVM也不太可能等待线程重新被分配CPU时间。这时就需要安全区域来解决了。

安全区域即在一段代码片段中,引用关系不会发生变化。在这个区域任意地方开始GC都是安全的。可以看作是被扩展了的安全点。

线程到达安全区域时,先标识自己进入安全区域。当这段时间内JVM要发起GC时,就不用管标识安全区域状态的线程了。当线程要离开安全区域时,先检查系统是否完成了根结点枚举或整个GC过程,完成了就继续执行,没有就等待直到收到可以安全离开安全区域的信号为止。
 

内存分配担保机制

  • 我们知道如果对象在复制到Survivor区时若Survivor空间不足,则会出发担保机制,将对象转入老年代;但老年代的能力也不是无限的,因此需要在minor GC时做一个是否需要Major GC 的判断:

  • 如果老年代的剩余空间 < 之前转入老年代的对象的平均大小,则触发Major GC

  • 如果老年代的剩余空间 > 之前转入老年代的对象的平均大小,并且允许担保失败,则直接Minor GC,不需要做Full GC

  • 如果老年代的剩余空间 > 之前转入老年代的对象的平均大小,并且不允许担保失败,则触发Major GC

    出发点还是尽量为对象分配内存。但是一般会配置允许担保失败,避免频繁的去做Full GC。

标记—整理算法

标记整理算法的标记过程类似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代)。无内存碎片

新生代、老年代、永久代

  • 新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的. 如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。注意,新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。
  • 老年代存放的都是一些生命周期较长的对象,就像上面所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中
  • 永久代主要用于存放静态文件,如Java类、方法等

垃圾收集器

JVM简述

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • ParNew收集器 (复制算法):新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 =用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器 (标记-整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

内存泄露问题

  • 静态集合类: 如 HashMap、Vector 等集合类的静态使用最容易出现内存泄露,因为这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放
  • 各种资源连接包括数据库连接、网络连接、IO连接等没有显式调用close关闭
  • 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

 

jvm的常见参数 

-Xmx3550m:设置JVM最大堆内存为3550M。

-Xms3550m:设置JVM初始堆内存为3550M

            -Xmn2g:设置年轻代大小为2G。

-XX:NewRatio=4:设置年轻代(包括1个Eden和2个Survivor区)与年老代的比值。表示年轻代比年老代为1:4。

-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的比值。表示2个Survivor区(JVM堆内存年轻代中默认有2个大小相等的Survivor区)与1个Eden区的比值为2:4,即1个Survivor区占整个年轻代大小的1/6。

jvm的监控及调优

jvm的监控与调优一方面依赖第三方工具,但更多的还是利用jdk中自带的一些工具

包括jmap,jstack,jconsole等

           gc日志输出

       在jvm启动参数中加入 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimestamps -XX:+PrintGCApplicationStopedTime,jvm将会按照这些参数顺序输出gc概要信息,详细信息,gc时间信息,gc造成的应用暂停时间。如果在刚才的参数后面加入参数 -Xloggc:文件路径,gc信息将会输出到指定的文件中