浅析JAVA之垃圾回收机制

导论:

对于JAVA编程和很多类似C、C++语言有一个巨大区别就是内存不需要自己去free或者delete,而是由JVM垃圾回收机制去完成的。对于这个过程很多人一直比较茫然或者觉得很智能,使得在写程序的过程不太考虑它的感受,其实知道一些内在的原理,帮助我们编写更加优秀的代码是非常有必要的。

本文从以下几个方面进行阐述:
1、finalize()方法
2、System.gc()方法及一些实用方法
3、JAVA如何申请内存,和C、C++有何区别

4、JVM如何寻找到需要回收的内存
5、JVM如何回收内存的(回收算法分解详述)
6、应用服务器部署及常用参数设置
7、扩展话题JIT(及时编译技术)与lazy evaluation(惰性评估)



1、finalize()方法:
    为了说明JVM回收,不得不先说明一个问题就是关于finalize()方法,所有实体对象都会有这个方法,因为这个Object类定义的,这个可能会被认为是垃圾回收的方法或者叫做析构函数,其实并非如此。finalize在JVM内存回收前会被调用(但并非绝对),而即使不调用它,JVM回收机制通过后面所述的一些算法就可以定位哪些是垃圾内存,那么这个拿来干什么用呢?
 finalize()其实是要做一些特殊的内存回收操作,如果对JAVA研究稍微多一点,大家会发现JAVA中有一种JNI的机制,即:Java native interface,这种属于JAVA本地接口调用,即调用本地的其他语言信息,JAVA虚拟机底层掉调用也是这样实现的,这部分调用中可能存在一些对C、C++语言的操作,在C和C++内部通过new、malloc、realloc等关键词创建的对象垃圾回收机制是无能为力的,因为这不是它要管理的范围,而平时这些对象可能被JAVA对应的实体所调用,那么需要在对应JAVA对象放弃时(并不代表回收,只是程序中不使用它了)去调用对应的C、C++提供的本地接口去释放这段内存信息,他们的释放同样需要通过free或delete去释放,所以我们一般情况下不要滥用finalize(),可能你会联想到另一类某些特殊引用对象的释放,如层数引用太多,JAVA虚拟机有些时候不知道这一线的对象是否都可能被回收那么,你可以自己将finalize()重写,并将内置对象的句柄先释放掉,这样也是没有问题的,不过一般不要滥用而已。

2、System.gc()或者Runtime.getRuntime().gc();

  这个可以被认为是强制垃圾回收的一种机制,但是并非强制回收,只是向JVM建议可以进行垃圾回收,而且垃圾回收的地方和多少是不能像C语言一样控制,这是JVM垃圾回收机去控制的。程序中尽量不要是去使用这些东西,除自己开发一些管理代码除外,一般由JVM自己管理即可。

这里顺便提及几个查看当前JVM内存的几个方法(在同一个集群下的多个SERVER,即使在同一个机器上通过下面方法只能查看到当前SERVER下的内存情况):

2.1.设置的最大内存:-Xmx等值:

(Runtime.getRuntime().maxMemory()/ (1024 * 1024)) + "MB"

2.2.当前JVM可使用的内存,这个值初始化和-Xms等值,若加载东西超过这个值,那么以下值会跟着变大,不过上限为-Xmx,由于变动过程中需要由虚拟机向操作系统申请新的内存,所以存在不连续内存以及影响开销,很多时候应用软件服务器提供商(如BEA)推荐是-Xms等价于-Xmx的值:

(Runtime.getRuntime().totalMemory()/ (1024 * 1024)) + "MB"

2.3.剩余内存,在当前可使用内存基础上,剩余内存等价于其剪掉使用了的内存容量:

(Runtime.getRuntime().freeMemory()/ (1024 * 1024)) + "MB"

同理如果要查看使用了多少内存或者百分比。可以通过上述几个参数进行运算查看到。。。。

顺便在这里提供几个实用方法和类,这部分可能和JVM回收关系不大,不过只是相关推敲,扩展知识面,而且也较为实用的东西:

2.4.获取JAVA中的所有系统级属性值(包含虚拟机版本、操作系统、字符集等等信息):

System.setProperty("AAA", "123445");

Properties properties = System.getProperties();
 Enumeration<Object> e = properties.keys();
 while (e.hasMoreElements()) {
  String key = (String) e.nextElement();
  System.out.println(key + " = " + properties.getProperty(key));
}


2.5.获取系统中所有的环境变量信息:

Map<String, String> env = System.getenv();
 for (Iterator<String> iterator = env.keySet().iterator(); iterator.hasNext();) {
  String key = iterator.next();
  System.out.println(key + " = " + env.get(key));
}

System.out.println(System.getenv("CLASSPATH"));

2.6.在Win环境下,打开一个记事本和一个WORD文档:

try {

 Runtime.getRuntime().exec("notepad");

 Runtime.getRuntime().exec("cmd /c start Winword");

}catch(Exception e) {

 e.printStackTrace();

}

2.7.查询当前SERVER下所有的线程信息列表情况(这里需要提供两个步骤,首先要根据任意一个线程获取到顶级线程组的句柄(有关线程的说明,后面专门会有一篇文章说明),然后通过顶级线程组得到其存在线程信息,进行一份拷贝,给与遍历):

2.7.1.这里通过当前线程得到顶级线程组信息:

public static ThreadGroup getHeadThreadGroup() {
 Thread t = Thread.currentThread();
 ThreadGroup group = t.getThreadGroup();
 while(group.getParent() != null) {
  group = group.getParent();
 }
 return group;
}

2.7.2.通过得到的顶级线程组,遍历存在的子元素信息(仅仅遍历常用属性):

public static void disAllThread(ThreadGroup threadgroup) {
 Thread list[] = new Thread[threadgroup.activeCount()];
 threadgroup.enumerate(list);
 for(Thread thread:list) {
  System.out.println(thread.getId()+"\t"+thread.getName()

                             +"\t"+thread.getThreadGroup()+"\t"

                             +thread.getState()+"\t"+thread.isAlive());}
}

2.7.3.测试方法如:

类名.disAllThread(getHeadThreadGroup());即可完成,第一个方法带有不断向上查询的过程,这个过程可能在一般情况下也不会太慢,不过我们最好将其记录在一个地方,方便我们提供管理类来进行直接管理,而不需要每次去获取,对外调用都是封装的运行过程而已。

好,回到话题,继续说明JVM垃圾回收机制的信息,下面开始说明JAVA申请内存、回收内存的机制了。

3、JAVA如何申请内存,和C、C++有何区别。

  在上一次缩写的关于JAVA集合类文章中其实已经有部分说明,可以大致看到JAVA内部是按照句柄指向实体的过程,不过这是从JAVA程序设计的角度去理解,如果我们需要更加细致的问一个问题是:JVM垃圾回收机制是如何知道哪些内存是垃圾内存的?JVM为什么不在平时就去回收内存,而是要等到内存不够用的时候才会去回收内存?不得不让我进一步去探讨JAVA是如何细节的申请内存的。

从编程思想的角度来说,C、C++new申请的内存也是通过指针指向完成,不过你可以看成是一个地球板块图,在这些板块中,他们去new的过程中,就是好比是找一个版块,因为C、C++在申请内存的过程中,是不断的free和delete操作,所以会产生很多内存的碎片操作,而JAVA不是,JAVA只有内存不够用的时候才会去回收(回收细节讲会在文章后面介绍),也就是说,可以保证内存在一定程度上是连续的。从某种意义上将,只要下一块申请的内存不会到头,就可以继续在上一块申请内存的后面紧跟着去申请内存,那么从某种意义上讲,其申请的开销可能可以和C++媲美。那么JAVA在回收内存后,内存还能是连续的嘛。。。。我们姑且这样去理解,在第五节会说明。。继续深入话题:

在启动weblogic的时候,如果打开任务管理器,可以马上发现,内存被占用了最少-Xms的大小,一个说明现象就是JVM首先将内存先占用了,然后再分配给其对象的,也就是说我们所谓的new可以理解为在堆上做了一个标记,所以在一定程度上做连续分配内存是可以实现的,只是你会发现若要真正实现连续,必然导致一定程度上的序列化,所以new的开销一般还是蛮大的,即使在后面说的JVM会将内存分成几个大块来完成操作,但是也避免不了序列化的过程。

在这里一个小推敲就是,一个SERVER的管理内存范围一般不要太大(一般在1~2G一个SERVER),推荐也不要太大,因数去考虑:

1、JAVA虚拟机回收内存是在不够用的时候再去回收,这个不够用何以说明,很多时候因为计算上的失误导致内存溢出。

2、如果一个主机只有2G左右内存,很少的CPU,那么一个JVM也好,但是如果主机很好,如32G内存,那么这样做未必有点过,第一发挥不出来,一个JVM管这么大块内存好像有点过,还有内存不够用去回收这么大块内存(回收内存时一般需暂停服务),需要花时间,第二举个很现实的例子,一个学校如果只有20~30人,一个人可以既当校长又当老师,如果一个学校有几百上千人,我想这个人再大的能力忙死也管不过来,而且会出乱子,此时它要请班主任来管了。

3、对于大内存来说,使用多个SERVER完成负载均衡,一个暂停服务回收内存,另一个还可以运行嘛。

4、JVM如何寻找到需要回收的内存:

4.1、引用计数算法:在早期的JAVA虚拟机中,他们采用思维上最为常用的算法,就是计数器,在对象被使用“=”给与句柄的过程中(在集合类内部虽然外部调用通过add或者put完成,不过内部仍然是这样),它同时会告诉JAVA虚拟机,这个对象被增加一次引用(注意一个句柄如果已经存在指向的实体,此时指向另一个实体,它也会同时告诉JAVA虚拟机以前指向那个实体少了一个引用,不然这就乱了),当你使用句柄=null的时候,它会告诉JAVA虚拟机这个指向的实体少了一个引用,这个计数器并不是记录在实体本省,而是被JVM私有化管理起来,这部分也是JVM垃圾回收机的基础信息(私有化管理部分为JVM中的永久域,为所有静态常量的管理池,如:public class的代码段、static代码段、static变量、String常量数组的一份拷贝等等),JVM回收内存的时候,就会去找计数器中位0的元素,将其回收(回收过程,在第五节说明),如果有级联引用的,如果父亲级别的引用被回收后,子对象的引用数会自动减少1。

问题出来了:

对于A对象有B对象的引用、B对象有A对象的引用,如果两个引用都没有程序员去手动去=null操作,那么实用引用计数算法,将永远计算不出来他们是需要被回收的内存,这就是一种在使用引用计数器中的JAVA内存泄露问题(对于这类内存泄露属于引用计数上,使用树结构遍历是没有问题的,同样在引用计数算法上,存在多层集合类级联引用的问题可能不会被回收到;另一类是流的内存泄露(连接的内存泄露也属于其中),对于这类对象,进行特殊的管理,内部有一个管理器,如连接数据有一个DriverManager,他们永远都保存着对连接的引用,如果直接使用JDBC操作,使用完然后做close操作,相当于告诉连接管理器断开并可以释放连接对象,反过来说,如果不做Close操作,系统永远不知道这块资源信息是垃圾内存,永远不会回收它,直到资源耗尽内存溢出为止,直接将句柄=null,对于连接对象是无效)。

这个引用计数需要遍历整块JVM才知道哪些需要回收,哪些不需要回收,太慢了。。。。

4.2.树结构遍历算法:其实为什么JAVA虚拟机可以管理其区域下的内存,因为我们是在JVM内部去创建内存,所以可以理解为打一个TAG,那么它必然有一个根引用(静态区)通过分类的管理机制遍历到所使用的每一个堆空间中,此时这棵很大的树上,进行遍历下来得到的全部是活着的结点。那么没有被遍历到的全部是没有活着的结点,对于大量内存需要回收的情况(很多情况下是的,因为业务级的请求用完这块内存就没用了),我们很快可以知道哪些内存是活着的,在后面回收时对应其快速复制的过程,另一个区域就作为自动作为垃圾内存了,在JDK1.4.2后开始逐步提出并行回收算法(包含了并行遍历算法,内部包含了后面说的需要将内存分块管理,而并非完全是一个整体)。

    现在的JVM根据实际情况会采用一种自适应的算法去寻找垃圾内存,它会按照上述两种算法进行分别管理,当发现树结构的开销较大的时候(大部分是不需要回收的内存,由于树的遍历要么通过递归消耗代码段并在时间开销上很大以外或者利用一个缓冲区来遍历,比顺序遍历要慢很多,这种情况其实很少,如果真是这样,内存很容易溢出),所以此时它会自适应的去采用引用计数算法去找需要回收的内存部分。

5、JVM如何回收内存的(回收算法分解详述):

首先了解几个其他的概念:

5.1.平时所说的JDK,其实是JAVA开发工具的意思,安装JAVA虚拟机会产生两个JRE目录,JRE目录为JAVA运行时环境的意思,两个JRE目录的区别是其中在JDK所在的JRE目录下没有Server和Client文件夹(JDK1.5自动安装包会自动将其复制到JDK下面一份),JRE为运行时环境,提供对JVM操作的API,JVM内部通过动态链接库(就是配置PATH的路径下),通过它作为主动态链接库寻找到其它的动态链接库,动态链接库为何OS绑定的参数,即代码最终要通过这些东西转换为X86指令集进行运行,另一个核心工具为JIT(JAVA即时编译工具),用于将代码转换为对应操作系统的运行指令集合的过程,不过其与惰性评估形成对比,后面会专门介绍。

5.1.JVM首先将大致分为:JVM指令集、JVM存储器、JVM内存(堆栈区域部分)、JVM垃圾回收区域;JVM的堆部分又一般分为:新域、旧域、永久域(很多时候不会认为段是堆的一部分,因为它是永远不会被回收的,它一般包含class的定义信息、static定义的方法、static匿名块代码段、常量信息(较为典型的就是String常量),不过这块内存也是可以被配置的);新域内部又可以分为Eden和两个救助区域,这几个对象在JVM内部有一定的默认值,但是也是可以被设置的。

   当新申请的对象的时候,会放入Eden区中(这个区域一般不会太大),当对象在一定时间内还在使用的时候,它会逐步的进入旧域(此时是一个内存复制的过程,旧区域按照顺序,其引用的句柄也会被修改指向的位置),JVM回收中会先将Eden里面的内存和一个救助区域的内存就会被赋值到另一个救助区域,然后对这两块内存进行回收,同理,旧区域也有一个差不多大小的内存区域进行被复制,这个复制的过程肯定就会在一定程度上将内存连续的排列起来;另外可以想到JAVA提供内存复制最快的就是System.arrayCopy方法,那么这个肯定是按照内存数组进行拷贝(JVM起始就是一个大内存,本身就可以成是几个大数组组成的,而这个拷贝方法,默认拷贝多长呢,其实数组最长可以达到多少,通过数组的length返回的是int类型数据就可以清楚发现,为int类型的上限1<<30的长度(理想情况,因为有可能因为操作系统的其他进程导致JVM内存本身就不是连续的),即在2G*单元内存长度,所以也在一定程度上说明我们的一个JVM设置内存不要太大,不然复制内存的过程开销是很大的)。

   其实上述描述的是一种停止-复制回收算法,在这个过程中形成了几个大的内存来回倒,这必然是很恶心的事情,那么继续将其切片为几个大的板块,有些大的对象会出现一两个对象占用一个版块的现象,这些大对象基本不会怎么移动(被回收就是另一回事,因为会清空这个版块),板块之间有一些对应关系,在回收时先将一些版块的小对象,向另一个还未装满的大板块内部转移,复制的粒度变小了,另外管理上可以发挥多线程的优势所在,好比是将一块大的田地,分成很多小田地,每块田地种植不同档次的秧苗,将其划分等级,我们假如秧苗经常会随机的死掉一些(这块是垃圾),在清理一些很普通的秧苗田地的时候,可能会将其中一块或几块田地的(活着的秧苗)种植到另一块田地中,但是他们不可以将高档次的秧苗移植到低档次的田地中,因为高档次的秧苗造价太高(内存太大),移植过程中代价太大,需要使用非普通秧苗的手段要移动他们,所以基本不移动他们,除非丰收他们的时候(他们也成为垃圾内存的时候),才会被拔出,腾出田地来。在转移秧苗的过程中,他们需要整理出顺序便于管理,在很多书籍上把这个过程叫做压缩,因为这样使得保证在只要内存不溢出的情况下,申请的对象都有足够的控件可以存放,不然零碎的空间中间的缝隙未必可以存放下一个较大的对象。将内存分块管理就是另一个停止复制收集器的进一步升级:增量收集思想。

5.2.回收过程,垃圾回收机制一般分为:标记—清除、标记—压缩、停止—复制、增量收集、分代收集、并发收集和并行收集。

标记清除收集器,一般依赖于第一种寻找垃圾的算法去寻找,当寻找到需要使用的内存,会打一个TAG,此时未标记的对象,就会被该收集器回收,一般先停止外部服务,并且是单线程的去寻找并清除。

标记压缩收集器,和上述收集器唯一区别就是多一步压缩操作,压缩操作删除前或删除后去操作是将标记为正在使用的对象复制到一块新的内存中,也就是大致上是按照顺序排列的。

停止复制收集器、增量收集器上述描述的较多,这里就不再多说了,总之是来回倒这些内存,为了介于内存,在其基础上按照一定规则进行内存分块操作。

并发收集器,这里一定要明确并发和并行的概念不同之处,并发收集器是可以再内存回收的过程中不暂停服务,也就是不影响运行,类似上述的收集器,要进行压缩空间等操作不得不暂停服务保证系统的正常运行;可以思考到它基本会建立在首先将内存分块的基础上,可能会更细,它独立的运行并和应用程序同时运行,回收的方式也是和上面一样,只是它复制的时候只是较小部分内存的复制,所以其他业务系统运行照常,粒度很小,最好的情况就是这块内存99%是需要回收的,它正在回收这块内存,关于剩下的1%的内存应用服务被暂停,其余其它块的内存照常运行不受到影响。

并行收集器是使用上述某类收集方法,但是使用多线程算法进行回收,在服务器应用中,利用多CPU进行回收可以显著提高性能,此时需要进行相关的配置,在第六节中有详细的说明。

对于上述了解后,可能对于不同的应用服务器有不同的JVM垃圾查找算法和回收算法,但是大致不离其中,根据实际情况,进行服务器的相关调试就可以在一定程度上提高服务器的运行性能,第六节就详细说明下。

6、应用服务器部署及常用参数设置:

说到JVM的配置,最常用的两个配置就是:

-Xms512m –Xmx1024m

-Xms设置JVM的初始化内存大小,-Xmx为最大内存大小,当突破这个值,将会报内存溢出,导致的原因有很多,主要是虚拟机的回收问题以及程序设计上的内存泄露问题;由于在超过-Xms时会产生页面申请的开销,所以一般很多应用服务器会推荐-Xms和-Xmx是等值的;最大值一般不保持在主机内存的75%的内存左右(多个SERVER是加起来的内存),当JVM绝大部分时间处于回收状态,并且内存长时间处于非常长少的状态就会报:java.lang.OutOfMemoryError:Java heap space的错误。

上面提及到JVM很多的知识面,很显然你想去设置一下其它的参数,其实对于JVM设置的参数有上百个,这里就说一些较为常用配置即可。

JVM内存配置分两大类:

1、-X开头的参数信息:一般每个版本变化不大。

2、-XX开头的参数信息:版本升级变化较大,如果没有太大必要保持默认即可。

3、另外还有一个特殊的选项就是-server还是-client,他们在默认配置内存上有一些细微的区别,直接用JDK运行程序默认是-client,应用服务器生产模式一般只会用-server。

这些命令其实就是在运行java命令或者javaw等相关命令后可以配置的参数,如果不配置,他们有相应的默认值配置。

1、-X开头的常用配置信息:

-Xnoclassgc  禁用垃圾回收,一般不适用这个参数

-Xincgc       启用增量垃圾回收

-Xmn1024K   Eden区初始化JAVA堆的尺寸,默认值640K

-Xms512m    JAVA堆初始化尺寸,默认是32M

-Xmx512m    JAVA堆最大尺寸,默认64M,一般不超过2G,在64位机上,使用64位的JVM,需要操作系统进行unlimited方可设置到2G以上。

2、-XX开头常用内存配置信息:

-XX:-DisableExplicitGC  将会忽略手动调用GC的代码,如:System.gc(),将-DisableExplicitGC,   改成+DisableExplicitGC即为启用,默认为启用,什么也不写,默认是加号,但是系统内部默认的并不是什么都启用。

-XX:+UseParallelGC     将会自动启用并行回收,多余多CPU主机有效,默认是不启用。

-XX:+UseParNewGC     启用并行收集(不是回收),也是多CPU有效。

-XX:NewSize=128m     新域的初始化尺寸。

-XX:MaxNewSize=128m新创建的对象都是在Eden中,其属于新域,在-client中默认为640K,而-server中默认是2M,为减少频繁的对新域进行回收,可以适当调大这个值。

-XX:PerSize=64m        设置永久域的初始化大小,在WEBLOGIC中默认的尺寸应该是48M,一般够用,可以根据实际情况作相应条调整。

-XX:MaxPerSize=64m    设置永久域的最大尺寸。

另外还可以设置按照区域的比例进行设置操作,以及设置线程、缓存、页面大小等等操作。

3、-XX开头的几个监控信息:

-XX:+GITime              显示有多少时间花在编译代码代码上,这部分为运行时编译为对应机器码时间。

-XX:+PrintGC              打印垃圾回收的基本信息

-XX:+PrintGCTimeStamps 打印垃圾回收时间戳信息

-XX:+PrintGCDetails       打印垃圾回收的详细信息

-XX:+TraceClassLoading    跟踪类的加载

-XX:+TraceClassResolution 跟踪常量池

-XX:+TraceClassUnLoading 跟踪类卸载

等等。。。。。。

例子:

编写一个简单的JAVA类:

public class Hello {

    public static void main(String []args) {

        byte []a1 = new byte[4*1024*1024];

        System.out.println("第一次申请");

        byte []a2 = new byte[4*1024*1024];

        System.out.println("第二次申请");

        byte []a3 = new byte[4*1024*1024];

        System.out.println("第三次申请");

        byte []a4 = new byte[4*1024*1024];

        System.out.println("第四次申请");

        byte []a5 = new byte[4*1024*1024];

        System.out.println("第五次申请");

        byte []a6 = new byte[4*1024*1024];

        System.out.println("第六次申请");

    }

}

此时运行程序,这样调试一下:

C:\>java -Xmn4m -Xms16m -Xmx16m Hello

第一次申请

第二次申请

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

此时内存溢出,因为这些空间都有释放,16M空间正好一半8M就溢出了,和我们的理论较吻合的就是一半在使用一半的大小就会崩掉,显示的使用值会成倍增加。

那么我们将程序修改一下再看效果:

public class Hello {

    public static void main(String []args) {

        byte []a1 = new byte[4*1024*1024];

        System.out.println("第一次申请");

        a1 = new byte[4*1024*1024];

        System.out.println("第二次申请");

        a1 = new byte[4*1024*1024];

        System.out.println("第三次申请");

        a1 = new byte[4*1024*1024];

        System.out.println("第四次申请");

        a1 = new byte[4*1024*1024];

        System.out.println("第五次申请");

        a1 = new byte[4*1024*1024];

        System.out.println("第六次申请");

    }

}

运行程序如下:

C:\>javac Hello.java

C:\>java -Xmn4m -Xms16m -Xmx16m Hello

第一次申请

第二次申请

第三次申请

第四次申请

第五次申请

第六次申请

程序正常下来了,说明中途进行了垃圾回收的动作,我们想看下垃圾回收的整个过程,如何看,把上面的参数搬下来:

E:\>java -Xmn4m -Xms16m -Xmx16m -XX:+PrintGCDetails Hello

第一次申请

第二次申请

[GC [DefNew: 189K->133K(3712K), 0.0014622 secs][Tenured: 8192K->4229K(12288K), 0.0089967 secs] 8381K->4229K(16000K), 0.0110011 secs]

第三次申请

[GC [DefNew: 0K->0K(3712K), 0.0004749 secs][Tenured: 8325K->4229K(12288K), 0.0083114 secs] 8325K->4229K(16000K), 0.0092936 secs]

第四次申请

[GC [DefNew: 0K->0K(3712K), 0.0003168 secs][Tenured: 8325K->4229K(12288K), 0.0081516 secs] 8325K->4229K(16000K), 0.0089735 secs]

第五次申请

[GC [DefNew: 0K->0K(3712K), 0.0003179 secs][Tenured: 8325K->4229K(12288K), 0.0080368 secs] 8325K->4229K(16000K), 0.0088335 secs]

第六次申请

从上面的结果中看到第一次回收是从两个对象申请后,开始回收,后面是每申请一个就回收一次,那是因为,程序代码中始终用一个句柄指向了一个空间,第一次回收的时候只回收了一个对象,还有一个对象没有回收,而当后面申请对象过程中,没申请一个对象以前那个对象就是垃圾,而且内存又满了,所以内存就会在每申请一个对象的时候被回收。

把程序稍微修改一下,增加两个申请得较大的内存,因为新区域的回收从上一个试验中看不出来:

public class Hello {

    public static void main(String []args) {

        byte []a1 = new byte[4*1024*1024];

        System.out.println("第一次申请");

        a1 = new byte[4*1024*1024];

        System.out.println("第二次申请");

        a1 = new byte[4*1024*1024];

        System.out.println("第三次申请");

        a1 = new byte[4*1024*1024];

        System.out.println("第四次申请");

        a1 = new byte[4*1024*1024];

        System.out.println("第五次申请");

        a1 = new byte[4*1024*1024];

        System.out.println("第六次申请");

        a1 = new byte[12*1024*1024];

        System.out.println("第七次申请(大三倍的内存)");

        a1 = new byte[12*1024*1024];

        System.out.println("第八次申请(大三倍的内存)");

    }

}

E:\>javac Hello.java

--先将新区域设置为4m看下结果:

E:\>java -Xmn4m -Xms32m -Xmx32m -XX:+PrintGCDetails Hello

第一次申请

第二次申请

第三次申请

第四次申请

第五次申请

第六次申请

[GC [DefNew: 189K->133K(3712K), 0.0015527 secs][Tenured: 24576K->4229K(28672K), 0.0091076 secs] 24765K->4229K(32384K), 0.0111978 secs]

第七次申请(大三倍的内存)

[GC [DefNew: 0K->0K(3712K), 0.0005428 secs][Tenured: 16517K->12421K(28672K), 0.0131774 secs] 16517K->12421K(32384K), 0.0142610 secs]

第八次申请(大三倍的内存)

--再将新域的大小扩大一倍:

E:\>java -Xmn8m -Xms32m -Xmx32m -XX:+PrintGCDetails Hello

第一次申请

[GC [DefNew: 4362K->133K(7424K), 0.0049861 secs] 4362K->4229K(32000K), 0.0053091 secs]

第二次申请

[GC [DefNew: 4229K->133K(7424K), 0.0043679 secs] 8325K->8325K(32000K), 0.0047056 secs]

第三次申请

[GC [DefNew: 4229K->133K(7424K), 0.0044629 secs] 12421K->12421K(32000K), 0.0047791 secs]

第四次申请

[GC [DefNew: 4229K->133K(7424K), 0.0044263 secs] 16517K->16517K(32000K), 0.0047646 secs]

第五次申请

[GC [DefNew: 4229K->133K(7424K), 0.0045198 secs] 20613K->20613K(32000K), 0.0048372 secs]

第六次申请

[GC [DefNew: 4229K->4229K(7424K), 0.0001604 secs][Tenured: 20480K->4229K(24576K), 0.0092190 secs] 24709K->4229K(32000K), 0.0098943 secs]

第七次申请(大三倍的内存)

[GC [DefNew: 0K->0K(7424K), 0.0005219 secs][Tenured: 16517K->12421K(24576K), 0.0131838 secs] 16517K->12421K(32000K), 0.0142096 secs]

[Full GC [Tenured: 12421K->12420K(24576K), 0.0115076 secs] 12421K->12420K(32000K), [Perm : 338K->337K(8192K)], 0.0123320 secs]

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

  尽然内存溢出了,怎么扩大了内存溢出了?难道新域不能设置得太大?那么为了论证,我们将新域的大小再放大一倍:

E:\>java -Xmn16m -Xms32m -Xmx32m -XX:+PrintGCDetails Hello

第一次申请

第二次申请

第三次申请

[GC [DefNew: 12551K->133K(14784K), 0.0052593 secs] 12551K->4229K(31168K), 0.0055915 secs]

第四次申请

第五次申请

第六次申请

[GC [DefNew: 12556K->12556K(14784K), 0.0001953 secs][Tenured: 4096K->4229K(16384K), 0.0091847 secs] 16652K->4229K(31168K), 0.0098906 secs]

第七次申请(大三倍的内存)

[GC [DefNew: 12288K->12288K(14784K), 0.0001972 secs][Tenured: 4229K->12421K(16384K), 0.0148896 secs] 16517K->12421K(31168K), 0.0155777 secs]

第八次申请(大三倍的内存)

为什么又不内存溢出了,所以这个设置和绝对是否大和绝对是否小几乎无关,而是和实际情况的对象的关系,我们推算一下:

前面每次理想申请堆空间是4M(新域内部还包含救助区域,所以直接放不下了),这个申请直接就进入旧域才能放下,所以在新区域申请4m的过程中,旧区域空间又28m空间,前6次申请空间的时候,是4*6=24M接近位数,此时第七次申请空间时候就需要回收内存,可以看到回收的全部是旧域的内存,新域的内存没有什么变化。

而当新域设置为8M的时候,旧域只有24M,第一个4M显然能放下去,第二个4M不行了,就要将前面那个转移到旧域内部,这个转移过程并不会去做GC的操作,而是不断的将信息转移过去,而这些内存属于垃圾,后面会不断从新域向旧区域转移数据,但是这个过程中不会回收信息,和上述一样,不过当一个12M的数据下来的时候,此时前面会有最多两个对象及8M的内容在新区域内部(但是通过试验论证最多只能又一个在新域,因为新域虽然有8M但是实际拿给Eden用的肯定没有这么多),此时也就是说旧域使用的内存应该是16M或者20M的空间,但是此时JVM认为还没有到必须要回收内存的地步,如果进来一个4M以内的内存,JVM启动内存马上回收,即可将垃圾内存回收掉;而进来一个12M的内存,整个JVM新域+旧区域也放不下那么大的内存了,所以会报内存溢出。同理可以分析设置为16M的情况。

在设置过程中并不是设置大或者小有好处,根据实际情况而定如何去控制,默认情况下,新域的大小为旧域的二分之一(即堆空间的三分之一),由参数:-XX:NewRatio的值来设置其比例关系,默认值为2

知识共享许可协议
《浅析JAVA之垃圾回收机制》常伟华 创作。
采用 知识共享 署名-相同方式共享 3.0 中国大陆 许可协议进行许可。
  • 多说评论
  • 签名
  • 新浪微博
  • 默认评论
  • Tab Header 5

0 条评论 / 点击此处发表评论

Tab Content 5

Warning: file_get_contents(http://ip.taobao.com/service/getIpInfo.php?ip=3.237.94.109) [function.file-get-contents]: failed to open stream: ӷһʱûȷ𸴻ӵûзӦӳʧܡ in D:\wwwroot\changweihua\wwwroot\v\content\templates\darklight\vrecord.php on line 129

开发技术


开发平台和工具

sitemap     21053.65ms