1,JVM内存模型
1.1 jvm的内存结构
1.2 垃圾回收器和垃圾回收算法
基本回收算法
- 引用计数 (Reference Counting)
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少 一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。 - 标记-清除 (Mark-Sweep)
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记 的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。 - 复制(Copying)
此 算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理 正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不过出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍 内存空间。 - 标记-整理(Mark-Compact)
此算法结合了“标记-清除”和“复 制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一 块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。 - 增量收集 (Incremental Collecting)
实施垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因 JDK5.0中的收集器没有使用这种算法的。 - 分代(Generational Collecting)
基 于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在 的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
- Young(年 轻代)
年 轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区 (两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一 个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关 系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空 的。
- Tenured(年老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是 生命期较长的对象。 - Perm(持久代)
用 于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等, 在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。 -
GC有两种类型:Scavenge GC和Full GC 。
- Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败 时,就好触发Scavenge GC,堆Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。 - Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC比Scavenge GC要慢,因此应该尽可能减少Full GC。有如下原因可能导致Full GC:- Tenured被写满
- Perm域被写满
- System.gc()被显示调用
- 上一次GC之后Heap的各域分配策略动态变化
并 发收集器
可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大 规模应用。使用-XX:+UseConcMarkSweepGC 打开。
- 并 发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器会 对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
- 并发收集 器使用处理器换来短暂的停顿时间 。在一个N个处理器的系统上,并发收集部分使用K/N 个 可用处理器进行回收,一般情况下1<=K<=N/4 。
- 在只有一个 处理器的主机上使用并发收集器 ,设置为incremental mode 模式也可获得较短的停顿时 间。
- 浮动垃圾 :由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产 生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20% 的 预留空间用于这些浮动垃圾。
- Concurrent Mode Failure :并发收集器在应 用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失 败”,此时整个应用将会暂停,进行垃圾回收。
- 启动并发收集器 :因为并发收集在应用运行时进行收 集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction=<N> 指 定还有多少剩余堆时开始执行并发收集
- 串 行处理器:
--适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。
--缺点:只能 用于小型应用 - 并行处理器:
--适用情况:“对吞吐量有高要求”,多CPU、对应用响 应时间无要求的中、大型应用。举例:后台处理、科学计算。
--缺点:应用响应时间可能较长 - 并发处理器:
-- 适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。
2, JVM调优( young generation=new generation )
堆内存设置选项 | 设置格式 | 说明 |
set new generation heap size | -XX:NewSize | 一般是最大堆内存的1/4,目的在增大较大数量的短生命周期对象的内存。相当于增加cpu数目,可并行的分配内存,但不可并行回收内存 |
set max ~同上 | -XX:MaxNewSize | 目的同上 |
set new generation heap ration | -XX:SurvivorRation | iNIT:EDEN/SURVIVOR=8. THEN try |
set minimu heap size | -xms | 一般情况同xmx,避免垃圾回收后重新分配空间 |
set max heap size | -xmx | 一般情况同xms,避免垃圾回收后重新分配空间 |
set stack size | -xss | 用来控制本地线程栈的大小。一般不要太大(2MB) |
SET NO CLASS GC | -noclassgc | 取消系统对特定类的垃圾回收;但这个类的所有引用没有后,不会被gc,进而无需重新装载;快??? |
3,对内存友好的编程ITEMS
很多人都觉得java编程相对CPP一个非常大的优势是不需要考虑内存,这放在入门阶段可以理解,但要想成为层次高点的coder,必须熟悉java的内存分配机制,并能够写出对内存友好的代码。
(1) 最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null。我们在 使用这种方式时,必须特别注意一些复杂的对象图,例如数组、队列、树、图等,这些对象之间的相互引用关系较为复杂。对于这类对象,GC回收它们的效率一般 较低。如果程序允许,尽早将不用的引用对象赋为null。这样可以加速GC的工作。 例如:
… …
A a = new A();
// 应用a对象
a = null; // 当使用对象a之后主动将其设置为空
… …
但要 注意,如果a是方法的返回值,千万不要做这样的处理,否则你从该方法中得到的返回值永远为空,而且这种错误不易被发现。因此这时很难及时抓住、排除 NullPointerException异常。
(2)尽量少用finalize函数。 finalize函数是Java给程序员提供一个释放对象或资源的机会。但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。
大家也都知道finalize()不好,分配代价昂贵,释放代价更昂贵(要多走一个循环,而且他们死得慢,和他们 相关联的对象也跟着死得慢了),又不确定能否被调用(JVM开始关闭时,就不会再进行垃圾收集),又不确定何时被调用(GC时间不定,即使 system.gc()也只是提醒而不是强迫GC,又不确定以什么样的顺序调用,所以finalize不是C++的析构函数,也不像C++的析构函数。
我们都知道啊,所以我从来都没使用。都是在显式的维护那些外部资源,比如在finally{}里释放。
(3)如果需要使用经常用到的图片,可以使用 soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory。
(4)注意集合数据类型,包括数组、树、图、 链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象,造成内存浪费。
(5)尽量避免在类的默认构造器中创建、初始 化大量的对象,防止在调用其自类的构造器时造成不必要的内存资源浪费。
(6)尽量避免强制系统做垃圾内存的回收(通 过显式调用方法System.gc()),增长系统做垃圾回收的最终时间,降低系统性能。
大家都知道System.gc()不好,full-gc浪费巨大,gc的时机把握不一定对等等,甚至有 -XX:+DisableExplicitGC的JVM参数来禁止它。
哈哈,但我还不会用System.gc()呢,不怕不怕。真的不怕吗?
先用FindBugs 查一下所用到的全部第三方类库吧... 至少RMI 就会老实不客气的执行System.gc()来实现分布式GC算法。但我也不会用RMI啊。那EJB呢,EJB可是建在RMI上的....如果无可避免,用-Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 (单位为微妙) 增大大GC的间隔(原默认值为1分钟),-XX:+ExplicitGCInvokesConcurrent 让System.gc() 也CMS并发执行.
(7)尽量避免显式申请数组空间,当不得不显 式地申请数组空间时尽量准确地估计出其合理值,以免造成不必要的系统内存开销。
(8)尽量在做远程方法调用(RMI)类应用 开发时使用瞬间值(transient)变量,除非远程调用端需要获取该瞬间值(transient)变量的值。
(9)尽量在合适的场景下使用对象池技术以提 高系统性能,缩减系统内存开销,但是要注意对象池的尺寸不易过大,及时清除无效对象释放内存资源,综合考虑应用运行环境的内存资源限制,避免过高估计运行 环境所提供内存资源的数量。
java 不是有垃圾收集器了吗?怎么还泄漏啊,唬我啊??
嗯,此泄漏非比泄漏。C/C++的泄漏,是对象已不可到达,而内存又没有回收,真正的内存黑洞。
而Java的泄漏,则是因为各种原因,对象对应用已经无用,但一直被持有,一直可到达。
总结原因无外乎几方面:
这些集合类的生命周期通常极长,而且是一个辅助管理性质的对象,在一个业务事务运行完后,如果没有将某个业务对象主动的从中清除的话,这个集合就会吃越来 越多内存,可以用WeakReference,如WeakHashMap,使得它持有的对象不增加对象的引用数。 Scope 定义不对,这个很简单了,方法的局部变量定义成类的变量,类的静态变量等。 异 常时没有加finally{}来释放某些资源,JDBC时代也是很普遍的事情。 另 外一些我了解不深的原因,如:Swing里的Listener没有显式remove;内部类持有外部对象的隐式引用;Finalizers造成关联对象没 有被及时清空等。
注:很多资料都是来源于网络。