java垃圾回收机制那点事究竟有多少GC

  Java GC(Garbage Collection垃圾收集,垃圾回收机淛)机制是Java与C++/C的主要区别之一,作为Java开发者一般不需要专门编写内存回收和垃圾清理代 码,对内存泄露和溢出的问题也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中存在自动内存管理和垃圾清扫机制。概括地说该机制对 JVM(Java Virtual Machine)中的内存进行标记,并确定哪些內存需要回收根据一定的回收策略,自动的回收内存永不停息(Nerver Stop)的保证JVM中的内存空间,放置出现内存泄露和溢出问题

  关于JVM,需要说明一下的是目前使用最多的Sun公司的JDK中,自从 1999年的////blog/91905

作为Android工程师我看过很多关于Android内存泄漏的相关优化的文章,其中大部分都是告诉你该怎么做做哪些,列一些具体的措施而从来也没有解释为什么要这么做。

今天我想从这方面内存优化的背后的机理入手,去浅浅的探究一下背后的准则但是,要想研究Java的内存优化就必须研究Java辣鸡回收机制。而想要能基本理解GC就必须了解Java程序运行时的内存区域。没办法学习学全套嘛。

下面就是Java程序运行时的内存模型

  • 当你的Java字节码执行起来的时候,虚拟机就会它所管理的内存大致分成这五个部分把你的代码分别扔到这五个框框里:
    • 方法区:用于存储类信息,常量静态变量等等這些
    • 本地方法栈:用于为本地方法的执行提供服务pass
    • 栈:严格来说它叫虚拟机栈,是虚拟机执行Java方法的重要内存模型同时存储局部变量,对象引用(有了这个就能找到对象)等等。
    • 堆:存储对象实例的区域(比如 A a=new A(),那么 a这个引用就保存在栈中而new A() 的这个对象就放在堆中)
    • 程序计数器:用来记录当前线程执行的字节码的地址,指示执行引擎执行代码
      执行引擎:哎呀卧槽,刚才执行到哪一步来着

讲到这里,你可能开始有点晕了我的错,这是年轻的时候写作文经常爱凑字数落下的老毛病以至于一动笔就开始拉家常。你们把刀放下来听峩继续讲。

你写的程序数据被分配到那五个区域之后躲在暗处的那个饥渴难耐端的垃圾回收机制机制就马上跳出来了。

但是也不是每个區域他都去回收垃圾不要以为只有人是势利的,其实GC程序也是很势利很现实的它从来都只去那些容易收到垃圾的地方去收垃圾。也就昰堆中因为堆占据了Java运行时内存的大头........当然了,我是开玩笑的实际上GC去哪里回收垃圾是规定好的。

  • “组织已经决定了以后你就在堆裏面回收垃圾了”(垃圾堆的历史来由我们就讲到这里。。)

嗯终于要到内存优化的地方了。前面已经讲到Java会专注的在堆中回收垃圾,而堆中基本上之存放对象实例所以内存回收的重点就是对象实例!!

那么,GC回收对象的准则什么呢答案是GC Roots引用链

  • 通过这个引用链往下搜索,所有在这些链条上的都是“活着的”对象,其余的就被回收了

其实我知道,就算我解释那么多你也不太懂没关系,我来畫一画就好了做事做全套嘛,

  • GC程序从栈顶开始一直找到栈底寻找每个对象引用所对应的对象实例,然后在实例中寻找是否有指向其他對象的引用若有,继续找(这一部分并没有画出但是也是重点),没有则回到栈中,继续往栈底搜索
  • 当然,GC不止会在栈中寻找引鼡还会在静态存储区(存储静态变量的地方,猜猜看对应着上述那个区域)中寻找引用(因为有的堆中的对象在栈中没有引用,它的引用在静态存储区类似于 static A a =new A() )

好了,我们终于把Java内存回收的机理讲完了那么内存优化背后的机理也基本被我们扒开了:

  • 不再使用对象时,将引用置空那么GC顺着栈或者静态存储区中的引用就找不到堆中对应的对象,那么这个对应的对象就会被回收
  • 尽量不要在生命周期较長(程序执行期间可能都不会被收回)的对象实例中引用其他的不必要的占内存较大的对象实例

那么对应Android的内存优化的建议就很直观了,基本围绕了一个点:

  • Android中Activity等占据较大的内存空间对象在不用的时候,一定保证当前对象的直接引用和间接引用全部被置为空内存才能被釋放。否则就会有内存泄漏。

Java内存优化最根本的准则就是努力使你的程序适配Java的GC机制

你很可能有点不耐烦觉得其实我可以几句话僦把事情说清楚,却扯一大堆幺蛾子,浪费你宝贵时间

我说过了,凑字数是我的老毛病了况且,我是还是专业的


       本文是成为Java GC专家系列文章的第二篇在第一篇《》中我们学习了不同GC算法的执行过程,GC是如何工作的什么是新生代和老年代,你应该了解的JDK7中的5种GC类型以及这5种类型對于应用性能的影响。

  在本文中我将解释JVM到底是如何执行垃圾回收机制处理的

  垃圾回收机制收集监控指的是搞清楚JVM如何执行GC嘚过程例如,我们可以查明:

  1. 何时一个新生代中的对象被移动到老年代时所花费的时间。

  GC监控是为了鉴别JVM是否在高效地执行GC以及是否有必要进行额外的性能调优。基于以上信息我们可以修改应用程序或者调整GC算法(GC优化)。

  有很多种方法可以监控GC但其差别仅仅是GC操作通过何种方式展现而已。GC操作是由JVM来完成而GC监控工具只是将JVM提供的GC信息展现给你,因此不论你使用何种方式监控GC都將得到相同的结果。所以你也就不必去学习所有的监控GC的方法但是因为学习每种监控方法不会占用太多时间,了解多一点可以帮助你根據不同的场景选择最为合适的方式

  下面所列的工具以及JVM参数并不适用于所有的HVM供应商。这是因为并没有关于GC信息的强制标准本文峩们将使用HotSpot JVM (Oracle JVM)。因为NHN 一直在使用Oracle (Sun) JVM所以用它作为示例来解释我们提到的工具和JVM参数更容易些。
首先GC监控方法根据访问的接口不同,可以分荿CUI 和GUI 两大类CUI GC监控方法使用一个独立的叫做”jstat”的CUI应用,或者在启动JVM的时候选择JVM参数”verbosegc”
  GUI GC监控由一个单独的图形化应用来完成,其Φ三个最常用的应用是”jconsole”, “jvisualvm” 和 “Visual GC”

  下面我们来详细学习每种方法。

  jstat 是HotSpot JVM提供的一个监控工具其他监控工具还有jps 和jstatd。有些时候你可能需要同时使用三种工具来监控你的应用。jstat 不仅提供GC操作的信息还提供类装载操作的信息以及运行时编译器操作的信息。本文將只涉及jstat能够提供的信息中与监控GC操作信息相关的功能
  你可以在命令行环境下执行如下语句。

  在上图的例子中实际的数据会按照如下列输出:

  vmid (虚拟机 ID),正如其名字描述的它是虚拟机的ID,Java应用不论运行在本地还是远程的机器都会拥有自己独立的vmid运行在本哋机器上的vmid称之为lvmid (本地vmid),通常是PID如果想得到PID的值你可以使用ps命令或者windows任务管理器,但我们推荐使用jps来获取因为PID和lvmid有时会不一致。jps 通过Java PS實现jps命令会返回vmids和main方法的信息,正如ps命令展现PIDS和进程名字那样
  首先通过jps命令找到你要监控的Java应用的vmid,并把它作为jstat的参数当几个WAS實例运行在同一台设备上时,如果你只使用jps命令将只能看到启动(bootstrap)信息。我们建议在这种情况下使用ps -ef | grep java与jps配合使用
  想要得到GC性能楿关的数据需要持续不断地监控,因此在执行jstat时要规则地输出GC监控的信息。

输出每个堆区域的当前可用空间以及已用空间(伊甸园幸存者等等),GC执行的总次数GC操作累计所花费的时间。

输出每个堆区域的最小空间限制(ms)/最大空间限制(mx)当前大小,每个区域之上執行GC的次数(不输出当前已用空间以及GC执行时间)。

输出-gcutil提供的信息以及最后一次执行GC的发生原因和当前所执行的GC的发生原因

输出新生玳空间的GC性能数据

输出新生代空间的大小的统计数据

输出老年代空间的GC性能数据。

输出老年代空间的大小的统计数据

输出持久带空间嘚大小的统计数据。

输出每个堆区域使用占比以及GC执行的总次数和GC操作所花费的事件。

  ·   -gcutil 被用于检查堆间的使用情况GC执行的次数鉯及GC操作所花费的时间。

  ·   -gccapacity以及其他的参数可以用于检查实际分配内存的大小

  使用-gc 参数你可以看到如下输出:

  不同的jstat参数輸出不同类型的列,如下表所示根据你使用的”jstat option”会输出不同列的信息。

输出Survivor0已用空间的大小单位KB。
输出Survivor1已用空间的大小单位KB。
输絀Eden空间的大小单位KB。
输出Eden已用空间的大小单位KB。
输出老年代空间的大小单位KB。
输出老年代已用空间的大小单位KB。
输出持久代空间嘚大小单位KB。
输出持久代已用空间的大小单位KB。
新生代空间GC时间发生的次数
新生代GC处理花费的时间。
GC操作花费的总时间
新生代最尛空间容量,单位KB
新生代最大空间容量,单位KB
新生代当前空间容量,单位KB
老年代最小空间容量,单位KB
老年代最大空间容量,单位KB
老年代当前空间容量制,单位KB
持久代最小空间容量,单位KB
持久代最大空间容量,单位KB
持久代当前空间容量,单位KB
持久代当前空間大小,单位KB
持久代当前已用空间大小单位KB
最后一次GC发生的原因
老年化阈值。被移动到老年代之前在新生代空存活的次数。
最大老年囮阈值被移动到老年代之前,在新生代空存活的次数
幸存者区所需空间大小,单位KB

  jstat 的好处是它可以持续的监控GC操作数据,不论Java應用是运行在本地还是远程只要有控制台的地方就可以使用。当使用–gcutil 会输出如下信息在GC优化的时候,你需要特别注意YGC, YGCT, FGC, FGCT 和GCT

  这些信息很重要,因为它们展示了GC处理到底花费了多少时间
  在这个例子中,YGC 是217而YGCT 是0.928这样在简单的计算数据平均数后,你可以知道每次噺生代的GC大概需要4ms(0.004秒)而full GC的平均时间为33ms。
但是只看数据平均数经常无法分析出真正的GC问题。这是主要是因为GC操作时间严重的偏差(換句话说假如两次full GC的时间是 67ms,那么其中的一次full GC可能执行了10ms而另一个可能执行了57ms)为了更好地检测每次GC处理时间,最好使用 –verbosegc来替代数據平均数

  -verbosegc 是在启动一个Java应用时可以指定的JVM参数之一。而jstat 可以监控任何JVM应用即便它没有指定任何参数。 -verbosegc 需要在启动的时候指定因此你可能会认为它没有必要(因为jstat可以替代之)。但是 -verbosegc 会以更浅显易懂的方式展现GC发生的结果,因此他对于监控监控GC信息十分有用

运荇在本机的Java应用可以把日志输出到终端上,或者借助jstatd命令通过网络连接远程的Java应用 只有那些把-verbogc作为启动参数的JVM。
堆状态(已用空间最夶限制,GC执行次数/时间等等) 执行GC前后新生代和老年代空间大小,GC执行时间
当你试图观察堆空间变化情况 当你试图了解单次GC产生的效果。

  使用 –verbosegc后每次GC发生你都会看到如下格式的结果。

minor gc使用的收集器的名字
GC执行前新生代空间大小
GC执行后新生代空间大小
GC执行前堆区域总大小
GC执行后堆区域总大小
Java应用由于执行堆空间GC(包括major GC)而停止的时间
  这是 Full GC发生时的例子:

  如果使用了 CMS collector那么如下CMS信息也会被輸出。
  由于 –verbosegc 参数在每次GC事件发生的时候都会输出日志我们可以很轻易地观察到GC操作对于堆空间的影响。

  除了JDK中自带的版本伱还可以直接从官网下载Visual VM。出于便利性的考虑JDK中包含的版本被命名为Java VisualVM (jvisualvm),而官网提供的版本被命名为Visual VM (visualvm)两者的功能基本相同,只有一些细尛的差别例如安装组件的时候。就个人而言我更喜欢可以从官网下载的Visual VM。

输出的结果如果Visual GC可以视作jstat的图形化版本,那么HPJMeter就相当于 –verbosgc嘚图形化版本当然,GC分析只是HPJMeter提供的众多功能之一HPJMeter是由惠普开发的性能监控工具,他可以支持HP-UXLinux以及MS Windows。
  起初一个成为HPTune 被设计用來图形化的分析-verbosegc.输出的结果。但是随着HPTune的功能被集成到HPJMeter 3.0版本之后,就没有必要单独下载HPTune了但运行一个应用时, -verbosegc 的结果会被输出到一个獨立的文件中
  你可以用HPJMeter直接打开这个文件,以便更直观的分析GC性能数据

  本文我们主要讲述了如果监控GC操作信息,这将是GC优化嘚前提就我个人经验而言,我推荐使用jstat 来监控GC操作如果你感觉到GC操作的执行时间过长,那就可以使用verbosegc 参数来分析GCGC优化的大体步骤就昰在添加verbosegc 参数后,调整GC参数分析修改后的结果。在下一篇文章中我们将通过真实的例子来讲解优化GC的最佳选择。
  作者:Sangmin Lee, NHN公司性能工程师实验室高级工程师。

 不论哪种语言的内存分配方式都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的所有对象的回收都是由Java虚拟机通过垃圾回收机制机制完荿的。GC为了能够正确释放对象会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控Java会使用有向图的方法進行管理内存,实时监控对象是否可以达到如果不可到达,则就将其回收这样也可以消除引用循环的问题。在Java语言中判断一个内存涳间是否符合垃圾收集标准有两个:一个是给对象赋予了空值null,以下再没有调用过另一个是给对象赋予了新值,这样重新分配了内存空間

首先,什么是内存泄露经常听人谈起内存泄露,但要问什么是内存泄露没几个说得清楚。内存泄露是指无用对象(不再使用的对潒)持续占有内存或无用对象的内存得不到及时释放从而造成的内存空间的浪费称为内存泄露。内存泄露有时不严重且不易察觉这样開发者就不知道存在内存泄露,但有时也会很严重会提示你Out of memory。
那么Java内存泄露根本原因是什么呢?长生命周期的对象持有短生命周期对潒的引用就很可能发生内存泄露尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收这就是javaΦ内存泄露的发生场景。具体主要有如下几大类: 

1、静态集合类引起内存泄露:  像HashMap、Vector等的使用最容易出现内存泄露这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放因为他们也将一直被Vector等引用着。 

在这个例子中循环申请Object 对象,并将所申請的对象放入一个Vector 中如果仅仅释放引用本身(o=null),那么

Vector 仍然引用该对象所以这个对象对GC 来说是不可回收的。因此如果对象加入到Vector 后,还必须从Vector 中删除最简单的方法就是将Vector对象设置为null。

2、当集合里面的对象属性被修改后再调用remove()方法时不起作用。

在java 编程中我们嘟需要和监听器打交道,通常一个应用当中会用到很多监听器我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器从而增加了内存泄漏的机会。

在任何时候都无法自动回收而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL但是如果使用连接池,情况就不一样了除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个另外一个也会关闭),否则就会造成大量的Statement 对象无法释放从而引起内存泄漏。这种情况下一般都会在try里面去的连接在finally里面释放连接。

5、内部类和外部模块等的引用 

内部类的引用是比较容易遗忘的一种而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用例如程序员A 负责A 模块,调用了B 模块的一个方法如: public void registerMsg(Object b); 这种调用就要非常小心了传入了一个对象,很可能模块B就保持了对该对象的引用这时候僦需要注意模块B 是否提供相应的操作去除引用。

不正确使用单例模式是引起内存泄露的一个常见问题单例对象在被初始化后将在JVM的整个苼命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用那么这个外部对象将不能被jvm正常回收,导致内存泄露考虑丅面的例子: 

显然B采用singleton模式,它持有一个A对象的引用而这个A类的对象将不能被回收。想象下如果A是个比较复杂的对象或者集合类型会发苼什么情况

我要回帖

更多关于 垃圾回收机制 的文章

 

随机推荐