JAVA之垃圾回收机制.docx
- 文档编号:8300306
- 上传时间:2023-01-30
- 格式:DOCX
- 页数:16
- 大小:30.14KB
JAVA之垃圾回收机制.docx
《JAVA之垃圾回收机制.docx》由会员分享,可在线阅读,更多相关《JAVA之垃圾回收机制.docx(16页珍藏版)》请在冰豆网上搜索。
JAVA之垃圾回收机制
浅析JAVA之垃圾回收机制
分类:
JAVA应用开发JVM2010-05-2222:
03401人阅读评论
(2)收藏举报
对于JAVA编程和很多类似C、C++语言有一个巨大区别就是内存不需要自己去free或者delete,而是由JVM垃圾回收机制去完成的。
对于这个过程很多人一直比较茫然或者觉得很智能,使得在写程序的过程不太考虑它的感受,其实知道一些内在的原理,帮助我们编写更加优秀的代码是非常有必要的;本文介绍一些JVM垃圾回收的基本知识,后续的文章中会深入探讨JVM的内在;首先在看文章之前大家需要知道为什么要写JVM垃圾回收,在Java发展以来,由于需要面向对象,而屏蔽掉程序员对于底层的关心,所以在性能上存在很多的缺陷,而通过不断改良,很多缺陷已经逐渐的取消掉了,不过还是依然存在很多的问题,其中最大的一块问题就是JVM的垃圾回收机制,一直以来Java在设计实时系统上都被骂声重重,就是因为垃圾回收存在非常多的问题,世界上目前还没有任何一个垃圾回收机制可以做到无暂停,而只是某些系统可以做到非常少的暂停;本文还不会讨论那么深入的只是,就简单的内部认识做一些概要性的介绍。
本文从以下几个方面进行阐述:
1、finalize()方法
2、System.gc()方法及一些实用方法
3、JAVA如何申请内存,和C、C++有何区别
4、JVM如何寻找到需要回收的内存
5、JVM如何回收内存的(回收算法分解详述)
6、应用服务器部署及常用参数设置
7、扩展话题JIT(即时编译技术)与lazyevaluation(惰性评估),如何在应用服务器中控制一些必要的信息(小小代码参考)
1、finalize()方法:
为了说明JVM回收,不得不先说明一个问题就是关于finalize()方法,所有实体对象都会有这个方法,因为这个Object类定义的,这个可能会被认为是垃圾回收的方法或者叫做析构函数,其实并非如此。
finalize在JVM内存会收前会被调用(单并非绝对),而即使不调用它,JVM回收机制通过后面所述的一些算法就可以定位哪些是垃圾内存,那么这个拿来干什么用呢?
finalize()其实是要做一些特殊的内存回收操作,如果对JAVA研究稍微多一点,大家会发现JAVA中有一种JNI(Javanativeinterface),这种属于JAVA本地接口调用,即调用本地的其他语言信息,JAVA虚拟机底层掉调用也是这样实现的,这部分调用中可能存在一些对C、C++语言的操作,在C和C++内部通过new、malloc、realloc等关键词创建的对象垃圾回收机制是无能为力的,因为这不是它要管理的范围,而平时这些对象可能被JAVA对应的实体所调用,那么需要在对应JAVA对象放弃时(并不代表回收,只是程序中不使用它了)去调用对应的C、C++提供的本地接口去释放这段内存信息,他们的释放同样需要通过free或delete去释放,所以我们一般情况下不要滥用finalize(),个人建议是最好不要用,所有非同类语言的调用不一定非要通过JNI来完成的,或者调用完就直接释放掉相应的内容,而不要寄希望于finalize这个方法,因为JVM不保证什么时候会调用这个方法。
2、System.gc()或者Runtime.getRuntime().gc();
这个可以被认为是强制垃圾回收的一种机制,但是并非强制回收,只是向JVM建议可以进行垃圾回收,而且垃圾回收的地方和多少是不能像C语言一样控制,这是JVM垃圾回收机去控制的。
程序中尽量不要是去使用这些东西,除自己开发一些管理代码除外,一般由JVM自己管理即可。
这里顺便提及几个查看当前JVM内存的几个简单代码方法(在JVM监控下有很多的工具,而且不同的厂商也有自己不同的工具,不过后续大部分关于java的文章都是只提及到:
HotspotVM的版本,其他的版本可能只是略微说明下):
2.1.设置的最大内存:
-Xmx等值:
(Runtime.getRuntime().maxMemory()/(1024*1024))+"MB"
2.2.当前JVM可使用的内存,这个值初始化和-Xms等值,若加载东西超过这个值,那么以下值会跟着变大,不过上限为-Xmx,由于变动过程中需要将虚拟内存做不断的伸缩过程,所以我们推荐服务器:
是-Xms等价于-Xmx的值:
(Runtime.getRuntime().totalMemory()/(1024*1024))+"MB"
2.3.剩余内存,在当前可使用内存基础上,剩余内存等价于其剪掉使用了的内存容量:
(Runtime.getRuntime().freeMemory()/(1024*1024))+"MB"
同理如果要查看使用了多少内存或者百分比。
可以通过上述几个参数进行运算查看到。
。
。
。
顺便在这里提供几个实用方法和类,这部分可能和JVM回收关系不大,不过只是相关推敲,扩展知识面,而且也较为实用的东西:
2.4.获取JAVA中的所有系统级属性值(包含虚拟机版本、操作系统、字符集等等信息):
System.setProperty("AAA","123445");
Propertiesproperties=System.getProperties();
Enumeration
while(e.hasMoreElements()){
Stringkey=(String)e.nextElement();
System.out.println(key+"="+properties.getProperty(key));
}
2.5.获取系统中所有的环境变量信息:
Map
for(Iterator
.hasNext();){
Stringkey=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/cstartWinword");
}catch(Exceptione){
e.printStackTrace();
}
2.7.查询当前SERVER下所有的线程信息列表情况(这里需要提供两个步骤,首先要根据任意一个线程获取到顶级线程组的句柄(有关线程的说明,后面专门会有一篇文章说明),然后通过顶级线程组得到其存在线程信息,进行一份拷贝,给与遍历):
2.7.1.这里通过当前线程得到顶级线程组信息:
publicstaticThreadGroupgetHeadThreadGroup(){
Threadt=Thread.currentThread();
ThreadGroupgroup=t.getThreadGroup();
while(group.getParent()!
=null){
group=group.getParent();
}
returngroup;
}
2.7.2.通过得到的顶级线程组,遍历存在的子元素信息(仅仅遍历常用属性):
publicstaticvoiddisAllThread(ThreadGroupthreadgroup){
Threadlist[]=newThread[threadgroup.activeCount()];
threadgroup.enumerate(list);
for(Threadthread:
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完成负载均衡,一个暂停服务回收内存,另一个还可以运行嘛。
但是JVM是不是真的就不支持大内存了呢?
现在你可以这样理解,因为到目前为止可以这样认为,因为世界上所有的java虚拟机,没有不暂停的,而内存越大,回收的时间是必然越长的,不论有多么优秀的算法还做不到“不暂停”的这一点,所以我们的目标是尽量少的暂停,现在的CMSGC已经让我们看到了希望,不过还存在很多的缺陷,我们期待G1的成熟版本的出现,G1的论文很清晰,不过现在还没有一个成熟的版本,所以很期待。
4、JVM如何寻找到需要回收的内存:
要回收垃圾,那么首先要知道哪些内存是垃圾,或者反过来哪些不是垃圾,这个过程我们一般称为:
Mark的过程,Mark过程世界上没有任何一门虚拟机不进行对外暂停。
4.1、引用计数算法:
引用计数这里简单说明下,就是当一个引用被赋值的时候,虚拟机将会被知道(部分虚拟机通过写屏障实现),多一个引用,对象的计数增加1,少一个减少1,回收时,只回收等于0的,好处是算法非常简单,而且这种算法由于回收过程中只是看那些没有被引用,所以在一般情况下无需暂停,不过由于它在计数的过程中需要一个锁的机制,而且遍历内存的过程十分漫长,所以现在已经没有这个东西的存在了;另外一个问题出来了:
问题出来了:
循环引用,以及被这些对象引用的对象都讲永远回收不掉,因为循环引用中的对象引用计数永远大于等于1,那么这个资源在循环引用中,其实不是虚拟机算不出来,而且为了这个非常低的代价,虚拟机的算法将会复杂非常多。
其次这种分配方法在分配回收的过程中因为需要记录哪些内存是垃圾,哪些不是垃圾,所以一般需要维护一个freelist的区域。
4.2.引用树遍历算法:
首先,每个内存都有原始的引用根,这些根部一般来源于当前线程的栈针、静态引用、JNI的句柄等,从这里开始mark,将可达的对象标记为活着的对象,其余的就认为不是活着的对象,至于找到这些对象如何处理也就是回收的算法所决定的。
5、JVM如何回收内存的(回收算法分解详述):
首先了解几个其他的概念:
5.1.平时所说的JDK,其实是JAVA开发工具的意思,安装JAVA虚拟机会产生两个JRE目录,JRE目录为JAVA运行时环境的意思,两个JRE目录的区别是其中在JDK所在的JRE目录下没有Server和Client文件夹(JDK1.5自动安装包会自动将其复制到JDK下面一份),JRE为运行时环境,提供对JVM操作的API,JVM内部通过动态链接库(就是配置PATH的路径下),通过它作为主动态链接库寻找到其它的动态链接库,动态链接库为何OS绑定的参数,即代码最终要通过这些东西转换为操作系统指令集进行运行,另一个核心工具为JIT(JAVA即时编译工具),用于将代码转换为对应操作系统的运行指令集合的过程,不过其与惰性评估形成对比,后面会专门介绍。
5.1.JVM首先将大致分为:
JVM指令集、JVM存储器、JVM内存(堆栈区域部分)、JVM垃圾回收区域;JVM的堆部分又一般分为:
新域、旧域、永久域(很多时候不会认为永久域是堆的一部分,因为它是永远不会被回收的,它一般包含class的定义信息、static定义的方法、static匿名块代码段、常量信息(较为典型的就是String常量),不过这块内存也是可以被配置的);新域内部又可以分为Eden和两个救助区域,这几个对象在JVM内部有一定的默认值,但是也是可以被设置的。
当新申请的对象的时候,会放入Eden区中(这个区域一般不会太大,默认为新域的3/4,还有1/4一般会被切成两块,成为救助域),当对象在一定时间内还在使用的时候,它会逐步的进入旧域(此时是一个内存复制的过程,旧区域按照顺序,其引用的句柄也会被修改指向的位置),JVM回收中会先将Eden里面的内存和一个救助区域的内存就会被赋值到另一个救助区域,然后对这两块内存进行回收,同理,旧区域也有一个差不多大小的内存区域进行被复制,这个复制的过程肯定就会在一定程度上将内存连续的排列起来;另外可以想到JAVA提供内存复制最快的就是System.arrayCopy方法,那么这个肯定是按照内存数组进行拷贝(JVM起始就是一个大内存,本身就可以成是几个大数组组成的,而这个拷贝方法,默认拷贝多长呢,其实数组最长可以达到多少,通过数组的length返回的是int类型数据就可以清楚发现,为int类型的上限1<<31-1的长度(理想情况,因为有可能因为操作系统的其他进程导致JVM内存本身就不是连续的),即在(2G-1)*单元内存长度,所以也在一定程度上说明我们的一个JVM设置内存不要太大,不然复制内存的过程开销是很大的)。
其实上述描述的是一种停止-复制回收算法,在这个过程中形成了几个大的内存来回倒,这必然是很恶心的事情,那么继续将其切片为几个大的板块,有些大的对象会出现一两个对象占用一个版块的现象,这些大对象基本不会怎么移动(被回收就是另一回事,因为会清空这个版块),板块之间有一些对应关系,在回收时先将一些版块的小对象,向另一个还未装满的大板块内部转移,复制的粒度变小了,另外管理上可以发挥多线程的优势所在,好比是将一块大的田地,分成很多小田地,每块田地种植不同档次的秧苗,将其划分等级,我们假如秧苗经常会随机的死掉一些(这块是垃圾),在清理一些很普通的秧苗田地的时候,可能会将其中一块或几块田地的(活着的秧苗)种植到另一块田地中,但是他们不可以将高档次的秧苗移植到低档次的田地中,因为高档次的秧苗造价太高(内存太大),移植过程中代价太大,需要使用非普通秧苗的手段要移动他们,所以基本不移动他们,除非丰收他们的时候(他们也成为垃圾内存的时候),才会被拔出,腾出田地来。
在转移秧苗的过程中,他们需要整理出顺序便于管理,在很多书籍上把这个过程叫做压缩,因为这样使得保证在只要内存不溢出的情况下,申请的对象都有足够的控件可以存放,不然零碎的空间中间的缝隙未必可以存放下一个较大的对象。
将内存分块管理就是另一个停止复制收集器的进一步升级:
增量收集思想。
5.2.一般在hotspot的回收过程有以下一些历史:
标记—清除、标记—压缩、停止—复制、增量收集、分代收集、并行收集、并发收集。
这些收集器各有优缺点,某些收集器可能在早期硬件设备上显示不出优势,不过后来的收集器显示出优势出来,这也是为什么技术没有长短之分,只有时机和场合的问题,不过不论那一种收集器都需要暂停,细节后续来讨论,总的来说hotspot版本的虚拟机经历了串行收集、到早期的并行收集、到新的并行收集算法、再到并发收集(CMSGC)的过程,不过一直以来还是不能满足很多高可用的需求,尤其是面对大内存时,回收会显得非常缓慢,很多时候不得不将其拆分为多个JVM来处理;一直以来我们都期待有一块自己管理的区域出现,或者几乎不受到JVM的干涉,或者有多快这样的区域,也就是目前来说一般JVM的Heap是短命、长命的两个大区域,不过很多时候我们的对象是半长命得,很难让我们控制起来,比如我们的pageCache,既不是很长的命,但是命也不短,但是又不想影响Young区域的正常对象申请,又不想去导致Old区域的大量回收和compaction,所以我们希望有这样的区域出现,不过可惜的事情是现在不是SUN说了算,而是Oracle,这个梦想我想在开源的Hotspot上很难实现了,不过G1倒是让我们看到一些希望,期望G1成熟版本的诞生。
6、应用服务器部署及常用参数设置:
说到JVM的配置,最常用的两个配置就是:
-Xms512m–Xmx1024m
-Xms设置JVM的初始化内存大小,-Xmx为最大内存大小,当突破这个值,将会报内存溢出,导致的原因有很多,主要是虚拟机的回收问题以及程序设计上的内存泄露问题;由于在超过-Xms时会产生页面申请的开销,所以一般很多应用服务器会推荐-Xms和-Xmx是等值的;最大值一般不保持在主机内存的75%的内存左右(多个SERVER是加起来的内存),当JVM绝大部分时间处于回收状态,并且内存长时间处于非常长少的状态就会报:
java.lang.OutOfMemoryError:
Javaheapspace的错误。
上面提及到JVM很多的知识面,很显然你想去设置一下其它的参数,其实对于JVM设置的参数有上百个,这里就说一些较为常用配置即可。
JVM内存配置分两大类:
1、-X开头的参数信息:
一般每个版本变化不大。
2、-XX开头的参数信息:
版本升级变化较大,如果没有太大必要保持默认即可。
3、另外还有一个特殊的选项就是-server还是-client,他们在默认配置内存上有一些细微的区别,直接用JDK运行程序默认是-client,应用服务器生产模式一般只会用-server。
这些命令其实就是在运行java命令或者javaw等相关命令后可以配置的参数,如果不配置,他们有相应的默认值配置。
1、-X开头的常用配置信息:
-Xnoclassgc禁用垃圾回收,一般不适用这个参数
-Xincgc启用增量垃圾回收
-Xmn1024KEden区初始化JAVA堆的尺寸,默认值640K
-Xms512mJAVA堆初始化尺寸,默认是32M
-Xmx512mJAVA堆最大尺寸,默认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跟踪类卸载
等等。
。
。
。
。
。
====》配置归配置,希望大家不要乱去配置,也不要想当然去配置,一般来说只需要设置最基本的几个参数,其余的就不用关心了,很多时候我们发现设置了的结果还不如不设置,很多时候不设置它也有默认值,默认值在很多情况下就是正确的,并且,当你要设置它的时候,一定要知道它的默认值,不同的厂商甚至于不同的版本每个参数的默认值都会有所不同,所以你在任何地方看到的默认值都是未必靠谱的,甚至于官方公布的一些默认值也未必百分之百的靠谱,因为写官方文档的朋友未必是编写这段代码的朋友,你要真正知道默认值还真得看代码,呵呵
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- JAVA 垃圾 回收 机制
![提示](https://static.bdocx.com/images/bang_tan.gif)