Java内存管理及垃圾回收机制

Contents

  1. 1. java内存管理
    1. 1.1. JDK,JRE,JVM的区别
    2. 1.2. java内存分区
      1. 1.2.1. Method Area (方法区)
      2. 1.2.2. Heap (堆)
      3. 1.2.3. JVM Stack(虚拟机栈)
      4. 1.2.4. Native Method Stack (本地方法栈)
      5. 1.2.5. PC Register(程序计数器)
  2. 2. java垃圾回收机制(Garbage Collection,GC)
    1. 2.1. 为什么要进行垃圾回收?
    2. 2.2. 哪些垃圾需要回收?
    3. 2.3. 什么时候进行垃圾回收?
    4. 2.4. 如何进行垃圾回收?
      1. 2.4.1. 基于分代:新生代、老年代、持久代
      2. 2.4.2. 常见的GC算法:
      3. 2.4.3. 垃圾回收器的类型
      4. 2.4.4. GC 相关参数总结
  3. 3. 参考资料

Java语言的一大特点就是会自动进行垃圾回收处理,不像C++语言,程序员必须对内存分配与管理亲力亲为,当内存使用完后必须手工释放内存空间,否则就会引起内存泄漏,严重时甚至导致程序瘫痪。而java的垃圾回收机制大大减轻开发人员的工作量,但也增加了软件系统的负担。本文首先介绍java的内存管理,包括内存的分区,使得能够更好地理解垃圾回收机制。
本文是在看了《Think in JAVA》、《JAVA核心技术卷》以及相关的一些博客后的总结笔记,纯属个人理解。若有错误,望指出。文末会贴出参考资料的链接。


java内存管理

在了解java内存模型JMM之前,需要先明白java的内存管理的一些基本概念,包括jvm的含义以及jvm对内存的分区。

JDK,JRE,JVM的区别

java内存管理中容易混淆的三个概念:jdk、jre以及jvm。
1. JDK——Java Development ToolKit(Java开发工具包)。
JDK是整个JAVA的核心。包括:

  1. Java运行环境(Java Runtime Envirnment)。
  2. 一堆Java工具(javac/java/jdb等)
  3. Java基础的类库(即Java API 包括rt.jar)。

2. JRE——Java Runtime Enviromental(java运行时环境)。
也就是我们说的JAVA平台,所有的Java程序都要在JRE下才能运行。包括:

  1. JVM
  2. JAVA核心类库和支持文件。
    与JDK相比,它不包含开发工具——编译器、调试器和其它工具。

3. JVM——Java Virtual Mechinal(JAVA虚拟机)。
JVM是JRE的一部分,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

  1. JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
  2. JVM 的主要工作是解释自己的指令集(即字节码)并映射到本地的 CPU 的指令集或 OS 的系统调用。
    跨平台性:不同的操作系统,使用不同的JVM映射规则,让其与操作系统无关

java内存分区

如下图所示:JVM将内存大致分为5个区:方法区(Method Area)堆(Heap)虚拟机栈(JVM Stack)本地方法栈(Native Method Stacks)程序计数器(Program Counter Register)

Method Area (方法区)

也称”永久代” 、“非堆”。

  1. 它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。
  2. 其默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。

Heap (堆)

也叫做java 堆、GC堆,是java虚拟机所管理的内存中最大的一块内存区域。

  1. 存放了对象实例及数组(所有new的对象),被各个线程共享的内存区域,在JVM启动时创建。
  2. 大小通过-Xms(最小值,默认为操作系统物理内存的1/64但小于1G)和-Xmx(最大值,默认为操作系统物理内存的1/4但小于1G)参数设置。
  3. 默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比例。
  4. 当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比例。
  5. 对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。
  6. 垃圾回收就是在Heap上运行。

因为垃圾回收器大都是采用分代收集算法,所以堆被分为新生代老年代、还有持久代(有些JVM没有持久代)。
1. 新生代( New Generation或者Young Generation):
(1) 程序新创建的对象都是从新生代分配内存;
(2) 由Eden Space 和两块相同大小的Survivor Space( FromSpace 和ToSpace)构成
(3) -Xmn:指定新生代的大小
(4) -XX:SurvivorRation来调整Eden Space及Survivor Space的大小
(5) 新生代适合那些生命周期较短,频繁创建及销毁的对象
2. 老年代( Old Generation):
(1) 用于存放经过多次新生代GC任然存活的对象,例如缓存对象,
(2) 新建的对象也有可能直接进入老年代:
a. 大对象: 通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。
b. 大的数组对象:
(3) 老年代的内存大小=(-Xmx)-(-Xmn)
(4) 旧生代适合生命周期相对较长的对象
3. 持久代( Permanent Generation):
(1) 在Sun的JVM中就是方法区的意思, 主要存放常量及类的一些信息。
(2) 默认最小值为16MB,最大值为64MB,可通过-XX:PermSize及-XX:MaxPermSize来设置最小值和最大值。
注意点:

  1. Java堆内存是操作系统分配给JVM的内存的一部分。
  2. 可以用JConsole或者 Runtime.maxMemory(), Runtime.totalMemory(), Runtime.freeMemory()来查看Java中堆内存的大小。
  3. 可以使用命令“jmap”来获得heap dump,用“jhat”来分析heap dump。
  4. 遇到java.lang.outOfMemoryError时:(1) 增加堆空间;(2) 查看 Java程序中是否存在内存泄露。
  5. 使用Profiler和Heap dump分析工具来查看Java堆空间。

JVM Stack(虚拟机栈)

  1. 描述的是java 方法执行的内存模型:每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。
    局部变量表:各种基本数据类型、对象引用
  2. 每个方法被调用到执行完的过程= 一个栈帧在虚拟机栈中从入栈到出栈的过程。
  3. 声明周期与线程相同,是线程私有的。

Native Method Stack (本地方法栈)

与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。

PC Register(程序计数器)

是最小的一块内存区域,它是当前线程所执行的字节码的行号指示器。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
面试整理——堆与栈的区别

  1. 存储内容:栈存放局部变量以及引用。 堆存放对象实例及数组(所有new的对象) 。
  2. 被谁占有:堆被整个程序共享,堆中的对象被所有线程可见。 栈属于单个线程,存储的变量只在其所属的线程中可见。
  3. 空间管理:Stack内存满足LIFO。Heap被分为Young Generation, Old Generation, Permanent Generation,在它基础上会运行垃圾回收机制。
  4. 生存时间:Stack Memory伴随调用它的Method存在、消失。而Heap Memory从程序的开始一直存活到终止。
  5. 体积大小:Stack Memory体积远大于Heap Memory。由于Stack用LIFO调度,它的访问速度也快得多。可以用-Xms或者-Xmx定义Heap的初始大小,用-Xss定义Stack的初始大小。
  6. 异常错误:当Stack满了,Java Runtime会抛出java.lang.StackOverFlowError。当Heap满了,会抛出java.lang.OutOfMemoryError: Java Heap Space Error。

java垃圾回收机制(Garbage Collection,GC)

这部分内容的总结主要由四个个问题引出:为什么要进行垃圾回收?哪些垃圾需要回收?什么时候进行垃圾回收?如何进行垃圾回收?

为什么要进行垃圾回收?

随着程序的运行,内存中存在的实例对象、变量等信息占据的内存越来越多,如果不及时进行垃圾回收,必然会带来程序性能的下降,甚至会因为可用内存不足造成一些不必要的系统异常。

哪些垃圾需要回收?

在java内存管理的五大区中, 有三个是不需要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,所以只有方法区和堆需要进行GC。

什么时候进行垃圾回收?

(1)经典的引用计数算法
每个对象添加一个引用计数器,每被引用一次,计数器加1;失去引用,计数器减1;当计数器在一段时间内保持为0时,该对象即被认为可回收
缺陷:当两个对象互相引用,但二者均没有作用时,无法完成内存清理
(2)根搜索算法
对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。也就是说,从堆栈和静态存储区开始,遍历所有的引用,就能找到“活”的对象

补充引用的概念
a> 强引用(Strong Reference):为刚被new出来的对象所加的引用,它的特点就是,永远不会被回收。
b> 软引用(Soft Reference):声明为软引用的类,是可被回收的对象; 如果JVM内存并不紧张,这类对象可以不被回收,如果内存紧张,则会被回收。
为什么不立即回收?: Java中是存在缓存机制的, 缓存的对象就是当前可有可无的, 方便使用,提高程序性能。
c> 弱引用(Weak Reference):弱引用的对象就是一定需要进行垃圾回收的,不管内存是否紧张,当进行GC时,标记为弱引用的对象一定会被清理回收。
d> 虚引用(Phantom Reference):虚引用可以忽略不计,JVM完全不会在乎虚引用,其唯一作用就是做一些跟踪记录,辅助finalize函数的使用。
什么样的类需要回收?
1> 该类的所有实例对象都已经被回收。
2> 加载该类的ClassLoader已经被回收。
3> 该类对应的反射类java.lang.Class对象没有被任何地方引用。

如何进行垃圾回收?

基于分代:新生代、老年代、持久代

常见的GC算法:

(1) 标记-清除算法(Mark-Sweep)
将需要进行回收的对象做标记,之后扫描,有标记的进行回收。
评价:效率不高、 会产生内存碎片。
(2) 复制算法(Copying)
sum的JVM将Eden区和Survivor区比例调为8:1,保证有一块Survivor区是空闲的。在垃圾回收时,将不需要进行回收的对象放在空闲区,然后将Edon区和第一块Survivor区进行完全清理。若第二块Survivor区的空间不够,可借用持久代。
评价:复制算法的高效性是建立在存活对象少、垃圾对象多的前提下,因此适合于新生代。
(3) 标记-整理(或叫压缩)算法(Mark-Compact)
是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。首先需要从根节点开始对所有可达对象做一次标记,然后将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
评价:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
(4) 分代(Generational Collecting)
将内存区间根据对象的特点分成几块(新生代、老年代。。),每块内存区间使用不同的回收算法,以提高垃圾回收的效率。在新生代用复制算法,老年代中的对象是经过几次垃圾回收之后依然幸存的,因此可被认为是常驻内存,故可使用标记-压缩算法。
(5) 增量算法(Incremental Collecting)
在垃圾回收过程中,应用软件将处于一种CPU消耗高的状态下,在这种状态下,应用程序的所有线程将会挂起,等待垃圾回收完成。线程挂起太久将会严重影响用户性能和系统的稳定性。
增量算法的思想是:让垃圾回收线程和程序线程交替执行,每次只让垃圾回收器回收一小片区域的内存空间,然后切换到应用程序线程。此种方法能减少系统的停顿时间,但是线程切换和上下文的消耗会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

垃圾回收器的类型

(1) 按线程数分:串行垃圾回收器和并行垃圾回收器
(2) 按工作模式分:并发式垃圾回收器和独占垃圾回收器
(3) 按碎片处理方式分:压缩垃圾回收器和非压缩垃圾回收器
(4) 按工作的内存区间分:新生代垃圾回收器和老年代垃圾回收器
垃圾回收器的评判标准:吞吐量、垃圾回收器负载、停顿时间、垃圾回收频率、反应时间、堆分配。

GC 相关参数总结

1. 与串行回收器相关的参数
-XX:+UseSerialGC:在新生代和老年代使用串行回收器。
-XX:+SuivivorRatio:设置 eden 区大小和 survivor 区大小的比例。
-XX:+PretenureSizeThreshold:设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。
-XX:MaxTenuringThreshold:设置对象进入老年代的年龄的最大值。每一次 Minor GC 后,对象年龄就加 1。任何大于这个年龄的对象,一定会进入老年代。
2. 与并行GC相关的参数
-XX:+UseParNewGC: 在新生代使用并行收集器。
-XX:+UseParallelOldGC: 老年代使用并行回收收集器。
-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。
-XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。
-XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。
-XX:+UseAdaptiveSizePolicy:打开自适应 GC 策略。在这种模式下,新生代的大小,eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
3. 与CMS(Concurrent Mark Sweep)回收器相关的参数。
CMS 收集器主要关注于系统停顿时间。 使用标记-清除算法,使用多线程并发回收的垃圾收集器
-XX:+UseConcMarkSweepGC: 新生代使用并行收集器,老年代使用 CMS+串行收集器。
-XX:+ParallelCMSThreads: 设定 CMS 的线程数量。
-XX:+CMSInitiatingOccupancyFraction:设置 CMS 收集器在老年代空间被使用多少后触发,默认为 68%。
-XX:+UseFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩。
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收。
-XX:+CMSParallelRemarkEndable:启用并行重标记。
-XX:CMSInitatingPermOccupancyFraction:当永久区占用率达到这一百分比后,启动 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。
-XX:UseCMSInitatingOccupancyOnly:表示只在到达阈值的时候,才进行 CMS 回收。
-XX:+CMSIncrementalMode:使用增量模式,比较适合单 CPU。
4. 与G1((Garbage First))回收器相关的参数。
作为一款服务器的垃圾收集器, 吞吐量和停顿控制上,预期要优于 CMS 收集器。 基于标记-压缩算法
-XX:+UseG1GC:使用 G1 回收器。
-XX:+UnlockExperimentalVMOptions:允许使用实验性参数。
-XX:+MaxGCPauseMills:设置最大垃圾收集停顿时间。
-XX:+GCPauseIntervalMills:设置停顿间隔时间。
5. 其他参数
-XX:+DisableExplicitGC: 禁用显示 GC。


参考资料

书本:
《Think in Java》
参考博文:
https://www.ibm.com/developerworks/cn/java/j-lo-JVMGarbageCollection/
http://blog.csdn.net/zhangerqing/article/details/8214365