JAVA内存管理

JAVA内存管理

JVM结构

  • Class Loader
    类加载器的作用是根据给定的全限定名类名(如
    java.lang.Object)来装载class 文件的内容到Runtime data area 中的method
    area(方法区域)。如HelloWord.java程序然后通过javac编译成class文件,ClassLoader负责加载class文件到内存。
  • Execution Engine
    执行引擎也叫做解释器(Interpreter),执行classes中的指令。任何JVM
    specification 实现(JDK)的核心是Execution engine, 换句话说:Sun 的JDK
    和IBM 的JDK 好坏主要取决于他们各自实现的Execution engine 的好坏。每个
    运行中的线程都有一个Execution engine的实例。
  • Native Interface
    本地接口的作用是:与native libraries 交互,是其它编程语言交
    互的接口,如融合C/C++程序。
  • Runtime data area
    运行数据区,将程序都被加载到这里,进行运行。JVM的内存管理也就是围绕着Runtime data area区域。下面重点介绍此区域。

JVM内存模型(Runtime data area)

Runtime data area 主要包括五个部分:Heap (堆), Method Area(方法区域),
Java Stack(java 的栈), Program Counter(程序计数器), Native method
stack(本地方法栈)。Heap 和Method Area 是被所有线程的共享使用的;而Java
stack, Program counter 和Native method stack 是以线程为粒度的,每个线
程独自拥。(这里可能出现的异常java.lang.OutOfMemoryError:
Java heap space)

  • Heap:每一个java程序有一个jvm实例,一个jvm实例只有一个堆空闲,所有线程共享。这就需要考虑多线程访问堆数据的同步问题。
  • Method area:被转载的class信息存储在Method area内存中。当虚
    拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个
    class文件内容并把它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并
    将这些信息存储到方法区。该类型中的类(静态)变量同样也存储在方法区中。(这里可能出现的异常java.lang.OutOfMemoryError: PermGen full)
  • Java Stack:。每当线程调用一个方法的时候,
    就对当前状态作为一个帧保存到java stack 中(压栈);当一个方法调用返回时,
    从java stack 弹出一个帧(出栈)
public class TestStackOverFlow {
    //Exception in thread "main" java.lang.StackOverflowError  
    public static void main(String[] args) {
        System.out.println("start------------");
        Recursion recursion = new Recursion();
        recursion.doAction(100000);
        System.out.println("end-----------");
    }

    static class Recursion {
        public int doAction(int n) {
            if (n <= 1) {
                return 1;
            }
            return n + doAction(n - 1);
        }
    }
} 
  • Program counter:每个运行中的Java 程序,每一个线程都有它自己的PC寄存器。PC 寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
  • Native method stack:对于一个运行中的Java 程序而言,它还能会用到一些跟本地方法相关的数据区。
    当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制
    的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,不止与
    此,它还可以做任何它想做的事情。比如,可以调用寄存器,或在操作系统中分
    配内存等。总之,本地方法具有和JVM 相同的能力和权限。(这里出现JVM 无法
    控制的内存溢出问题native heap OutOfMemory )

Sun JVM的内存管理

JVM Specification 只是抽象的说明了JVM 实例按照子系统、内存区、数据类型
以及指令这几个术语来描述的,但是规范并非是要强制规定Java 虚拟机实现内
部的体系结构,更多的是为了严格地定义这些实现的外部特征。
Sun JVM 实现中:Runtime data area(JVM 内存) 五个部分中的Java Stack ,
Program Counter, Native method stack 三部分和规范中的描述基本一致;但
对Heap 和Method Area 进行了自己独特的实现,这与Sun JVM的内存管理和Garbage
collector(垃圾回收)机制有关,下面我们来重点介绍。

JVM的分代


备注:

  • 新生代中大部分为临时对象,因此采用了复制算法实现,划分为了eden、s0、s1三个区域,或者又称为eden、from、to。
  • 通常将对新生代迚行的回收称为Minor GC;对旧生代进行的回收称为Major GC,但由于Major GC除并发GC外均需对整个堆以及持久代进行扫描和回收,因此又称为Full GC。

疑问:Permanent Space属于堆还是Method Area?
Method Area 在HotSpot JVM的实现中属于非堆区,非堆区包括两部分:Permanet Generation和Code Cache,而Method Area属于Permanert Generation的一部分。Permanent Generation用来存储类信息,比如说:class definitions,structures,methods, field, method (data and code) 和 constants。Code Cache用来存储Compiled Code,即编译好的本地代码,在HotSpot JVM中通过JIT(Just In Time) Compiler生成,JIT是即时编译器,他是为了提高指令的执行效率,把字节码文件编译成本地机器代码。对于不同厂家和不同版本的JDK实现有差别,最靠谱的方法还是JVM Specification

更详细的介绍可以参考:http://www.cnblogs.com/duanxz/p/3724120.html

JVM Heap:从广义的角度来说,JVM堆内存在物理上被划分为两个部分Young Generation(年轻代)和Old Generation(老年代)。

  • Young Generation(年轻代)
    • 年轻代是存放所有被创建的新对象的区域。当年轻代满了的时候,垃圾回收将被执行。这时的垃圾回收叫做 Minor GC. 年轻代又被划分为三个部分-Eden Memory和两个Survivor Memory区域。
    • 大多数的新创建对象位于年轻代的Eden Memory区域
    • 当Eden区域被对象占满时,Minor GC启动,所有的幸存对象被移动到两个Survivor区域中的一个
    • Minor GC也会检查Survivor区域中的对象,并且也会把它们从一个幸存区域移动到另一个幸存区域中。所以,两个幸存区域(S0,S1)总有一个是空的。
    • 经过多次GC而幸存下来的对象会移动到Old Generation中,这个次数其实是一个对象年龄(Age)计数器,默认为15,通常会设定一个阈值来指定,可以通过参数 -XX:MaxTenuringThreshold 来设置
  • Old Generation(老年代)
    • 老年代中的对象通常生命周期较长,经过了多轮的Minor GC后依然存活,则会被复制到老年代。
  • Permanent Generation(永久代)
    • 方法区中的数据存储于永久代中,其垃圾回收主要分两种:常量和无用的类信息,对于常量的回收,只要是没有引用就可以被回收。
    • 对于类信息的回收要满足:类的所有实例被回收;类的ClassLoader被回收;没有通过反射引用该类。

JVM参数说明
(如果你发现上述参数不满足你需求,请读这里,JVM Options Official Page.)
查看命令:jmap–heap [pid]
-Xms:JVM启动时设置初始堆大小,默认是物理内存的1/64;
-Xmx:最大堆大小,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。一般设置xmx和xms一样,避免每次GC调整堆大小 -Xmn:年轻代大小,剩下的空间分配给老年代
-XX:PermGen:初始永久代大小
-XX:MaxPermGen:最大永久代大小
-XX:SurvivorRatio:Eden区和Survivor区的比率,例如,如果年轻代总共大小为10M,这个参数设置为-XX:SurvivorRatio=2 那么Eden区域获得5M,SO,S1分别是2.5M,默认值为8
-XX:NewRatio:老年代和年轻代的比率,默认值为2
-XX:MaxTenuringThreshold: 用于控制对象在新生代存活的最大次数。默认值15
-XX:PretenureSizeThreshold=x,控制超过多大字节的对象就在old上分配

Garbage Collector

不同厂商的不同版本GC实现会略微不同。下面我们介绍Sun JDK 1.6 HotSpot的版本实现。下面demo代码中会涉及到GC日志,其解读可以参考Understanding Garbage Collection Logs

Serial Copying(串行GC)

单线程收集器,使用单线程完成所有GC工作,没有线程通信,较为高效。使用单处理器机器。JVM client模式下默认GC方式。可通过参数-XX:+UseSerialGC强行指定。

GC触发条件

Minor GC

  • eden空间不足

Full GC:

  • old空间不足
  • perm空间不足
  • 显示调用System.gc()
GC触发时的工作

Minor GC:

  • 清空eden+from中所有no ref的对象占用的内存
  • 将eden+from中所有存活的对象copy到to中
  • 在这个过程中一些对象将晋升到old中
    • to放不下的
    • 存活次数超过tenuringthreshold的
  • 重新计算Tenuring Threshold
  • 单线程做以上所有动作
  • 全过程暂停应用

Full GC:

  • 如配置了CollectGen0First,则先触发YGC;
  • 清空heap中no ref的对象,permgen中已经被卸载的classloader中加载的class的信息;
  • 单线程做以上所有动作;
  • 全过程暂停应用。
ParNew(并行GC)
Parallel (并行回收GC)

Server模式下默认;YGC: PSYoungGen(Parallel Scavenge) FGC: Parallel MSC。可以通过-XX:+UseParallelGC或-XX:+UseParallelOldGC来强制指定;
– ParallelGC代表FGC为Parallel MSC
– ParallelOldGC代表FGC为Parallel Compacting

GC触发条件

Minor GC

  • eden空间不足

Full GC:

  • old空间不足
  • perm空间不足
  • 显示调用System.gc()
GC触发时的工作

Minor GC:

  • 和Serial所作的动作基本相同,不同的为多线程在做这些动作;
  • 另一不同:MinorGC的时候的最后不仅重新计算Tenuring Threshold,还会重新调整Eden和From的大小。

Full GC:

  • 如配置了ScavengeBeforeFullGC(默认),则先触发YGC;
  • MSC: 清空heap中no ref的对象,permgen中已经被卸载的classloader中加载的class的信息,并进行压缩;
  • Compacting: 清空heap中部分no ref的对象,permgen中已经被卸载的classloader中加载的class的信息,并进行部分
    压缩;
  • 多线程做以上动作。

Concurrent Mark-Sweep (CMS) Collector

  • 可用-XX:+UseConcMarkSweepGC来强制指定;
  • 优点:在对Old 进行回收时,对应用造成的暂停时间非常短,适合对latency要求高的应用;
  • 缺点:1.内存碎片和浮动垃圾;2.Old区上的内存分配效率低;3.回收的整个耗时比较长;4.和应用争抢CPU;
GC触发条件

Minor GC

  • eden空间不足

CMS GC

  • Old Gen的使用到达一定的比率,默认为92%;
  • 配置了CMSClassUnloadingEnabled,且Perm Gen的使用到达一定的比率,默认为92%;
  • Hotspot自己根据估计决定是否要触发;
  • 在配置了ExplicitGCInvokesConcurrent 的情况下显示调用了System.gc。

Full GC:

  • Promotion Failed 或 Concurrent Mode Failure时;
GC触发时的工作

Minor GC:

  • 和Serial所作的动作基本相同,不同的为多线程在做这些动作;

CMS GC

  • old gen到达比率时只清除old gen中no ref的对象所占用的空间;
  • perm gen到达比率时只清除已被清除的classloader加载的class信息;

Full GC: 和Serial动作完全相同。

更多关于GC和GC的算法可以阅读神级文档jvm hotspot内存管理白皮书

参考文献

命令工具

  • jstat –gcutil:在运行时查看堆中各个内存区间的变化以及GC的状况
  • jmap –heap:可用查看各个内存区间的大小。
  • java -version: 查看jvm在server还是client方式运行
  • 启动时可添加-XX:+PrintGCDetails–Xloggc:<file> 输出到日志文件来查看GC的状冴,方便想看就看

推荐阅读更多精彩内容