JVM 内存管理探讨

JAVA - JVM - Memory Management

Posted by Yuanbo on August 1, 2017

java内存管理分析

Java Memory Management

java中的引用

相对于C++,Java语言非常好的一点就是不需要我们手动管理内存,因为有一个像妈妈一样的虚垃圾回收器,每天默默地替我们清扫着房间的垃圾,无怨无悔。基本上所有的java程序员都知道有这么个默默无闻的妈妈,可是你真的了解她吗? 上面提到垃圾回收器替我们清扫垃圾,那么重要的一点来了,什么是垃圾?在现实生活中,没有用的东西就是垃圾,会被丢进垃圾桶,运到处理厂,掩埋或是焚烧。其实在java的世界里也是一样,无用即为垃圾,不过这里的垃圾全都是:对象。 什么叫无用,即没有人要用,在java里,无用即代表没有被引用,java里面操控对象全都是通过引用来进行的,For Example:Object o=new Object(),这里我们就创建了一个Object对象,并用一个引用o来引用这个对象,这样我们就可以通过o来操作这个Obejct了。 java里一共有四种引用:

  1. 强引用 这就是我们上面写的Obejct o=new Object(),只要强引用还存在,对象就不会被回收。
  2. 软引用 SoftReference,只有将发生内存溢出时,才会进行回收。
  3. 弱引用 WeakReference,GC工作时,无论当前内存是否够用,一定会回收。
  4. 虚引用 PhantomReference, 有这个引用和没有一样,因为通过引用拿到的一定是null,和弱引用一样,GC工作时一定会被回收,唯一的作用就是监听对象被GC回收,可以用来做GC监听器,监听虚拟机的每一次GC。

内存回收

  1. 引用——计数法 即在对象头部增加一个计数器,每当对象被引用,内部的计数器就+1,当引用失效,计数器就-1。这样当垃圾回收时,就回收那些计数值为0的对象。 缺点:很难解决对象之间相互循环引用的问题。
  2. 标记——清除法 从堆栈和静态存储区出发,遍历所有引用,找出所有存活的对象。每当找到一个对象,就给它设置一个标记,这样它就不会被回收。 缺点:清除后会产生大量的不连续空间,这样对于将来大对象的内存分配是不利的,因为可能因为找不到连续的大块内存,不得不触发下一次gc。
  3. 复制——清除法 将可用空间分为两块A和B,每次只使用AB其中的一块,比如当A中内存已满的时候,就将A中所有存活的对象一次性复制到B中,然后清空整个A区。一般来说,98%的对象都是朝生夕死,所有没必要1:1的分配AB,所以一般会将内存划分为3块,一块较大的Eden区,两块较小的Survivor区。每次使用Eden和一块Survivor,GC时将存活对象移动到另一块Survivor中。但是一块Survivor可能容纳不下所有的存活对象,所以需要依赖另一块进行分配担保,只能将存活的一部分对象放入担保中,这就是内存的分配担保机制。
  4. 标记——整理法 这种方法的标记过程和标记清除算法一样,但是它解决了标记清除的问题。它的清扫过程不是直接进行的,而是先将所有存活对象都移动到一边,然后整个清扫那一边,这样就不存在内存空间不连续的问题了。

    内存分配

  5. 对象优先在新生代中分配 新生代分为Eden区和Survivor区,大小比例通常为8:1,新对象一般在Eden区进行分配,当Eden区已满时,虚拟机会发起一次GC,将Eden区还存活的对象移动至Survivor区,并一次性清扫Eden区。 大对象直接进入老年代 所谓的大对象是指需要大量连续内存空间的对象,比如说很长的字符串或者数组。经常出现大对象容易导致内存还有不少空间时就要进行gc,以获取足够空间来存放它们。 长期存活的对象将进入老年代 如果对象在Eden区出生,并能够顺利熬过第一次gc,且能被Survivor区容纳的话,那么将被移动到Survivor区,并初始化年龄为1岁,以后每熬过一次gc,年龄就加一次,当年龄增加到一定程度(默认15),就会晋升到老年代中。 分代收集 对于所有的对象总不能一刀切吧,每种算法都有其缺点和优点。所以一般会将对象划分为新生代和老年代,对于新生代这种朝生夕死的对象,用复制清除算法,并采用老年代进行担保。而对于老年代中生命力较为顽强的对象,采用复制清除是不合适的,因为需要巨大的空间来进行分配担保,所以一般会采用标记清除或者标记整理算法。

    两点疑问

  6. 为什么新生代采用复制清除法? 新生代gc比较频繁、对象存活率低,用复制算法在回收时的效率会更高,也不会产生内存碎片。
  7. 为什么复制清除法需要两块Survivor区? 如果说只有一块Survivor区,那么会发生如下情况: 第一次GC:Eden区的存活对象移动到Survivor区; 第二次GC:Eden区和Survivor区都发生了GC,都会只有一部分对象存活,这时再将Eden区存活对象复制至Survivor区,因为Survior自身存活对象的不连续性,便会产生内存碎片。 如果有两块Survivor区(S1,S2)的话,情况就能好很多: 第一次GC:Eden区的存活对象移动到S1区,S2空闲; 第二次GC:Eden区和S1区都发生了GC,都会只有一部分对象存活,这时再将Eden和S1的存活对象复制至S2区,这时Eden和S1又会保持空闲,且S2中的空闲内存也是连续的。

    内存实战

    GC日志分析 只学习理论是不够的,只会似懂非懂,绝知此事要躬行,具体还是要通过实战分析才能更深入了解JAVA的垃圾回收。

2017年8月02日11:38:52 更新

朱渊博,特普通一人,渊为深,博为广,一直努力做一个有深度有广度的人。多混迹于Github, Stackoverflow, ITeye, 知乎, Linkedin 等地带。蹉跎中练就了一身码农好本领。 酷爱编程,但绝不宅。爱生活,爱思考,性情温和但充满激情,做事有主见,信奉读万卷书行万里路。 http://www.freerambo.com/about


END