`
406657836
  • 浏览: 4104 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

java也谈gc

    博客分类:
  • java
阅读更多
一直以来想写博客,而没有写。今天下定决心开始培养写博客的习惯。第一篇博客,不会写的太深。主要谈谈java关于gc方面的知识。

本文主要是针对Hotspot虚拟机讨论。其它的虚拟机可能有所不同,请读者自行区别呵。因为我们通常用的也是Hotspot。
众所周知,java对内存的回收是由gc自动回收。这就不得不说java中有哪些内存需要回收,那么就得先看看java有哪些内存区域。

java运行时的内存区域:

1.java堆,这是java程序中应用最多的一块内存区域。我们new一个对象的内存都在这里分配。

2.方法区,这是java程序的代码区域。类,方法,变量的信息都存在这个区域,当然常量池也在这个区域。

3.虚拟机栈,java程序是支持多线程的程序,也是基于栈解释的程序。所以程序调用方法的压栈,出栈就是指的这个虚拟机栈。虚拟机栈主要是分配对象的引用和基本类型。

4.本地方法栈,java程序是支持native方法的程序。那么native方法栈的空间将在这部分内存分配。

5.程序计数器,这个用到的内存极少,很多时候不需要考虑。当然这个是和一个线程绑定的。存放程序命令执行到的位置。

6.直接内存,需要强调这个不是在jvm进程的内存空间,而是用的操作系统的内存。之所以把它写到这里,是要提醒大家千万别忘了这块内存。这也是容易发生OOM的地方。特别是NIO的时候。下面会具体谈。

可以看出这些内存区域,1,2,6内存区域是全局共享的,也就是所有的线程都共用这些内存空间。3,4,5是线程绑定的,也就是说这些内存空间的生命周期是和线程息息相关的。那么3,4,5的内存释放就非常简单了。比如虚拟机栈,随着压栈内存分配,出栈内存释放。所谓压栈,出栈就是方法调用和方法退出。本地方法栈也一样。程序计数器随线程的回收而回收。下面重点谈谈1,2,6的内存回收。

关于java堆的内存回收:

毫无疑问这块内存是java中最活跃的内存区域。所有的java new一个对象都要在这里完成内存分配。当这块内存在发生gc后任然不能分配对象时,将发生OOM。java.lang.OutOfMemoryError: ......java heap space。这个通常是最容易产生的内存溢出。如果发生了这类OOM就要考虑是否堆内存设置得太小,或者程序是不是一直在分配内存并且持有强引用,使这些对象一直是强可及对象?好了,直接进入正题说这块内存是如何回收的。要知道如何回收先要知道关于任何gc都需要解决的问题。

1.内存中有哪些对象,哪些对象又是可以回收的,哪些对象是不能回收的

2.如何回收,怎么把需要回收的对象从内存中移除。
3,什么时候需要回收呢。

4.如何解决回收时程序的延迟,以及发生gc时的内存分配请求。特别是多线程程序,在发生gc时候必然还有很多线程在请求堆内存分配。怎么处理这些请求。

下面对这些问题一一解答。请记住问题的编号,下面会提到问题的编号。

关于第1个问题——java中找到对象是用的根搜索算法(不是引用计数)。那么java中的根如何判定呢。java中的根主要存在在这些地方:实例变量,静态变量,方法栈上的引用,本地方法栈的引用。也就是说java只能在这里找到现在程序还在使用的对象,不能回收的对象。那么怎么找到不需要的对象呢。嘿嘿,当然就是对内存中的所有变量进行标记,把需要使用的对象统统标记出来,打过标记的对象就是要用的,不要回收掉了。这里就产生了一系列问题呵,这些问题也是用来解答问题4的。java的内存可以分配N个G,那么这么多的对象,每次都标记,得要多久啊。况且java中有些对象是程序启动到结束一直都在堆里面的。每次都来标记它,然后又不清除,这多浪费啊!所以java的gc强大之处就展现出来了。

关于问题2如何回收——java采用分代回收。java中的堆内存分成两个代:新生代,老年代。

所谓新生代:  可以从字面上理解,就是那些还比较年轻的对象。就是"才"分配出来的对象。新生代的对象会根据一定的晋升策略进入老年代。晋升策略有(可能不全):

1,new一个对象这个对象足够大(默认是超过新生代内存的一半,这个可以配置),那么直接进入老年代。

2,当一个对象在新生代存活了一定次数(默认是15次,这个次数还是有点多呵,可以根据情况配置),就进入老年代。

3,在新生代发生gc时,“内存不足”(这里关系到新生代的内存回收策略,等会会讲到,也会说明为什么会不足),要老年代提供担保时。新生代多出的对象直接进入老年代。

新生代的回收策略: 新生代也分为两个区域:一部分叫做Eden区,乐透区,新生代会首先在这个区域分配对象。另一部分,由两块对等的区域组成,叫做Survivor。为什么会由两部分组成呢。这就不得不从新生代的特性说起了。在新生代请求分配内存最多的地方毫无疑问是方法栈中。通常随着方法的退栈,大多数对象都不是根可及对象了。所以这里的大多数对象都是可以回收的了。那么对这个些对象进行标记并且回收,那么回收率是非常高的。那采用哪种标记回收策略呢?这里主要有3种标记回收策略:

1,标记复制——标记可用对象然后复制到另一块内存区域,没有内存碎片。

2,标记清除——标记可用对象,然后对于不可用对象直接清除,这里必然产生内存碎片。

3,标记整理——标记可用对象,然后把可用对象往一段移动,记录可用对象区域,把可用对象另一端的对象清除掉。没有内存碎片。

显然对于新生代这种大多数对象在很快就可以被回收的情况,采用1,3种策略最合适,因为需要复制或移动的对象很少,同时不会产生内存碎片。GC在新生代通常采用的是标记复制算法。并且基于大多数对象可以被回收的假设,复制的两个区域大小不是对等的,即Eden和Survivor的大小不是对等的。默认配置是8:1。由于Survivor有两块对等的。所以新生代Eden区默认会占到五分之四。当发生GC时,会把Eden区和其中一块Survivor往另外一块Survivor复制。这个时候,另一块Survivor可能不能装下Eden和那一块Survivor的对象,如果这种情况发生就需要老年代担保,并直接进入老年代。新生代就这样来回复制,实现GC回收。在回收的过程中是要暂停内存的分配。所以这里是一个程序停顿点。通常年轻代的回收会很快,所以这里的停顿时间不会太长。但是年轻代的回收频率往往是较高的。当然年轻代的垃圾回收也可以配置单线程,多线程,可控制延时等回收器。

老年代的回收策略:这里主要讲cms的回收策略。至于G1或JDK1.5以前的一些老年代回收策略现在很少使用,这里暂时不解释。cms(concurrent-mark-sweep)从名字上来说,他是并行的老年代回收器。但是这不意味着所有老年代gc的过程都是可以和内存分配并行的。老年代回收分为下面几个过程:

1,初始化标记

2,并发标记

3,重新标记

4,并发清除

对于1,3过程对于内存的分配请求也是停顿的,对于2,4过程可以和内存的分配请求同时进行。老年代是用的标记清除算法,所以必然产生内存碎片。这可以开启内存碎片整理,在多少次GC后进行一次内存碎片整理(这是可以配置的)。这个整理过程是full GC 是让应用程序停顿的。

关于第3个问题——什么时候回收。对于新生代,通常是对象无法分配的时候会发生gc。对于老年代是到达一定的内存使用率发生一次gc。默认配置下的使用率是68%.这也是可以配置项。对于full gc可能在显示调用System.gc()后发生。当然有参数可以关闭显示调用gc。DisableExplicitGC参数设置成true,就不能显示的调用gc了。当然还有其它发生gc的情况,比如是否配置了允许担保失败呀。

关于第4个问题——分代回收是提升java性能的关键,cms的并行回收在一定程度上满足了gc和应用程序同时运行,减少了停顿时间。这也是java语言和虚拟机走向成熟,倍受关注,广泛使用的原因。当然很期待G1回收器的使用。这个并行,可以控制停顿时间,标记整理的老年代收集器,必然是java虚拟器的趋势。也是java语言走向更辉煌的一个期望。

关于方法区的内存回收:

一般来说方法区的内存分配和回收都不会像java堆那样频发。但这也不是真正意义上的永久代,这里还是会发生内存回收的。一般来说对于一个类的回收需要满足一下条件才能被回收(可能不全)

1,该类的所有实例都已经被回收

2,类的加载器已经被回收

3,类对应的class没有任何地方在使用,包括反射也不会用到。

这些条件其实比较苛刻了。在一般的应用程序中可能不会遇到。但是如果频繁使用cglib生成类或动态加载类或热部署jsp等应用程序里面还是会发生。

对于常量的回收,要满足常量没有任何强可及的引用。

关于直接内存的回收:

什么地方会使用到直接内存。一般来说是在NIO中。比如netty是默认是用的直接内存分配,mina在2.0后已经不是默认使用的直接内存了。之前已经强调了,直接内存不是使用的jvm进程的内存,而是使用操作系统的内存。这部分内存也可以在jvm启动参数里面配置。MaxDirectMemorySize。对于这块内存的回收往往要在full gc的时候才能回收。以前公司是有用netty的地方(Hbase的通信)发生了OOM,就是因为老年代的回收频率太低了,在直接内存溢出了也没有发生老年代回收。当然这里可以关闭DisableExplicitGC来解决。不过这也是一个危险的操作,意味着你允许了应用程序显示的调用System.gc(),所以在应用程序里面最好不要这么调用。关闭DisableExplicitGc,应该把相应的并行回收参数也打开,不然这里就是一个真正意义上的full gc,意味着程序是停顿的。至于具体参数可以查看jvm参数配置,这里不贴出来了。对于直接内存的回收还可以显示调用,DirectBuffer的cleaner的clean方法,具体可以参照另一个同学的博客http://blog.csdn.net/xieyuooo/article/details/7547435。里面介绍的很详细。

关于java的gc就简单介绍到这里了,当然java的gc还有很多复杂的应用。java的核心和优势之一就是强大的gc。平时可以多观察下java gc的情况,这对掌握应用程序的性能是非常有帮助的。整个jvm调优都是要基于gc的情况调整的。
0
2
分享到:
评论
3 楼 406657836 2013-06-26  
406657836 写道
baitian 写道
楼主,不严谨 ,提两点
1.
引用
当发生GC时,会把Eden区和其中一块Survivor往另外一块Survivor复制。

所谓的“其中一块Survivor”是何时被写入的?
2.
引用
当然这里可以打开DisableExplicitGC来解决。不过这也是一个危险的操作,意味着你允许了应用程序显示的调用System.gc(),所以在应用程序里面最好不要这么调用

恰恰相反,就是通过system.gc()去触发对直接内存的回收

嗯 谢谢指正呵。
对于第一个问题:
  对象分配首先会选择在Eden区(如果对象足够大,比如超过Eden区可用大小的一半,这个可以指定的,这里还和年轻代采用的回收器有关)就直接到老年代。如果是分配到年轻代会首先优先选择Eden区,也不排除有一部分进入survivor。在jvm启动的时候会选择任意选择其中一块survivor。如果Eden区不够分配了会触发minor gc。这时候就把之前选择的那块survivor和Eden的可用对象复杂到另一开survivor上去。

对于第二个问题:
  这个问题之前的表述确实存在很大的问题。应该改为"关闭DisableExplicitGC"(设为false),因为DisableExplicitGC的意思就是禁止显示调用gc。所以应该是要一个双重否定,让显示调用生效,已达到让System.gc()调用起作用,解决直接内存溢出问题。

不过仍然要注意,这个参数关闭掉是有性能风险的,至少要在程序中尽量少调用。直接内存的回收也是有其他办法的 比如 http://blog.csdn.net/xieyuooo/article/details/7547435 这个同学的博客讲的很清楚了。



补充下,任意时刻只有一块Survivor接受分配,另外一块是备用的,在minor gc的时候,接受可用对象的。这样彼此切换!
2 楼 406657836 2013-06-26  
baitian 写道
楼主,不严谨 ,提两点
1.
引用
当发生GC时,会把Eden区和其中一块Survivor往另外一块Survivor复制。

所谓的“其中一块Survivor”是何时被写入的?
2.
引用
当然这里可以打开DisableExplicitGC来解决。不过这也是一个危险的操作,意味着你允许了应用程序显示的调用System.gc(),所以在应用程序里面最好不要这么调用

恰恰相反,就是通过system.gc()去触发对直接内存的回收

嗯 谢谢指正呵。
对于第一个问题:
  对象分配首先会选择在Eden区(如果对象足够大,比如超过Eden区可用大小的一半,这个可以指定的,这里还和年轻代采用的回收器有关)就直接到老年代。如果是分配到年轻代会首先优先选择Eden区,也不排除有一部分进入survivor。在jvm启动的时候会选择任意选择其中一块survivor。如果Eden区不够分配了会触发minor gc。这时候就把之前选择的那块survivor和Eden的可用对象复杂到另一开survivor上去。

对于第二个问题:
  这个问题之前的表述确实存在很大的问题。应该改为"关闭DisableExplicitGC"(设为false),因为DisableExplicitGC的意思就是禁止显示调用gc。所以应该是要一个双重否定,让显示调用生效,已达到让System.gc()调用起作用,解决直接内存溢出问题。

不过仍然要注意,这个参数关闭掉是有性能风险的,至少要在程序中尽量少调用。直接内存的回收也是有其他办法的 比如 http://blog.csdn.net/xieyuooo/article/details/7547435 这个同学的博客讲的很清楚了。
1 楼 baitian 2013-06-25  
楼主,不严谨 ,提两点
1.
引用
当发生GC时,会把Eden区和其中一块Survivor往另外一块Survivor复制。

所谓的“其中一块Survivor”是何时被写入的?
2.
引用
当然这里可以打开DisableExplicitGC来解决。不过这也是一个危险的操作,意味着你允许了应用程序显示的调用System.gc(),所以在应用程序里面最好不要这么调用

恰恰相反,就是通过system.gc()去触发对直接内存的回收

相关推荐

    浅谈关于Java的GC垃圾回收器的一些基本概念

    主要介绍了关于Java的GC垃圾回收器的一些基本概念,牵扯倒JVM内存模型的一些知识,需要的朋友可以参考下

    Java虚拟机

    这本书的内容是帮你全面了解java虚拟机,本书第1版两年内印刷近10次,98%以上的评论全部为5星级的好评,是整个Java图书领域公认的经典著作和超级畅销书,繁体版在台湾也十分受欢迎。第2版在第1版的基础上做了很大的...

    浅谈Java的虚拟机结构以及虚拟机内存的优化

    主要介绍了Java的虚拟机结构以及虚拟机内存的优化,讲到了JVM的堆和栈空间及GC垃圾回收等重要知识,需要的朋友可以参考下

    浅谈CC++内存泄漏及其检测工具

    Garbage Collection技术在Java中已经比较成熟,但是在c/c++领域的发展并不顺畅,虽然很早就有人思考在C++中也加入GC的支持。现实世界就是这样的,作为一个c/c++程序员,内存泄漏是你心中永远的痛。不过好在现在有...

    谈谈你对垃圾回收机制的了解?.docx

    选定活动对象作为 GC Roots,然会跟踪引用链,如果一个对象和GC Roots之间不可达,也就是说不存在引用链,那么即可认为是2可回收对象。jvm会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量,...

    浅谈C/C++内存泄露及其检测工具

    Smart Pointer,Garbage Collection等。Smart Pointer技术比较成熟,STL中已经包含支持...Garbage Collection技术在Java中已经比较成熟,但是在c/c++领域的发展并不顺畅,虽然很早就有人思考在C++中也加入GC的支持。

    浅谈C/C++内存泄漏及其检测工具

    GarbageCollection技术在Java中已经比较成熟,但是在c/c++领域的发展并不顺畅,虽然很早就有人思考在C++中也加入GC的支持。现实世界就是这样的,作为一个c/c++程序员,内存泄漏是你心中永远的痛。不过好在现在有许多...

    2017阿里技术年度精选01

    16 如何降低 90%Java 垃圾回收时间?以阿里 HBase 的 GC 优化实践为例 37 如何打造千万级 Feed 流系统?阿里数据库技术解读 阿里下一代数据库技术:把数据库装入容器不再是神话 接下时序数据存储的挑战书,阿里 ...

    c#学习笔记.txt

    看完了前面几段,我的朋友提出了不同的意见:C#不是Java的Clone,它只是长得有些像Java而已,其实面向对象、中间语言什么的也不是什么新玩意儿,非Sun独创,有文为证:华山论剑:C#对Java。另外他对我上一集中说...

    net学习笔记及其他代码应用

    Abstract 类的子类为它们父类中的所有抽象方法提供实现,否则它们也是抽象类为。取而代之,在子类中实现该方法。知道其行为的其它类可以在类中实现这些方法。 接口(interface)是抽象类的变体。在接口中,所有方法...

    C#微软培训资料

    <<page 1>> page begin==================== 目 目目 目 录 录录 录 第一部分 C#语言概述.4 第一章 第一章第一章 第一章 .NET 编 编 ... 比尔....这一天 微软公司正式推出了其下一代...

Global site tag (gtag.js) - Google Analytics