0%

理解JVM概念

《深入理解JAVA虚拟机》部分概念整理,内存结构、垃圾收集、内存分配。

JVM内存区域

运行时数据区

  1. 方法区:线程共享,存储已被虚拟机加载的类信息、常量、静态变量
  2. 堆:线程共享,虚拟机内存中最大的一块,存放对象实例
  3. 虚拟机栈:线程私有,生命周期与线程相同,描述Java方法执行的内存模型。每个方法在执行的同时会创建一个栈帧,存储:局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成,对应着一个栈帧在虚拟机中入栈-出栈的过程
  4. 本地方法栈:线程私有、与本地方法栈相似,只是描述的是Native方法的执行过程
  5. 程序计数器:线程私有,记录当前线程执行代码执行到哪里了。可以看做是线程所执行字节码行号指示器

内存溢出和栈溢出

  1. 堆溢出:修改参数-Xmx限制最大堆内存,循环创建对象,导致OOM。java.lang.OutOfMemeryError:Java heap space

  2. 虚拟机栈/本地方法栈溢出: 有两种情况

    2.1 修改参数-Xss减少每个栈内存的容量,递归调用方法,线程栈深度过大导致栈溢出。导致StackOverflowError

    2.2 修改参数-Xss增大每个栈内存的容量,循环创建子线程调用方法,会导致没有足够的内存再创建新线程。报OutOfMemeryError:unable to create new native thread

    因此要根据实际情况设置-Xss参数,过小导致栈深度小、过大导致可运行的线程总数小

  3. 方法区和运行时常量池溢出:运行时常量池属于方法区一部分。修改参数-XXPermSize限制永久代(JDK8中是元空间)大小,循环调用String.intern()将不同的字符串存入运行时常量池,会导致OutOfMemeryError:PermGen space

  4. 直接内存溢出:指定参数-XX:MaxDirectMemorySize(如果不设置默认与-Xmx一样),直接循环中通过Unsafe申请内存导致内存溢出。导致OutOfMemeryError,如果没有看到明显的OOM异常原因,OOM之后Dump文件比较小,考虑是直接内存溢出

垃圾收集

垃圾收集需要关注三点:

  1. 哪些内存需要回收
  2. 什么时候需要回收
  3. 如何回收

判断对象是否可以回收

判断对象是“存活”还是“死去”(不能再被任何地方使用)

引用计数法

给对象添加一个引用计数器:有一个地方引用它时,计数器+1,引用失效时计数器-1,如果计数器==0说明对象不能在被使用了,可以被回收掉。

优点:效率高。

缺点:无法解决循环引用的问题,即A对象中属性引用B,B对象中属性引用A,AB之外再无其他引用,这样的对象计数器始终不为0,永远不会被回收掉

可达性分析

主流语言(Java、C#)用可达性分析算法来判定对象是否存活。通一系列被称为“GC Roots”的对象作为起始节点,从这些节点向下搜索,当一个对象到GC Roots中没有任何连接时,认为是不可达的,可以被回收掉。

Java中GC Roots对象有以下几种:

  1. 虚拟机栈中的变量引用对象
  2. 方法区中静态变量引用对象
  3. 方法区中final常量引用对象
  4. 本地方法栈中引用对象

Java中四种引用

  1. 强引用:直接=引用的对象
  2. 软引用:在将要发生内存溢出异常值前,把软引用对象进行二次回收(内存不足时才回收)
  3. 弱引用:只要发生垃圾回收,就会被回收掉
  4. 虚引用:最弱的引用关系,甚至无法通过虚引用获得这个对象实例。只是为了在对象被回收时通过ReferenceQueue来处理一些事情

垃圾收集算法

标记-清除算法

在一块连续的内存区域中先标记所有需要回收的对象、标记完成后统一清除掉。

优点:简单

缺点:标记、清除两个动作效率都不高。并且会产生大量不连续的内存碎片,如果再分配较大的对象时可能无法找到足够的连续的内存区域

复制算法

将内存区域分为2块,每次用其中的1块。回收时将区域内存活的对象复制到另一块内存区域中,然后整个清除原来的内存区域。

优点:简单、效率高,无内存碎片。 缺点:运行时内存只能应用一半

新生代垃圾回收时,剩下存活对象比较少,适合用复制算法来回收。实际使用时不是将内存划分为两部分,而是分为一块较大的Eden区和两块较小的Survivor区。 HostSpot虚拟机中默认E : S1 : S2内存大小为8 : 1 : 1。每次只使用E、S1两块内存,垃圾回收时,将E、S1中存活的对象复制到S2上,清掉E、S1内存,然后继续使用E、S2。 这样内存只浪费了10%

标记-整理算法

标记存活的对象,然后将存活的对象都向内存一端移动,然后清除掉边界外的内存。

老年代垃圾回收时,由于可能存活的对象比较多,不适合复制算法,所以采用这种回收算法

分代收集算法

主流商用虚拟机垃圾收集器都采用分代收集的算法,将内存分为新生代、老年代。 新生代用“复制算法”回收,老年代用“标记-清除”/“标记-整理算法“回收

内存分配与回收策略

  1. 新内存直接分配到新生代Eden区中。如果Eden区中没有足够的空间,虚拟机将发起一次MinorGC

  2. 大对象直接进入老年代。例如很长的字符串及数组,对象内存超过一个临界值时将直接分配到老年代

  3. 长期存活的对象进入老年代。新生代对象每次MinorGC时Age+1,超过某个值后将进入到老年代

  4. 动态年龄判定。 实际并不一定按照年龄临界值来进入老年代,如果新生代S区内相同年龄对象占用空间大于S区一半,将>=该Age的对象直接进入老年代。

  5. 空间分配担保。新生代MinorGC发生之前,检查一下老年代连续可用内存空间是否大于新生代所有对象总空间(极端情况新生代对象都存活,S区无法容纳的对象直接进入老年代。要保证老年代有足够的空间)

    5.1 如果空间足够,空间分配担保成功,则正常进行MinorGC

    5.2 如果空间不够,空间分配担保失败。

    ​ 如果允许失败(HandlePromotionFailure=true):检查老年代连续可用空间是否大于历次晋升到老年代对象平均大小。如果大于则进行MinorGC,如果小于或者MinorGC失败,则进行FullGC