Java面试-JVM-基础


java文件编译成二进制字节码class文件, class文件再通过类加载器加载到JVM的永久代(jdk8以后就变成了Metaspace元数据,这里说一下,永久代跟元数据都是实现方法区的手段,方法区是一种规范). 应用启动时,通过方法区中类的元信息,静态变量,静态方法(想当于制造对象的说明书)创建对象。

  1. Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),
  2. JVM中的类加载器加载各个类的字节码文件。
  3. 加载完毕之后,交由JVM执行引擎执行。
    • 在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)

类加载器

  • Java 的类加载过程可以分为 5 个阶段:载入、验证、准备、解析和初始化
    1. Loading(载入): JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象。
    2. Verification(验证): JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。下面是一些主要的检查。
      • 确保二进制字节流格式符合预期(比如说是否以 cafe bene 开头)。
      • 是否所有方法都遵守访问控制关键字的限定。
      • 方法调用的参数个数和类型是否正确。
      • 确保变量在使用之前被正确初始化了。
      • 检查变量是否被赋予恰当类型的值。
    3. Preparation(准备): JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。
    4. Resolution(解析): 该阶段将常量池中的符号引用转化为直接引用。在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 com.Wanger 类引用了 com.Chenmo 类,编译时 Wanger 类并不知道 Chenmo 类的实际内存地址,因此只能使用符号 com.Chenmo。
    5. Initialization(初始化): 是执行类构造器<clinit>()方法。这个方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static {}块)中的语句合并产生的。clinit是一个由编译器自动生成的类构造器方法,用于初始化类变量和静态语句块
  • Java 类加载器:
    1. 启动类加载器(Bootstrap Class-Loader): 用来加载java核心类库。主要负责加载JAVA_HOME/lib下的类库。启动类加载器无法被应用程序直接使用。
    2. 扩展类加载器(Extension or Ext Class-Loader): 用来加载Java的扩展库。。加载 jre/lib/ext 包下面的 jar 文件。
    3. 应用类加载器(Application or App Clas-Loader): 它根据Java应用的类路径(classpath)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。
    4. 用户自定义类加载器: 通过继承 java.lang.ClassLoader 类的方式实现。
  • 类初始化顺序(属性和代码块级别相同,按前后顺序执行): 静态内部类和非静态内部类一样,都不会因为外部类的加载而加载,同时静态内部类的加载不需要依附外部类,在使用时才加载,不过在加载静态内部类的过程中也会加载外部类
    1. 父类静态属性
    2. 父类静态代码块
    3. 子类静态代码块
    4. 子类静态属性
    5. 父类普通属性
    6. 父类构造方法
    7. 子类普通属性
    8. 子类构造方法
  • 双亲委派机制:如果一个类加载器收到了类加载的请求,这个类加载器不会先尝试加载这个类,而是会先把这个请求委派给自己的父类加载器去完成,在每个层次的类加载器都是依此类推的。因此所有的类加载器请求其最后都应该被委派到顶层的启动类加载器中(Bootstrap),只有当父类的加载器在自己管辖范围内没有找到所需要的类的时候,子类加载器才会尝试自己去加载的,如果自己管理范围内也找不到需要加载的就会抛出:ClassNotFoundException 这个异常了。
  • 双亲委派的作用
    1. 防止加载同一个.class。通过委托去询问上级是否已经加载过该.class,如果加载过了,则不需要重新加载。保证了数据安全。
    2. 保证核心.class不被篡改。通过委托的方式,保证核心.class不被篡改,即使被篡改也不会被加载,即使被加载也不会是同一个class对象,因为不同的加载器加载同一个.class也不是同一个Class对象。这样则保证了Class的执行安全。
      • 如果没有双亲委派机制,同一个类可能就会被多个类加载器加载,如此类就可能会被识别为两个不同的类,相互赋值时问题就会出现。
      • 双亲委派机制能够保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。

JVM 内存(堆、栈、方法区)

  • 程序计数器: 可以看作是当前线程所执行的字节码的行号指示器。线程私有
  • 堆区: 堆内存用于存放由new创建的对象和数组。堆被所有线程共享。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
  • 栈区: Java栈是一块线程私有的空间,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆中
    • 每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame), Java虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)。每一个方法被调用直至执行完毕的过程,就对应这一个栈帧在虚拟机栈中从入栈到出栈的过程。
    • 虚拟机栈大小的调整
      • Java虚拟机规范允许虚拟机栈的大小固定不变或者动态扩展。
      • 固定情况下:如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,则抛出StackOverflowError异常;
      • 可以通过 java -Xss<size> 设置 Java 线程堆栈大小,或者在idea中 help -> edit vm option中改变大小
    • 运行时栈帧结构(每个栈帧包含5个组成部分): 局部变量表、操作数栈(Operand Stack)、动态链接(Dynamic Linking)、方法返回地址(Return Address)和一些附加信息 (异常处理表)
      • 局部变量表:一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
      • 操作数栈:主要保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间。
      • 帧数据区:除了局部变量表和操作数据栈以外,栈还需要一些数据来支持常量池的解析,这里帧数据区保存着访问常量池的指针,方便计程序访问常量池,另外当函数返回或出现异常时虚拟机必须有一个异常处理表,方便发送异常的时候找到异常的代码,因此异常处理表也是帧数据区的一部分。
  • 本地方法栈:本地方法栈则是为了执行native方法所服务的.只是这个栈是属于本地的,不属于JAVA虚拟机,但受JAVA虚拟机的控制.调用一个C函数使用。
  • 方法区:Java方法区和堆一样,方法区是一块所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量
    • 在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代。
    • 元空间不在虚拟机设置的内存中,而是使用本地内存。类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。

JVM 参数调优

进行虚拟机参数配置,其实就是围绕着堆、栈、方法区进行配置,而最多的就是关于堆内存中新生代和老年代的参数配置,不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特点做出合理的配置,基本策略:尽可能将对象预留在新生代,减少老年代的GC次数。除了可以设置新生代的绝对大小(-Xmn),可以使用(-XX:NewRatio)设置新生代和老年代的比例:-XX:NewRatio=老年代/新生代

  • 堆的参数配置
    • -XX:+PrintGC 每次触发GC的时候打印相关日志
    • -XX:+PrintGCDetails 更详细的GC日志
    • -Xms 堆初始值
    • -Xmx 堆最大可用值
    • -Xmn 新生代堆最大可用值
    • -XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.
  • 设置新生代与老年代优化参数
    • -Xmn 新生代大小,一般设为整个堆的1/3到1/4左右
    • -XX:SurvivorRatio 设置新生代中eden区和from/to空间的比例关系n/1
    • -Xms20m -Xmx20m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
    • -Xms20m -Xmx20m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
    • -XX:NewRatio=2
  • 内存溢出解决办法
    • 设置堆内存大小
      • 错误原因: java.lang.OutOfMemoryError: Java heap space
      • 解决办法:设置堆内存大小 -Xms1m -Xmx70m -XX:+HeapDumpOnOutOfMemoryError
    • 设置栈内存大小
      • 错误原因: java.lang.StackOverflowError. 栈溢出, 产生于递归调用,循环遍历是不会的,但是循环方法里面产生递归调用, 也会发生栈溢出。
      • 解决办法:设置线程最大调用深度, -Xss5m 设置最大调用深度

垃圾回收 GC

  • 分为2种,一是Minor GC,可以可以称为YGC,即年轻代GC. 还有一种称为Major GC,又称为FullGC
    • Minor GC触发条件:JVM 无法为一个新对象分配内存空间时
    • Full GC触发条件:
      • 调用System.gc
      • 老年代空间不足
      • 方法区空间不足
      • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
        • 这是一种避免在新生代GC时发生promotion failed的机制。promotion failed是指在新生代GC时,有些对象无法放入Survivor区,需要晋升到老年代,但是老年代空间不足,导致GC失败。为了防止这种情况,JVM会在每次新生代GC前检查老年代的可用空间是否大于新生代的所有对象大小,如果不是,就会再检查老年代的可用空间是否大于历次新生代GC后晋升到老年代的对象的平均大小,如果还不是,就会触发Full GC,回收老年代的垃圾对象,为新生代GC后的对象晋升腾出空间。
  • JVM内存分配担保原则:在JDK1.5以及之前版本中默认是关闭的,需要通过HandlePromotionFailure手动指定,JDK1.6之后就默认开启。
    • 内存分配:在JVM在内存分配的时候,新生代内存不足时,eden space的对象(6MB)又无法全部放入Survivor空间。把新生代的存活的对象搬到老生代,然后新生代腾出来的空间用于为分配给最新的对象。这里老年代是担保人。在不同的GC机制下,也就是不同垃圾回收器组合下,担保机制也略有不同。
      • Serial+Serial Old的情况下,发现放不下就直接启动担保机制;
      • Parallel Scavenge+Serial Old的情况下,却是先要去判断一下要分配的内存是不是>=Eden区大小的一半,如果是那么直接把该对象放入老生代,否则才会启动担保机制。
    • 要在新生代实行Minor GC的时候,首先要检查老年代中连续的最大的内存空间是否大于新生代中所有的对象的大小
      • 如果大于,那么进行Minor GC
      • 如果小于,继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
        • 如果大于,将尝试者进行一次Minor GC,尽管这次Minor GC是有风险的。
        • 如果小于
          • 如果HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC.
          • 否则老年代无法装下,垃圾收集器进行一次预测:根据以往minorGC过后存活对象的平均数来预测这次minorGC后存活对象的平均数。
            • 以往平均数小于当前老年代最大的连续空间,就进行minorGC
            • 大于,则进行一次fullGC,通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代做担保。
  • 内存分配原则
    • 对象优先在 Eden 分配
    • 大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起 Minor GC
    • 大对象直接进入老年代
    • 长期存活的对象将进入老年代
    • 虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Survivor 区中每熬过一次 Minor GC,年龄加 1 岁,当年龄到一定程度(默认15岁),就会晋升到老年代中。
    • 动态对象年龄判定: 在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
  • 垃圾收集算法
    • 标记-清除算法(Mark-Sweep): 这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
      • 优点:实现起来比较容易
      • 缺点:有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
    • 复制算法(Copying):为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量**划分为大小相等的两块,每次只使用其中的一块。这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题
      • 优点:实现简单,运行高效且不容易产生内存碎片
      • 缺点:对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低
    • 标记-整理算法(Mark-Compact)(压缩法):为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
    • 分代收集算法(Generational Collection):不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
      • 分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法
      • 目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将EdenSurvivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
      • 而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)
  • 分代的垃圾回收策略: 是基于这样一个事实, 不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
    • 年轻代(Young Generation)
      1. 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
      2. 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor 区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
      3. survivor1区不足以存放 edensurvivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
      4. 新生代发生的GC也叫做Minor GCMinorGC发生频率比较高(不一定等Eden区满了才触发)
    • 年老代(Old Generation)
      1. 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
      2. 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GCFull GCFull GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
    • 持久代(Permanent Generation): 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类
  • Java没有采用引用计数法是因为无法解决循环引用的问题,Python采用的是引用计数法
  • 可达性分析:通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。可作为GC Roots的对象包含以下几种:
    • 虚拟机栈中的引用的对象,我们在程序中正常创建一个对象
    • 我们在类中定义了全局的静态的对象,也就是使用了static关键字
    • 常量引用,就是使用了static final关键字
  • 三色标记法
    • 我们把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色:
      • 白色:尚未被GC访问过的对象,如果全部标记已完成依旧为白色的,称为不可达对象,既垃圾对象。
      • 灰色:本对象已访问过,但是本对象的子引用对象还没有被访问过,全部访问完会变成黑色,属于中间态。
      • 黑色:本对象已经被GC访问过,且本对象的子引用对象也已经被访问过了。
    • 标记过程:
      1. GC并发标记刚开始时,所以对象均为白色集合。
      2. 将所有GCRoots直接引用的对象标记为灰色集合。
      3. 判断若灰色集合中的对象不存在子引用,则将其放入黑色集合,若存在子引用对象,则将其所有的子引用对象放入灰色集合,当前对象放入黑色集合
      4. 按照步骤3,以此类推,直至灰色集合中的所有对象变成黑色后,本轮标记完成,且当前白色集合内的对象称为不可达对象,既垃圾对象。
    • 问题:由于此过程是在和用户线程并发运行的情况下,对象的引用处于随时可变的情况下,那么就会造成多标和漏标的问题。
      • 浮动垃圾:本应该被标记为白色的对象,没有被标记,造成该对象可能不会被回收。
      • 漏标:灰色对象指向白色对象的引用消失了,然后一个黑色的对象重新引用了白色对象。不难分析,漏标只有同时满足以下两个条件时才会发生:
        • 条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。
        • 条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。
      • 解决方案
        • CMS:Incremental Update算法:当一个白色对象被一个黑色对象引用,将黑色对象重新标记为灰色,让垃圾回收器重新扫描。(破坏条件二)
        • G1:SATB(Snapshot At The Beginning)算法:当原来成员变量的引用发生变化之前,记录下原来的引用对象,既原始快照,当B和C之间的引用马上被断掉时,将这个引用记录下来,使GC依旧能够访问到,那样白色就不会漏标。(破坏条件一)
  • JVM 判断对象是否存活:可达性分析 -> 第一次标记 -> 第二次标记
    • 标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
    • 第一次标记并进行一次筛选:筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。
    • 第二次标记:如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。
      • Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己, 只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。
  • 直接内存的回收:直接内存(Direct Memory)通常指的是在Java程序中,通过Native方法分配的内存区域,它并不属于Java堆栈管理的范畴。最常见的使用直接内存的情况是通过Java的ByteBuffer类的allocateDirect()方法来创建直接缓冲区,主要用于IO操作,因为它可以被操作系统的本地IO操作直接使用,避免了在Java堆和原生堆之间复制数据的开销。垃圾回收器(GC)管理的是堆内存,而直接内存的分配和释放通常由操作系统和Java虚拟机(JVM)的直接内存管理来负责,不受Java垃圾回收器的直接控制。也就是说,直接内存的生命周期并不是由GC决定的,GC一般不会直接回收直接内存。然而,Java为直接内存提供了一种间接回收的机制。
    • ByteBuffer对象本身是作为Java堆上的一个对象存在的,当这个对象不再有任何引用,并且成为垃圾回收的目标时,GC会回收这个ByteBuffer对象。在ByteBuffer对象被GC回收的同时,如果它是一个直接缓冲区,JVM会在其垃圾收集过程中安排回收和清理与之关联的直接内存。这种回收是通过ByteBuffer对象的finalize()方法实现的,其中会调用本地方法来释放直接内存。
    • 因为直接内存的回收依赖于ByteBuffer对象的垃圾回收,而GC运行是不可预测的,所以直接内存的释放可能会延迟。如果直接内存的分配量很大,或者需要即时释放资源,依赖GC来回收可能会导致内存不足或其他问题。在这些情况下,可以通过手动方式调用System.gc()来提示垃圾收集器执行,或更好的做法是通过调用DirectByteBuffercleaner()方法来清理直接内存。不过,cleaner()方法并不是官方公开支持的API,也会随着Java版本而变化。
  • 引用的分类
    • 强引用(=):设为 null 才会被 GC
    • 软引用(SoftReference):内存溢出之前进行回收,用于高速缓存
    • 弱引用(WeakReference):如果一个对象只剩下弱引用,那么这个对象将会被垃圾回收器回收。主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾和缓存机制,比如 ThreadLocal
    • 虚引用(PhantomReference):虚引用是每次垃圾回收的时候都会被回收;无法通过引用取到对象值(PhantomReference.get()方法总是返回null);主要作用是用来跟踪对象被垃圾回收的活动,当垃圾回收器决定回收一个对象之前,如果该对象有虚引用,则会把这个虚引用添加到与之关联的引用队中,当程序发现某个虚引用已经加入到了引用队列,那么就可以在对象被回收之前采取必要的行动,比如资源清理工作或者是性能监控等
      • new PhantomReference<>(myObject, referenceQueue)
      • if (referenceQueue.poll() != null) {}
  • 垃圾回收器:上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用
    • 串行收集器:用于并行能力较弱的计算机,它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)
      • Serial:复制算法
      • Serial Old:标记-整理算法
    • 并行收集器:注重吞吐量以及CPU资源敏感的场合
      • ParNewSerial 多线程版本;-XX:+UseParNewGC,新生代并行(ParNew),老年代串行(Serial Old
      • Parallel Scavenge-XX:+UseParallelGC,新生代使用并行回收收集器,老年代使用串行收集器
      • Parallel OldParallel Scavenge的老年代版本;-XX:+UseParallelOldGC 新生代和老年代都使用并行回收收集器;多线程标记-整理算法
    • CMS收集器:以获取最短回收停顿时间为目标的收集器,重视服务的响应速度,使用标记-清除算法;内存回收过程是与用户线程一起并发执行
      • 执行流程
        • 初始标记(CMS initial mark):Stop The World,只是标记一下GC Roots能直接关联到的对象,速度很快
        • 并发标记(CMS concurrent mark):从GC Roots开始找到它能引用的所有其它对象
          • 本来可达的对象,变得不可达了:浮动垃圾是可容忍的问题
          • 本来不可达的对象,变得可达了:进行重新标记
        • 重新标记(CMS remark):Stop The World,修正并发标记期间因用户程序继续动作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些
        • 并发清除(CMS concurrent sweep)
      • 参数设置:-XX:+UseConcMarkSweepGC
        • -XX:CMSlnitiatingOccupanyFraction : 设置堆内存使用率的阀值,一旦达到该阈值,便开始进行回收。如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。
        • -XX:+UseCMSCompactAtFullCollection : 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行 ,所带来的问题就是停顿时间变得更长了
        • -XX:CMSFullGCsBeforeCompaction : 设置在执行多少次Full GC后对内存空间进行压缩整理
      • 缺点
        • 内存碎片过多,导致启动单线程serial垃圾回收器:内存碎片整理 -XX:CMSFullGCsBeforeCompaction, -XX:+UseCMSCompactAtFullCollection
        • 浮动垃圾,并发回收垃圾的同时产生新的垃圾:降低阈值 -XX:CMSlnitiatingOccupanyFraction
          • 需要预留一些空间用来保存用户新创建的对象,在JDK1.5之前老年带使用了68%空间后就会激活CMS收集,JDK6 CMS收集器的启动阀值就已经默认提升到92%
    • G1 (Garbage-First)收集器:面向服务端应用
      • 优点
        • 并行与并发:充分利用多CPU、多核环境下的硬件优势
        • 分代收集:不需要其他收集器配合就能独立管理整个GC堆
        • 空间整合:“标记—整理”算法实现的收集器,局部上基于“复制”算法不会产生内存空间碎片
        • 可预测的停顿:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
      • 执行流程
        • 初始标记:标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短
        • 并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
        • 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
        • 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
      • 软实时:用户可以指定垃圾回收时间的限时,G1会努力在这个时限内完成垃圾回收,但是G1并不担保每次都能在这个时限内完成垃圾回收。通过设定一个合理的目标,可以让达到90%以上的垃圾回收时间都在这个时限内
      • 分区regionG1将内存划分一个个固定大小的region,每个region可以是年轻代、老年代的一个。内存的回收是以region作为基本单位的
        • G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可
        • 每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换
        • -XX:G1HeapRegionSize=n : 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
      • 卡片Card : 在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度
        • 所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片
          • 如果一个老年代 CardTable 中有对象指向新生代, 就将它设为 Dirty(标志位1), 下次扫描时,只需要扫描 CardTable 上是 Dirty 的内存区域即可
        • 当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)
        • 每次对内存的回收,都是对指定分区的卡片进行处理
      • 已记忆集合Remember Set (RSet): 为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet)
        • 内部类似一个反向指针,记录引用分区内对象的卡片索引
        • 当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况.
        • 每个 Region 都有,非常消耗空间
          • 记录了其他 Region 中的对象到本 Region 的引用, 在一个 Region 区里面
          • 每一个 Region 都需要一个 RSet 的内存区域,导致有 G1RSet 可能会占据整个堆容量的 20%乃至更多

GC


OOM

  • GC 日志
    • GC表示这次发生的是 Minor GC
    • Full GC表示这次GC是一次Major GC
    • Allocation Failure 表示导致这次GC的原因是内存分配失败
    • DefNew 新生代内存情况
  • Jstack: 能得到运行java程序的java stack和native stack的信息。可以轻松得知当前线程的运行情况。
    • jstack -l pid > jstack.log : 输出这一时刻的线程栈
  • jmap: 查看一下应用中具体存在了哪些对象,以及其实例数和所占大小
    • jmap -heap pid : 显示Java堆详细信息,打印一个堆的摘要信息,包括使用的GC算法、堆配置信息和各内存区域内存使用信息
    • jmap -histo:live pid : 显示堆中对象的统计信息。包括每个Java类、对象数量、内存大小(单位:字节)、完全限定的类名
    • jmap -dump:format=b,file=heap.dump pid : dump 内存
  • 堆内存快照:-XX:+HeapDumpOnOutOfMemoryError -> java_pid.hprof (-XX:HeapDumpPath= 可以改变存储路径)
    • 分析工具:MAT(离线分析), jvisualVM(实时分析;离线分析)
  • 异常种类
    • java.lang.OutOfMemoryError: Java heap space :java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改
    • java.lang.OutOfMemoryError: PermGen spac : >java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出
    • •java.lang.StackOverflowErr :JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。不会抛 OOM error,但也是比较常见的Java内存溢出
  • 频繁 Full GC:一般是新生代空间太小,会导致一些本应该可以很快就被回收的对象被放到了老生代
  • 问题定位流程:
    • 使用jps查看线程ID
    • 使用jstat -gc 3331 250 20 查看gc情况,一般比较关注PERM区的情况,查看GC的增长情况
    • 使用jstat -gccause:额外输出上次GC原因
    • 使用jmap -dump:format=b,file=heapDump 3331生成堆转储文件
    • 使用jhat或者可视化工具(MAT 等)分析堆情况
    • 结合代码解决内存溢出或泄露问题
  • 内存溢出原因
    • 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
    • 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
    • 代码中存在死循环或循环产生过多重复的对象实体
    • 启动参数内存值设定的过小
  • 内存溢出的解决方案
    • 修改JVM启动参数,直接增加内存(-Xms, -Xmx参数)
    • 检查错误日志,查看OutOfMemory错误前是否有其它异常或错误
    • 对代码进行走查和分析,找出可能发生内存溢出的位置
  • 重点排查的点
    • 检查对数据库查询中,是否有一次获得全部数据的查询,对于数据库查询尽量采用分页的方式查询
    • 检查代码中是否有死循环或递归调用
    • 检查是否有大循环重复产生新对象实体
    • 检查List, MAP等集合对象是否有使用完后,未清除的问题
    • 使用内存查看工具动态查看内存使用情况

文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录