JAVA虚拟机

561 阅读8分钟

Java内存区域

  1. 程序计数器:当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
  2. 虚拟机栈:指我们平常所说的堆栈中的栈(或者说是虚拟机栈中的局部变量部分),线程私有的,生命周期与线程相同。用来描述Java方法执行的内存模型,用于存储局部变量表、操作栈、动态链接、方法出口等信息
  3. 本地方法栈:跟虚拟机栈类似,不过本地方法栈是为虚拟机使用到的Native方法服务
  4. 堆:被所有线程共享的一块内存区域,用来存放对象实例以及对象类型数据的地址信息,还有字符串常量池(JDK7.0后放入堆中,JDK6.0及之前的版本在方法区中),如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
  5. 方法区:跟堆一样,是各个线程共享的内存区域,用来存储类信息(对象类型、父类、实现的接口、方法等)、静态变量,其中方法区还包含一个运行时常量池,用来存储编译期生成的各种字面量和符号引用

垃圾收集器及内存分配策略

对象存活判断

垃圾回收主要是回收堆内存。在垃圾回收期(GC)回收之前,需要确定哪些对象可以回收,有以下几种方法:

  1. 引用计数算法
    原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。这种算法效率高。不过很难解决对象之间的相互循环引用的问题。
  2. 根搜索算法(默认)
    原理:通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。作为GC Roots的对象包括以下几种:
    • 虚拟机栈中的引用的对象
    • 方法区中的类静态属性引用的对象
    • 方法区中的常量引用的对象
    • 本地方法栈中JNI的引用的对象

引用

  • 强引用,类似"Object obj = new Object()"这种,只要强引用存在,则GC永远不会回收被引用的对象
  • 软引用,指还有用,但是并非必须的对象,内存溢出之前进行回收,实现软引用可以通过SoftReference类,软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
  • 弱引用,跟软引用一样,不过强度比软引用弱一些,第二次垃圾回收时回收,实现弱引用可以通过WeakReference类,弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
  • 虚引用,是最弱的一种引用关系,垃圾回收时回收,无法通过引用取到对象值。主要用于检测对象是否已经从内存中删除。

垃圾收集算法

  1. 标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,不过该算法有以下缺点:
    • 效率低
    • 空间问题,该算法会产生大量不连续的内存碎片,这样导致程序在以后的运行中如果需要分配较大对象时无法找到足够的连续内存而触发另一次垃圾收集动作
  2. 复制算法:将可用内存按容量划分大小相等的两块,每次只使用其中的一块。当一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法实现简单,效率高,不过会将可使用的内存减少一半。如果对象存活率高就要执行较多的复制操作,将导致效率变低。目前在复制算法中,通常是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性拷贝到另外一块Survivor中,最后清理使用的Eden和Survivor。并且以老年代作为空间分配担保,即Survivor无法容纳的对象会直接进入老年代。目前新生代主要采用这个算法。
  3. 标记-整理算法:将所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
  4. 分代收集算法:根据对象的存活周期的不同将内存划分为几块,一般是分为新生代和老年代。然后根据各个年代的特点采用最适当的收集算法。新生代通常采用复制算法,因为对象生存时间都不长。老年代一般采用"标记-清理"或者"标记-整理"算法回收,因为老年代中对象存活率高,没有额外空间对它进行分配担保。

垃圾收集器

  1. Serial收集器:这是一个单线程的收集器,该收集器在进行垃圾收集时,必须暂停其它所有的工作线程,不过该收集器简单而高效。它一般运行在Client模式下的虚拟机。新生代收集器。
  2. ParNew收集器:这是一个多线程版本的Serial收集器。新生代收集器。
  3. Parallel Scavenge收集器:新生代收集器,使用复制算法,并行多线程。它主要是控制吞吐量=(运行用户代码时间)/(运行用户代码时间+垃圾收集时间)。有两个参数可以用来精确控制吞吐量。
    • -XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间
    • -XX:GCTimeRatio:设置吞吐量大小
    • -XX:+UseAdaptiveSizePolicy:开关参数,打开以后就由虚拟机自动调节策略。这也是跟ParNew收集器的一个重要区别
  4. Serial Old收集器:这是Serial收集器的老年代版本,使用"标记-整理"算法,该收集器有两大用途,一是与Parallel Scavenge收集器搭配使用。二是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。
  5. Parallel Old收集器:是Parallel Scavenge收集器的老年代版本。使用"标记-整理"算法。
  6. CMS收集器:以获取最短回收停顿时间为目标的收集器。重视服务的响应速度。主要用于互联网或B/S系统的服务端上。基于"标记-清除"算法。老年代收集器。使用CMS收集器时,不能像其它收集器那样等到老年代几乎被填满了再进行收集,需要预留一部分空间提供并发收集时的程勋运行使用。主要优点就是并发收集、低停顿。它有以下缺点:
    • 对CPU资源非常敏感
    • 无法处理浮动垃圾。即在垃圾收集阶段用户线程继续进行,这个过程会有新的垃圾产生,但是CMS收集器不会对这部分垃圾(浮动垃圾)进行标记。如果预留的内存空间不足,就会导致Concurrent Mode Failure。这时会临时启动Serial Old收集器重新进行老年代的垃圾收集,导致停顿时间过长。
    • 由于采用的算法,会导致收集结束时产生大量空间碎片。
  7. G1收集器:基于"标记-整理"算法实现。可以非常精确的控制停顿,并且在不牺牲吞吐量的前提下完成低停顿的内存回收。因为该收集器会将整个java堆划分为多个大小固定的独立区域,并且跟踪这些区域的垃圾堆积程度,每次根据允许的收集时间,优先回收垃圾最多的区域。 垃圾收集相关的常用参数参见下图:

内存分配

  1. 对象首先在新生代Eden区分配,当Eden没有足够的空间进行分配时,虚拟机将发起一次MinorGC
  2. 大对象直接进入老年代,通过-XX:PretenureSizeThreshold参数区分,大于这个值的表示大对象,该参数只对Serial和ParNew有效
  3. 长期存活的对象将进入老年代,识别对象长期存活通过参数-XX:MaxTenuringThreshold设定,对于每个对象而言,虚拟机都会定义一个年龄计数器,如果对象在经过第一次MinorGC后仍然存活,并且能够被Survivor容纳的话,就将被移动到Survivor中,并且年龄增加一岁。每经过一次MinorGC,年龄就增加一岁,超过设定值或者默认值(15),就会进入老年代中。

名词解释

  1. MinorGC:指发生在新生代的垃圾收集动作,频繁,速度快
  2. MajorGC/Full GC:指发生在老年代的GC,速度慢
  3. 大对象:需要大量连续内存空间的Java对象,比如很长的字符串及数组,应当避免短命的大对象