精华内容
下载资源
问答
  • 怎样选择适合自己的对象
    千次阅读
    2020-06-19 22:25:49

    栈对象

    的优势是在适当的时候自动生成,又在适当的时候自动销毁,不需要程序员操心;而且栈对象的创建速度一般较堆对象快,因为分配堆对象时,会调用operator new操作,operator new会采用某种内存空间搜索算法,而该搜索过程可能是很费时间的,产生栈对象则没有这么麻烦,它仅仅需要移动栈顶指针就可以了。但是要注意的是,通常栈空间容量比较小,一般是1MB~2MB,所以体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,因为随着递归调用深度的增加,所需的栈空间也会线性增加,当所需栈空间不够时,便会导致栈溢出,这样就会产生运行时错误。

    堆对象

    其产生时刻和销毁时刻都要程序员精确定义,也就是说,程序员对堆对象的生命具有完全的控制权。我们常常需要这样的对象,比如,我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择,然后在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享。另外,相比于栈空间,堆的容量要大得多。实际上,当物理内存不够时,如果这时还需要生成新的堆对象,通常不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。

    static对象

    首先是全局对象。全局对象为类间通信和函数间通信提供了一种最简单的方式,虽然这种方式并不优雅。一般而言,在完全的面向对象语言中,是不存在全局对象的,比如C#,因为全局对象意味着不安全和高耦合,在程序中过多地使用全局对象将大大降低程序的健壮性、稳定性、可维护性和可复用性。C++也完全可以剔除全局对象,但是最终没有,我想原因之一是为了兼容C。

    其次是类的静态成员,上面已经提到,基类及其派生类的所有对象都共享这个静态成员对象,所以当需要在这些class之间或这些class objects之间进行数据共享或通信时,这样的静态成员无疑是很好的选择。

    接着是静态局部对象,主要可用于保存该对象所在函数被屡次调用期间的中间状态,其中一个最显著的例子就是递归函数,我们都知道递归函数是自己调用自己的函数,如果在递归函数中定义一个nonstatic局部对象,那么当递归次数相当大时,所产生的开销也是巨大的。这是因为nonstatic局部对象是栈对象,每递归调用一次,就会产生一个这样的对象,每返回一次,就会释放这个对象,而且,这样的对象只局限于当前调用层,对于更深入的嵌套层和更浅露的外层,都是不可见的。每个层都有自己的局部对象和参数。

    在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

    更多相关内容
  • 主要介绍了python实现对象列表根据某个属性排序的方法,结合具体实例形式详细分析了Python对象列表遍历、排序的两种常见操作技巧,需要的朋友可以参考下
  •  本书内容精炼,示例简单明了,适合各层次面向对象开发人员阅读,也是高校相关专业面向对象课程的理想教学参考书。 第1章 面向对象概念介绍 1 1.1 过程式程序设计与OO程序设计 2 1.2 从过程式开发转向面向对象...
  • python版,面向对象编程分三篇给大家介绍,这是第一篇,欢迎阅读学习,一起进步 Python专栏请参考:人生苦短-我学python ...面向对象则出现得更晚一些,典型代表为Java或C++等语言,更加适合用于大型开发
  • 提到年龄计算器适合你的恋爱对象,大家都知道,有人问岁数计算器怎么对象生日日期,另外,还有人想问11.06出生的人的”适合你的恋爱对象”及”适合你的朋…,你知道这是怎么回事?其实统计:能接受恋爱对象自己...

    提到年龄计算器适合你的恋爱对象,大家都知道,有人问岁数计算器怎么查对象生日日期,另外,还有人想问11.06出生的人的”适合你的恋爱对象”及”适合你的朋…,你知道这是怎么回事?其实统计:能接受恋爱对象比自己年龄大多少,下面就一起来看看什么年龄适合恋爱,希望能够帮助到大家!

    年龄计算器适合你的恋爱对象

    爱情就像便便来的时候,你想挡也挡不住,爱情就像便便去的时候,你想留也留不住,对于什么年龄谈恋爱,这个因人而异,缘分到了,当那个人出现了,就算是你不想谈恋爱,你也会爱上对方,至 内于结婚的话,最好是在22岁以后,工作事业生活各方面比较稳定,然 容后考虑选择一个比较适合的人恋爱相处,然后结婚生子过日子。年龄计算器在线计算。

    恋爱计算器在线使用

    一款Android平台的应用。爱计算器使用复杂的算法,检查情人兼容性,输入你想爱的兼容性来检查的人的名字,并与一个按钮,一推了之爱计算器应用程序开始工作,和预测的兼容性得分。

    fdf3bc88b7fc012c2850d8c902670c88.png

    年龄计算器适合你的恋爱对象:岁数计算器怎么查对象生日日期

    步骤如下:算年龄的公式计算器。

    1、进入年龄计算器,然后选择“公历计算”或“农历计算”,选择对方的出生日期后,点击开始计算即可。

    2、最后我们就可以看到对方的生日、生肖、星座等等相关信息了,还是比较全面的。

    年龄计算器,在上方列表中填写您的生日年月日,即可快速计算出您的准确年龄、周龄、月龄、换算后的小时、分钟时间等,以及距离下次生日的详细年月日!是一款很实用的实际年龄计算器,可以用来计算宠物、狗狗、宝宝的实际年龄、月龄、周龄等。图形计算器使用对象。

    拓展资料

    fdf3bc88b7fc012c2850d8c902670c88.png

    1、计算方法

    统计:能接受恋爱对象比自己年龄大多少

    随着全球人口平均寿命的延长和老龄化的加剧,WHO对年龄分期进行了重新划定,规定44岁以内为青年人,45~59岁为中年人(壮年期),60~74岁为年轻老人,75~89岁为真正老人,90岁以上为长寿老人,100岁以上为百岁老人。

    2、虚岁年龄年龄计算器。

    中国在习惯上常用的年龄计算方法,按出生后所经历的日历年头计算,即生下来就算1岁,以后每过一次新年便增加1岁。一般按农历新年算,也有按公历算的。例如,12月末出生的婴儿,出生后就算1岁,过了公历1月1日或当地农历新年又算1岁。这样,婴儿出生才几天,已算虚岁2岁了。这种计算方法较为实用。

    3、周岁年龄

    又称实足年龄,指从出生到计算时为止,共经历的周年数或生日数。例如,1990年7月1日零时进行人口普查登记,一个1989年12月15日出生的婴儿,按虚岁计算是2岁,实际刚刚6个多月,还未过一次生日,按周岁计算应为不满1周岁,即0岁。周岁年龄比虚岁年龄常常小1~2岁,它是人口统计中常用的年龄计算方法。

    周岁—出生时为0岁,每过一个公历生日长1岁。

    4、确切年龄精确年龄计算器。

    指从出生之日起到计算之日止所经历的天数。它比周岁年龄更为精确地反映人们实际生存的时间,但由于其统计汇总时较为繁琐,故人口统计中使用甚少。在实际生活中,人们除对不满1周岁的婴儿,特别是不满1个月的新生儿常常按月日计算外,一般不按日计算确切年龄。

    以上就是与什么年龄适合恋爱?相关内容,是关于岁数计算器怎么查对象生日日期的分享。看完年龄计算器适合你的恋爱对象后,希望这对大家有所帮助!

    发布者:姓名配对,原创文章禁止转载 出处:http://www.allyfurn.com/pdxzx/75716.html

    展开全文
  • 展开全部前面是我自己理解的后面是复制的java有自32313133353236313431303231363533e59b9ee7ad9431333236396530动垃圾回收机制当垃圾收集器判断已经没有任何引用指向对象的时候,会调用对象的finalize方法来释放对象...

    展开全部

    前面是我自己理解的后面是复制的

    java有自32313133353236313431303231363533e59b9ee7ad9431333236396530动垃圾回收机制

    当垃圾收集器判断已经没有任何引用指向对象的时候,会调用对象的finalize方法来释放对象占据的内存空间~

    java中垃圾回收以前听老师讲好像是内存满了他才去做一次整体垃圾回收,在回收垃圾的同时会调用finalize方法.你在构造一个类时可以构造一个类时覆盖他的finalize方法以便于该类在被垃圾回收时执行一些代码,比如释放资源.

    1.JVM的gc概述

    gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存。java语言并不要求jvm有gc,也没有规定gc如何工作。不过常用的jvm都有gc,而且大多数gc都使用类似的算法管理内存和执行收集操作。

    在充分理解了垃圾收集算法和执行过程后,才能有效的优化它的性能。有些垃圾收集专用于特殊的应用程序。比如,实时应用程序主要是为了避免垃圾收集中断,而大多数OLTP应用程序则注重整体效率。理解了应用程序的工作负荷和jvm支持的垃圾收集算法,便可以进行优化配置垃圾收集器。

    垃圾收集的目的在于清除不再使用的对象。gc通过确定对象是否被活动对象引用来确定是否收集该对象。gc首先要判断该对象是否是时候可以收集。两种常用的方法是引用计数和对象引用遍历。

    1.1.引用计数

    引用计数存储对特定对象的所有引用数,也就是说,当应用程序创建引用以及引用超出范围时,jvm必须适当增减引用数。当某对象的引用数为0时,便可以进行垃圾收集。

    1.2.对象引用遍历

    早期的jvm使用引用计数,现在大多数jvm采用对象引用遍历。对象引用遍历从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集。在对象遍历阶段,gc必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。

    下一步,gc要删除不可到达的对象。删除时,有些gc只是简单的扫描堆栈,删除未标记的未标记的对象,并释放它们的内存以生成新的对象,这叫做清除(sweeping)。这种方法的问题在于内存会分成好多小段,而它们不足以用于新的对象,但是组合起来却很大。因此,许多gc可以重新组织内存中的对象,并进行压缩(compact),形成可利用的空间。

    为此,gc需要停止其他的活动活动。这种方法意味着所有与应用程序相关的工作停止,只有gc运行。结果,在响应期间增减了许多混杂请求。另外,更复杂的 gc不断增加或同时运行以减少或者清除应用程序的中断。有的gc使用单线程完成这项工作,有的则采用多线程以增加效率。

    2.几种垃圾回收机制

    2.1.标记-清除收集器

    这种收集器首先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其他操作。

    2.2.标记-压缩收集器

    有时也叫标记-清除-压缩收集器,与标记-清除收集器有相同的标记阶段。在第二阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也停止其他操作。

    2.3.复制收集器

    这种收集器将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,jvm生成的新对象则放在另一半空间中。gc运行时,它把可到达对象复制到另一半空间,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。

    2.4.增量收集器

    增量收集器把堆栈分为多个域,每次仅从一个域收集垃圾。这会造成较小的应用程序中断。

    2.5.分代收集器

    这种收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。jvm生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。

    2.6.并发收集器

    并发收集器与应用程序同时运行。这些收集器在某点上(比如压缩时)一般都不得不停止其他操作以完成特定的任务,但是因为其他应用程序可进行其他的后台操作,所以中断其他处理的实际时间大大降低。

    2.7.并行收集器

    并行收集器使用某种传统的算法并使用多线程并行的执行它们的工作。在多cpu机器上使用多线程技术可以显著的提高java应用程序的可扩展性。

    3.Sun HotSpot

    1.4.1 JVM堆大小的调整

    Sun HotSpot 1.4.1使用分代收集器,它把堆分为三个主要的域:新域、旧域以及永久域。Jvm生成的所有新对象放在新域中。一旦对象经历了一定数量的垃圾收集循环后,便获得使用期并进入旧域。在永久域中jvm则存储class和method对象。就配置而言,永久域是一个独立域并且不认为是堆的一部分。

    下面介绍如何控制这些域的大小。可使用-Xms和-Xmx 控制整个堆的原始大小或最大值。

    下面的命令是把初始大小设置为128M:

    java –Xms128m

    –Xmx256m为控制新域的大小,可使用-XX:NewRatio设置新域在堆中所占的比例。

    下面的命令把整个堆设置成128m,新域比率设置成3,即新域与旧域比例为1:3,新域为堆的1/4或32M:

    java –Xms128m –Xmx128m

    –XX:NewRatio =3可使用-XX:NewSize和-XX:MaxNewsize设置新域的初始值和最大值。

    下面的命令把新域的初始值和最大值设置成64m:

    java –Xms256m –Xmx256m –Xmn64m

    永久域默认大小为4m。运行程序时,jvm会调整永久域的大小以满足需要。每次调整时,jvm会对堆进行一次完全的垃圾收集。

    使用-XX:MaxPerSize标志来增加永久域搭大小。在WebLogic Server应用程序加载较多类时,经常需要增加永久域的最大值。当jvm加载类时,永久域中的对象急剧增加,从而使jvm不断调整永久域大小。为了避免调整,可使用-XX:PerSize标志设置初始值。

    下面把永久域初始值设置成32m,最大值设置成64m。

    java -Xms512m -Xmx512m -Xmn128m -XX:PermSize=32m -XX:MaxPermSize=64m

    默认状态下,HotSpot在新域中使用复制收集器。该域一般分为三个部分。第一部分为Eden,用于生成新的对象。另两部分称为救助空间,当Eden 充满时,收集器停止应用程序,把所有可到达对象复制到当前的from救助空间,一旦当前的from救助空间充满,收集器则把可到达对象复制到当前的to救助空间。From和to救助空间互换角色。维持活动的对象将在救助空间不断复制,直到它们获得使用期并转入旧域。使用-XX:SurvivorRatio 可控制新域子空间的大小。

    同NewRation一样,SurvivorRation规定某救助域与Eden空间的比值。比如,以下命令把新域设置成64m,Eden占32m,每个救助域各占16m:

    java -Xms256m -Xmx256m -Xmn64m -XX:SurvivorRation =2

    如前所述,默认状态下HotSpot对新域使用复制收集器,对旧域使用标记-清除-压缩收集器。在新域中使用复制收集器有很多意义,因为应用程序生成的大部分对象是短寿命的。理想状态下,所有过渡对象在移出Eden空间时将被收集。如果能够这样的话,并且移出Eden空间的对象是长寿命的,那么理论上可以立即把它们移进旧域,避免在救助空间反复复制。但是,应用程序不能适合这种理想状态,因为它们有一小部分中长寿命的对象。最好是保持这些中长寿命的对象并放在新域中,因为复制小部分的对象总比压缩旧域廉价。为控制新域中对象的复制,可用-XX:TargetSurvivorRatio控制救助空间的比例(该值是设置救助空间的使用比例。如救助空间位1M,该值50表示可用500K)。该值是一个百分比,默认值是50。当较大的堆栈使用较低的 sruvivorratio时,应增加该值到80至90,以更好利用救助空间。用-XX:maxtenuring threshold可控制上限。

    为放置所有的复制全部发生以及希望对象从eden扩展到旧域,可以把MaxTenuring Threshold设置成0。设置完成后,实际上就不再使用救助空间了,因此应把SurvivorRatio设成最大值以最大化Eden空间,设置如下:

    java … -XX:MaxTenuringThreshold=0 –XX:SurvivorRatio=50000 …

    4.BEA JRockit JVM的使用

    Bea WebLogic 8.1使用的新的JVM用于Intel平台。在Bea安装完毕的目录下可以看到有一个类似于jrockit81sp1_141_03的文件夹。这就是 Bea新JVM所在目录。不同于HotSpot把Java字节码编译成本地码,它预先编译成类。JRockit还提供了更细致的功能用以观察JVM的运行状态,主要是独立的GUI控制台(只能适用于使用Jrockit才能使用jrockit81sp1_141_03自带的console监控一些cpu及 memory参数)或者WebLogic Server控制台。

    Bea JRockit JVM支持4种垃圾收集器:

    4.1.1.分代复制收集器

    它与默认的分代收集器工作策略类似。对象在新域中分配,即JRockit文档中的nursery。这种收集器最适合单cpu机上小型堆操作。

    4.1.2.单空间并发收集器

    该收集器使用完整堆,并与背景线程共同工作。尽管这种收集器可以消除中断,但是收集器需花费较长的时间寻找死对象,而且处理应用程序时收集器经常运行。如果处理器不能应付应用程序产生的垃圾,它会中断应用程序并关闭收集。

    分代并发收集器这种收集器在护理域使用排它复制收集器,在旧域中则使用并发收集器。由于它比单空间共同发生收集器中断频繁,因此它需要较少的内存,应用程序的运行效率也较高,注意,过小的护理域可以导致大量的临时对象被扩展到旧域中。这会造成收集器超负荷运作,甚至采用排它性工作方式完成收集。

    4.1.3.并行收集器

    该收集器也停止其他进程的工作,但使用多线程以加速收集进程。尽管它比其他的收集器易于引起长时间的中断,但一般能更好的利用内存,程序效率也较高。

    默认状态下,JRockit使用分代并发收集器。要改变收集器,可使用-Xgc:,对应四个收集器分别为 gencopy,singlecon,gencon以及parallel。可使用-Xms和-Xmx设置堆的初始大小和最大值。要设置护理域,则使用- Xns:java –jrockit –Xms512m –Xmx512m –Xgc:gencon –Xns128m…尽管JRockit支持-verbose:gc开关,但它输出的信息会因收集器的不同而异。JRockit还支持memory、 load和codegen的输出。

    注意 :如果 使用JRockit JVM的话还可以使用WLS自带的console(C:\bea\jrockit81sp1_141_03\bin下)来监控一些数据,如cpu, memery等。要想能构监控必须在启动服务时startWeblogic.cmd中加入-Xmanagement参数。

    5.如何从JVM中获取信息来进行调整

    -verbose.gc开关可显示gc的操作内容。打开它,可以显示最忙和最空闲收集行为发生的时间、收集前后的内存大小、收集需要的时间等。打开- xx:+ printgcdetails开关,可以详细了解gc中的变化。打开-XX: + PrintGCTimeStamps开关,可以了解这些垃圾收集发生的时间,自jvm启动以后以秒计量。最后,通过-xx: + PrintHeapAtGC开关了解堆的更详细的信息。为了了解新域的情况,可以通过-XX:=PrintTenuringDistribution开关了解获得使用期的对象权。

    6.Pdm系统JVM调整

    6.1.服务器:前提内存1G 单CPU

    可通过如下参数进行调整:-server 启用服务器模式(如果CPU多,服务器机建议使用此项)

    -Xms,-Xmx一般设为同样大小。 800m

    -Xmn 是将NewSize与MaxNewSize设为一致。320m

    -XX:PerSize 64m

    -XX:NewSize 320m 此值设大可调大新对象区,减少Full GC次数

    -XX:MaxNewSize 320m

    -XX:NewRato NewSize设了可不设。

    -XX: SurvivorRatio

    -XX:userParNewGC 可用来设置并行收集

    -XX:ParallelGCThreads 可用来增加并行度

    -XXUseParallelGC 设置后可以使用并行清除收集器

    -XX:UseAdaptiveSizePolicy 与上面一个联合使用效果更好,利用它可以自动优化新域大小以及救助空间比值

    6.2.客户机:通过在JNLP文件中设置参数来调整客户端JVM

    JNLP中参数:initial-heap-size和max-heap-size

    这可以在framework的RequestManager中生成JNLP文件时加入上述参数,但是这些值是要求根据客户机的硬件状态变化的(如客户机的内存大小等)。建议这两个参数值设为客户机可用内存的60%(有待测试)。为了在动态生成JNLP时以上两个参数值能够随客户机不同而不同,可靠虑获得客户机系统信息并将这些嵌到首页index.jsp中作为连接请求的参数。

    在设置了上述参数后可以通过Visualgc 来观察垃圾回收的一些参数状态,再做相应的调整来改善性能。一般的标准是减少fullgc的次数,最好硬件支持使用并行垃圾回收(要求多CPU)。

    2Q==

    已赞过

    已踩过<

    你对这个回答的评价是?

    评论

    收起

    展开全文
  • Java引用对象

    万次阅读 2018-12-11 10:09:03
    借助指针切换(pointer handoffs)等编码实践或者Purify等工具,我认为自己对C风格的内存管理已经得心应手了,甚至已经不记得上次发生内存泄露是什么时候了。所以起初我接触到Java的自动内存管理时有些不屑,但很快就...

    简介

    在写了15年C/C++之后,我于1999年开始写Java。借助指针切换(pointer handoffs)等编码实践或者Purify等工具,我认为自己对C风格的内存管理已经得心应手了,甚至已经不记得上次发生内存泄露是什么时候了。所以起初我接触到Java的自动内存管理时有些不屑,但很快就爱上它了。在我不需要再管理内存后我才意识到之前耗费了多少精力。

    接着我就遇到了第一个OutOfMemoryError。当时我就坐在那面对着控制台,没有堆栈,因为堆栈也需要内存。调试这个错误很困难,因为常用的工具都不能用了,甚至malloc logger都没有,而且1999年的时候Java的调试器还很原始。

    我不记得当时是什么原因导致的错误了,但我肯定当时没有用引用对象解决它。引用对象是在一年后我写服务端数据库缓存,尝试用软引用来限制缓存大小时才进入我的“工具箱”的。结果证明它们在这种场景下用处不大,我下面会解释原因。但当引用类型才进入我的“工具箱”后,我发现了很多其他用途,并且对JVM也有了更好的理解。

    Java堆和对象生命周期

    对于刚接触Java的C++程序员来说,栈和队之间的关系很难理解。在C++中,对象可以通过new操作在堆上创建,也可以通过“自动”分配在栈上创建。下面这种在C++中是合法的,会在栈上创建一个新的Integer对象,但对于Java编译器来说这有语法错误。

    1
    
    Integer foo = Integer(1);
    

    不同于C++,Java的所有对象都在堆保存,要求用new操作来创建对象。局部变量仍然储存在栈中,但它们持有这个对象的指针而不是这个对象本身(更让C++程序员困惑的是这些指针被叫做“引用”)。下面这个Java方法,有一个Integer变量引用一个从String解析而来的值:

    1
    2
    3
    4
    
    public static void foo(String bar)
    {
        Integer baz = new Integer(bar);
    }
    

    下图显示了这个方法相应的堆和栈之间的关系。栈被分割为栈帧,用于保存调用树中各个方法的参数和局部变量。这些变量指向对象–这个例子中的参数bar和局部变量baz–指23向存在于堆中的变量。

    现在仔细看看foo()的第一行,创建了一个Integer对象。这种情况下,JVM会先试图去为这个对象找足够的堆空间–在32位JVM上大约12 bytes,如果可以分配出空间,就调用Integer的构造函数,Integer的构造函数会解析传入的String然后初始化这个新创建的对象。最后,JVM在变量baz中保存一个指向该对象的指针。

    这是理想的道路,还有一些不那么美好的道路,其中我们关心的是当new操作不能为这个对象找到12 bytes的情况。在这种情况下,JVM会在放弃并抛出OutOfMemoryError之前调用垃圾回收器尝试腾出空间。

    垃圾回收

    虽然Java给了你new操作来在堆上分配对象,但是没有给你对应的delete操作来移除它们。当方法foo()返回,变量baz离开了作用域,但是它指向的对象依然存在于堆中。如果只是这样的话,那所有的程序都会很快耗尽内存。Java提供了垃圾回收器来清理那些不再被引用的对象。

    垃圾回收器会在程序尝试创建一个新对象但堆没有足够的空间时工作。回收器在堆上寻找那些不再被程序使用的对象并回收它们的空间时,请求创建对象的线程会暂停。如果回收器无法腾出足够的空间,并且JVM无法扩展堆,new操作就会失败并抛出OutOfMemoryError,通常接下来你的应用会停止。

    标记-清除

    其中一个关于垃圾回收器的误区是,很多人认为JVM为每个对象保存了一个引用计数,回收器只会回收那些引用计数为0的对象。事实上,JVM使用被称为“标记-清除”的技术。标记-清除算法的思路很简单:所有不能被程序访问到的对象都是垃圾,都可以被收集。

    标记-清除算法有以下阶段:

    阶段一:标记

    垃圾回收器从“root”引用开始,标记所有可以到达的对象。

    阶段二:清除

    在第一阶段没有被标记的都是不可到达的,也就是垃圾。如果垃圾对象定义了finalizer,它会被加到finalization队列(后文详细讨论)。否则,它占用的空间就可以被重新分配使用(具体的情况视GC的实现而定,有很多种实现)。

    阶段三:压缩(可选)

    一些回收器有第三步——压缩。在这一步,GC会移动对象使回收的对象留下的空闲空间合并,这可以防止堆变得碎片化,避免大块相邻内存分配的失败。

    例如,Hotspot JVM,在新生代使用会压缩的回收器,而在老年代使用非压缩的回收器(至少在1.6和1.7的“server” JVM是这样)。想了解更多信息,可以看本文后面的参考文献。

    那么什么是“roots”呢?在一个简单的Java应用中,它们是方法参数和局部变量(保存在栈中)、当前执行的表达式操作的对象(也保存在栈中)、静态类成员变量。

    对于使用自己classloader的程序,例如应用服务器,情况复杂一些:只有被system classloader(JVM启动时使用这个loader)加载的类包含root引用。那些被应用创建的classloader一旦没有其他引用也会被回收。这是应用服务器可以热部署的原因:它们为每个部署的应用创建独立的classloader,当应用下线或重新部署时释放classloader引用。

    理解root引用很重要,因为这定义了“强引用”,即如果可以从root沿着引用链到达某个对象,那么这个对象就被“强引用”了,则不会被回收。

    回到foo()方法,参数bar和局部变量baz使用当方法执行时才是强引用,一旦方法结束,它们都超出了作用域,被他们引用的对象就可以回收。另一种可能是,foo()返回一个它创建的Integer引用,这意味着这个对象会被调用foo()的那个方法保持强引用。

    看下面这个例子:

    1
    2
    
    LinkedList foo = new LinkedList();
    foo.add(new Integer(123));
    

    变量foo是一个指向LinkedList对象的root引用,列表中有0个或多个元素,都指向其对象。当我们调用add()时,向列表中添加了一个指向值为123的Integer实例的元素,这是一个强引用,意味着这个Integer实例不会被回收。一旦foo超出了作用域,这个LinkedList和它里面的一切都可以被回收,当前前提是没有其他强引用指向它了。

    你也许想知道循环引用会发生什么,即对象A包含一个对象B的引用,同时对象B也包含对象A的引用。答案是标记-清除回收器并不傻,如果A和B都无法由强引用链到达,那么它们都可以被回收。

    Finalizers

    C++允许对象定义析构方法,当对象离开作用域或者被明确删除时,它的析构函数会被调用来清理它使用的资源,对大多数对象来说即释放通过newmalloc分配的内存。在Java中,垃圾回收器会为你处理内存清理,所以不需要明确的析构函数来做这些。

    然而,内存并不是唯一可能需要被清理的资源。例如FileOutputStream,当创建这个对象的实例时,会从操作系统分配一个文件操作符(文件句柄),如果你在关闭流之前让它的所有引用都离开作用域了,这个文件操作符会发生什么呢?答案是流有finalizer,这个方法会在垃圾回收器回收对象前被JVM调用。这个例子中的FileOutputStreamfinalizer方法中会关闭流,这样就会将文件操作符返回给操作系统,同时也会刷新缓存,确保所有数据被正确地写到磁盘。

    任何对象都可以有finalizer,你只需要定义finalize()方法即可:

    1
    2
    3
    4
    
    protected void finalize() throws Throwable
    {
        // 在这里释放你的对象
    }
    

    finalizers看上去是一个由你自己清理的简单方式,但实际上有严重的限制。首先,你永远也不要依赖它做重要的事,因为对象的finalizers可能不会被调用,应用可能在对象被回收之前就结束了。finalizers还有一些更微妙的问题,我会在虚引用时讨论。

    对象的生命周期(无引用对象)

    总结起来,对象的一生可以用下面的图总结:被创建、被使用、可回收、最终被回收。阴影部分表示对象是“强可达”的时期,这是与引用对象规定的可达性比较而言很重要的时期。

    进入引用对象的世界

    JDK 1.2引入了java.lang.ref包,对象的生命周期增加了3种阶段:软可达、弱可达、虚可达。这些阶段只用来可否被回收,换言之,那些不是强引用的对象,必须是其中一种引用对象的被引用者:

    • 软可达
      对象是SoftReference的被引用者,并且没有强引用指向它。垃圾回收器会尽可能地保留它,但会在抛出OutOfMemoryError之前回收它。

    • 弱可达
      对象是WeakReference的被引用者,并且没有强引用指向它。垃圾回收器可以在任何时间回收它,不会试图去保留它。通常这个对象会在Major GC被回收,可能在Minor GC中存活。

    • 虚可达
      对象是PhantomReference的被引用者,它已经被选择要回收并且finalizer(如果有)已经运行了。这里的“可达”有点用词不当,在这个时候你已经没有办法访问到原始的对象了。

    如你所想,把这三种新的可选状态加到对象生命周期图中会变得很复杂。尽管文档指出了一个逻辑上从强可达到软可达、弱可达、虚可达的回收过程,但实际过程取决于你的程序创建了哪种引用对象。如果你创建了一个WeakReference而不是一个SoftReference,那么对象回收的过程是直接从强可达到弱可达最后被回收的。

    还有一点需要清楚的是,不是所有的对象都需要与引用对象关联,事实上,只有极少部分对象需要。引用对象是一个间接层:你通过引用对象去访问它的被引用者,你肯定不希望你的代码中充斥着这些间接层。事实上大部分程序只会使用引用对象去访问很少一部分它创建的对象。

    引用和被引用者

    引用对象是在你程序代码和一些称为被引用者的对象之间的中间层。每个引用对象都是围绕它的被引用者创建,并且被引用者是不能修改的。

    引用对象提供了get()方法来获取被引用者的强引用。垃圾回收器可能在某些情况下回收被引用者,一旦回收了,get()会返回null。正确使用引用,你需要类似下面这样的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    SoftReference<List<Foo>> ref = new SoftReference<List<Foo>>(new LinkedList<Foo>());
    
    // 代码其他地方创建了`Foo`,你想把它添加到列表中
    List<Foo> list = ref.get();
    if (list != null)
    {
        list.add(foo);
    }
    else
    {
        // 列表已经被回收了,做一些恰当的事
    }
    

    换言之:

    1. 你必须总是检查看被引用者是否是null。
      垃圾回收器可能在任何时间回收被引用者,如果你无所顾忌地使用,很快就会收获NullPointerException
    2. 当你想使用被引用者时,你必须持有一个它的强引用。
      再次强调, 垃圾回收器可能在任何时间回收被引用者,甚至是在单个表达式之间。上面的例子如果我不定义list变量,我而是简单地调用ref.get().add(foo),被引用者可能在检查是否为null和实际使用之间被回收。牢记垃圾回收器是在它自己的线程运行的,它不关心你的代码在干什么。
    3. 你必须持有引用类型的强引用。
      如果你创建了一个引用对象,但超出了它的作用域,那么这个引用对象自己也会被回收。这是显然的,但很容易被忘记,特别是在用引用队列(qv)追踪引用对象的时候。

    同样要记住的是软引用、弱引用、虚引用只有在没有其他强引用指向被引用者时才有意义,它们让你可以在对象通常会成为垃圾回收器的食物时候获得该对象。这可能看起来很奇怪,如果不再持有强引用了,为什么我还关心这个对象呢?原因视特殊的引用类型而定。

    软引用

    我们先从软引用开始来回答这个问题。如果一个对象是SoftReference的被引用者,并且它没有强引用,那么垃圾回收器可以回收但尽量不去回收它。因此,只要JVM有足够的内存,软引用对象就会在垃圾回收中存活,甚至经历好几轮垃圾回收依然存活。

    JDK文档说软引用适用于内存敏感的缓存:每个缓存对象都通过SoftReference访问,如果JVM觉得需要内存,它就会清除一些或者所有引用并回收对应的被引用者。如果JVM不需要内存,被引用者就会留在堆中,并且可以被程序代码访问到。在这种方案下,被引用者在使用时是强引用的,其他情况是软引用的,如果软引用被清除了,你需要刷新缓存。

    想作为这种角色使用,被缓存的对象需要比较大,如每个几kB。比如说,你想实现一个文件服务相同的文件会被定期检索,或者有一些大的图片对象需要缓存时会有用。但如果你的对象很小,你只有在需要定义大量对象时情况才会不同,引用对象还会增加整个程序的负担。

    内存限制型缓存被认为是有害的
    我的观点是,可用内存绝对是最差的管理缓存的方式。如果你的堆很小,你不时需要重新加载对象,无论它们是不是被活跃地使用,你也无法知道这个,因为缓存会静默地处理它们。大的堆更糟:你会持有对象远大于它的正常寿命,当每次垃圾回收时会使你的应用变慢,因为需要检查这些对象。如果这些对象没有被访问,这一部分堆有可能被交换出去,回收过程中可能有大量页错误。
    底线:如果你要用缓存,详细它会如何被使用,选一个适合的缓存策略(LRU、timed LRU),在选择基于内存的策略前仔细考虑。

    软引用用于断路器

    用软引用为内存分配提供断路器是更好的选择:在你的代码和它分配的内存之间使用软引用,你就可以避免可怕的OutOfMemoryError。这个技巧可以正常运作是因为在应用里内存的分配是趋于局部的:从数据库中读取行、从一个文件中处理数据。

    例如,如果你写过很多JDBC的代码,你可能会有类似下面这样的方法以某种方式处理查询的结果并且确保ResultSet被正确地关闭。这只有一个小缺陷:如果查询返回了一百万行,你没有可用的内存去保存它们时会发生什么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    public static List<List<Object>> processResults(ResultSet rslt)
    throws SQLException
    {
        try
        {
            List<List<Object>> results = new LinkedList<List<Object>>();
            ResultSetMetaData meta = rslt.getMetaData();
            int colCount = meta.getColumnCount();
    
            while (rslt.next())
            {
                List<Object> row = new ArrayList<Object>(colCount);
                for (int ii = 1 ; ii <= colCount ; ii++)
                    row.add(rslt.getObject(ii));
    
                results.add(row);
            }
    
            return results;
        }
        finally
        {
            closeQuietly(rslt);
        }
    }
    

    答案当然是会得到OutOfMemoryError。这是使用断路器的绝佳地方:如果在处理查询时JVM要耗尽内存了,那就释放所有已经使用的那些内存,抛出一个应用特殊的异常。

    你可能很奇怪,这种情况下这次查询将被忽略,为什么不直接让内存耗尽的错误来做这件事呢?原因是并不仅仅只有你的应用被内存耗尽影响。如果你在一个应用服务器上运行,你的内存使用可能干掉其他应用。即使是在一个独有的环境,断路器也能提升你的应用的健壮性,因为它能限制问题,让你有机会恢复并继续运行。

    要创建一个断路器,首先你需要做的是把结果的列表包装在SoftReference中(你在前面已经见过这个代码了):

    1
    2
    
    SoftReference<List<List<Object>>> ref
            = new SoftReference<List<List<Object>>>(new LinkedList<List<Object>>());
    

    然后,你遍历结果,在你需要更新这个列表时为它创建强引用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    while (rslt.next())
    {
        rowCount++;
        // store the row data
    
        List<List<Object>> results = ref.get();
        if (results == null)
            throw new TooManyResultsException(rowCount);
        else
            results.add(row);
    
        results = null;
    }
    

    这可以满足要求是因为这个方法几乎所有的内存分配都发生在2个地方:调用next()时和代码把行里的数据存放到它自己的列表中时。第一种情况当你调用next()时会发生很多事情:ResultSet一般会在包含多行的一大块二进制数据中检索,然后当你调用getObject(),它会取出一部分数据把它转成Java对象。

    当这些昂贵的操作发生时,这个list只有来自SoftReference的引用,如果内存耗尽,引用会被清除,list会变成垃圾。这意味着这个方法可能抛出异常,但抛出异常的影响是有限的,也许调用方能以一点数量限制重新进行查询。

    一旦昂贵的操作完成,你可以没有影响地拿到list的强引用。注意到我用LinkedList保存结果而不是ArrayListLinkedList增长时只会增加少量字节,不太可能引起OutOfMemoryError,而如果ArrayList需要增加容量,它需要创建一个新数组,对于大列表来说,这可能意味着数MB的内存分配。

    还注意到我在添加新元素后把results变量设置为null,这是少数几种这样做是合理的情形之一。尽管在循环的最后变量超出了作用域,但垃圾回收器可能并不知道(因为JVM没有理由去清除变量在调用栈中的位置)。因此如果我不清除这个变量的话,它会在随后的循环中成为隐藏的强引用。

    软引用不是万能的

    软引用可以预防很多内存耗尽的情况,但不能预防所有。问题在于:为了真正地使用软引用,你需要创建一个被引用者的强引用,即为了向results中添加一行,我们需要持有实际列表的引用。我们持有强引用的时候就会面临发生内存耗尽错误的风险。

    使用断路器的目标是把一些无用的东西的时间窗口减到最小:你持有对象强引用的时间,更重要的是在这段时间中分配内存的总量。在我们的例子中,我们限制强引用去添加一行到results中,我们使用LinkedList而不是ArrayList因为前者扩容时增长更小。

    我想重申的是,如果我一个变量持有强引用,但这个变量很快超出了作用域,语言细则没有说JVM需要清除超出作用域的变量,如果是像写的这样,Oracle/OpenJDK JVM都没有这样做,如果我不明确地清除results变量,在遍历期间会保持强引用,阻止软引用做它的工作。

    最后,仔细考虑那些隐藏的强引用。例如,你可能会想在使用DOM构造XML文档时加入断路器。在DOM中,每个节点都持有它父节点的引用,从而导致持有了树中每个其他节点的引用。如果你用递归去创建文档,你的栈中可能塞满了个别节点的引用。

    弱引用

    弱引用,正如它名字显示,是一个当垃圾回收器来敲门时不会反抗的引用对象。如果被引用者没有强引用或软引用而只有弱引用,那它就可以被回收。所以弱引用有什么用呢?有2个主要用途:关联没有内在联系的对象,或者通过canonicalizing map减少重复。

    ObjectOutputStream的问题

    第一个例子,我准备聚焦不使用弱引用的对象序列化。ObjectOutputStream以及它的伙伴ObjectInputStream提供了任意Java对象与字节流之间相互转换的方式。根据对象模型的观点,流和用这些流写的对象之间是没有联系的。流不是由这些被写的对象组成的,也不是它们的聚集。

    但是当你看这些流的说明时,你会看到事实上是有联系的:为了维持对象的唯一性,输出流会和每个被写的对象关联一个唯一的标识符,随后的写对象的请求被替换为写这个标识符。这个特征对于流序列号对象的能力来说绝对是很重要的,如果没有这个特征,自我引用的对象会变成一个无限的字节流。

    要实现这个特征,流需要持有每个写到流中的对象的强引用。对于决定在socket通信时用对象流作为消息协议的程序员来说,有这么一个问题:消息被设计为短暂的,但流会在内存中持有它们,不久之后,程序会耗尽内存(除非程序员知道在每次通信后调用reset())。

    这种非与生俱来的联系惊人的普遍。它们会在程序员为了使用对象而需要去维持必不可少的上下文时出现。有时这些联系被运行环境默默管理,例如servlet Session对象;有时这些联系需要被程序员明确地管理,例如对象流;还有些时候,这种联系只有当生产环境的服务抛出内存耗尽的错误时才会被发现,比如埋藏在程序代码深处的静态Map

    弱引用提供了一种维持这种联系的同时还能让垃圾回收器做它的工作的方式,弱引用只有在同时还有强引用时才保持有效。回到对象流的例子,如果你用流来通信,一旦消息被写完就可以被回收了。另一方面,当流用来RMI访问一个生命周期很长的数据结构时,它能保持它一致。

    不幸的是,尽管对象流通信协议在JDK 1.2时被更新了。虚引用也是这样被加入的,但JDK的开发者并没有选择把二者结合到一起,所以记得调用reset()

    Canonicalizing Maps消除重复数据

    尽管存在对象流这种情况,但我不认为有很多你应该关联两个没有内在关系的对象的情行。我所看到的一些例子,例如Swing监听器,它们会自我清理,看起来更像是黑客,而不是有效的设计选择。

    当我最初写这篇文章的时候,大约是在2007年,我提出了canonicalizing map作为String.intern()的替代物,是在假设被存入常量池的字符串永远不会被清理的前提下。后来我得知这种担心是毫无根据的。更重要的是,从JDK 8开始,OpenJDK已经完全去掉了永久代。因此,没有必要害怕intern(),但是canonicalizing map对于字符串以外的对象仍然有用。

    在我看来,弱引用的最佳用途是实现canonicalizing map,这是一种确保同时只存在一个值对象实例的办法。String.intern()是这种map的典型例子:当你把一个字符串存入常量池时,JVM会将它添加到一个特殊的map中,这个map也用于保存字符串文本。这样做的原因不是像一些人认为的那样为了更快地进行比较。这是为了最大限度地减少重复的非文字字符串(如从文件或消息队列中读取的字符串)占用的内存量。

    简单的canonicalizing map通过使用相同的对象作为key和value来工作:你用任意实例传给map,如果map中已经有一个值,你就返回它。如果map中没有值,则存储传入的实例(并返回它)。当然,这仅适用于可用作map的key的对象。如果我们不担心内存泄漏,下面可能是我们实现String.intern()的方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    private Map<String,String> _map = new HashMap<String,String>();
    
    public synchronized String intern(String str)
    {
        if (_map.containsKey(str))
            return _map.get(str);
        _map.put(str, str);
        return str;
    }
    

    如果你只有少量字符串要放入常量池,例如也许在处理一个文件的简单方法中,这个实现没什么问题。然而,假设你正在编写一个长期运行的应用程序,该应用程序必须处理来自多个来源的输入,其中包含范围广泛的字符串,但仍有高度的重复。例如,一台处理上传的邮政地址数据文件的服务:New York将会有很多条目,Temperanceville VA的条目就不多了。你会想要消除前者的重复,但是不想保留后者超过必要的时间。

    这就是弱引用的canonicalizing map有所帮助的地方:只有程序中的一些代码正在使用它,它才允许你创建一个规范的实例。最后一个强引用消失后,这个规范的字符串将被回收。如果稍后再次出现该字符串,它将成为新的规范的实例。

    为了改进我们的“规范化工具”,我们可以用WeakHashMap替换HashMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    private Map<String,WeakReference<String>> _map
        = new WeakHashMap<String,WeakReference<String>>();
    
    public synchronized String intern(String str)
    {
        WeakReference<String> ref = _map.get(str);
        String s2 = (ref != null) ? ref.get() : null;
        if (s2 != null)
            return s2;
    
        _map.put(str, new WeakReference(str));
        return str;
    }
    

    首先要注意的是,虽然map的key是字符串,但它的值是WeakReference<String>。这是因为WeakHashMap对其key使用弱引用,但对其value持有强引用。因为我们的key和value是相同的,所以entry永远不会被回收。通过包装条目,我们让GC回收它。

    其次,注意返回字符串的过程:首先我们检索弱引用,如果它存在,那么我们检索引用对象。但是我们也必须检查那个对象。存在引用仍在map中但已经被清除了的可能。只有当引用对象不为空时,我们才返回它;否则,我们认为传入的字符串是新的规范的版本。

    第三,请注意我对intern()方法用了synchronizedcanonicalizing map最有可能的用途是在多线程环境中,例如应用服务,WeakHashMap没有内部同步。这个例子中的同步实际上相当幼稚,intern()方法可能成为争论的焦点。在现实世界的实现中,我可能会使用ConcurrentHashMap,但是对于教程来说,这种幼稚的方法更有效。

    最后,WeakHashMap的文档关于条目何时从map中移除有些模糊。它指出,“WeakHashMap的行为可能就像一个未知线程正在无声地删除条目。”实际上没有其他线程。相反,每当map被访问时,它就会被清理。为了跟踪哪些条目不再有效,它使用了引用队列。

    引用队列

    虽然判断一个引用是不是null可以让你知道它的引用对象是不是已经被回收,但是这样做并不是很高效;如果你有很多引用,你的程序会花大部分时间寻找那些已经被清除的引用。

    更好的解决方案是引用队列:你在构建时将引用与队列相关联,并且该引用将在被清除后放入队列中。要发现哪些引用已被清除,你需要从队列拉取。这可以通过后台线程来完成,但是在创建新引用时从队列拉取通常更简单(WeakHashMap就是这样做的)。

    引用队列最常与虚引用一起使用,在后面会描述,但是可以与任何引用类型一起使用。下面的代码是一个弱引用的例子:它创建了一组缓冲区,通过WeakReference访问,并且在每次创建后查看哪些引用已经被清除。如果运行此代码,你会看到create消息的长时间出现,当垃圾回收器运行时偶尔会出现一些clear消息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    public static void main(String[] argv) throws Exception
    {
        Set<WeakReference<byte[]>> refs = new HashSet<WeakReference<byte[]>>();
        ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
        
        for (int ii = 0 ; ii < 1000 ; ii++)
        {
            WeakReference<byte[]> ref = new WeakReference<byte[]>(new byte[1000000], queue);
            System.err.println(ii + ": created " + ref);
            refs.add(ref);
            
            Reference<? extends byte[]> r2;
            while ((r2 = queue.poll()) != null)
            {
                System.err.println("cleared " + r2);
                refs.remove(r2);
            }
        }
    }
    

    一如既往,关于这个代码有一些值得注意的事情。首先,虽然我们创建的是WeakReference实例,但是队列会给我们返回Reference。这提醒你,一旦它们入队,你使用的是什么类型的引用就不再重要了,被引用者已经被清除。

    第二,我们必须对引用对象本身进行强引用。引用对象知道队列,但是队列在引用进入队列前不知道引用。如果我们没有维护对引用对象的强引用,它本身就会被回收,并且永远不会被添加到队列中。在这个例子中,我使用了一个Set,一旦引用被清除,就删除它们(将它们留在Set中是内存泄漏)。

    虚引用

    虚引用不同于软引用和弱引用,它们不用于访问它们的被引用者。相反,他们的唯一目的是当它们的被引用者已经被回收时通知你。虽然这看起来毫无意义,但它实际上允许你比finalizers更灵活地执行资源清理。

    Finalizers的问题

    这篇文章中我更详细地讨论了finalizers。简而言之,你应该依靠try/catch/finally清理资源,而不是finalizers或虚引用。

    在对象生命周期的描述中,我提到finalizers有一些微妙的问题,使得它们不适合清理非内存资源。还有一些非微妙的问题,为了完整起见,我将在这里讨论。

    • finalizer可能永远不会被调用

      如果你的程序从未用完可用内存,那么垃圾回收器不会运行,你的finalizer也不会运行。对于长时间运行的应用程序(例如服务)来说,通常不会出现这个问题,但是短时间运行的程序可能会在没有运行垃圾收集的情况下完成。虽然有一种方法可以告诉JVM在程序退出之前运行finalizers,但这是不可靠的,可能会与其他shutdown hooks冲突。

    • Finalizers可能创建一个对象的其他强引用

      例如,通过将对象添加到集合中。这基本上复活了这个对象,但是,就像Stephen King's Pet Sematary一样,返回的对象“不太正确”。尤其是,当对象再次符合回收条件时,它的finalizer不会运行。也许你会使用这种复活技巧是有原因的,但是我无法想象,而且在代码上看起来会非常模糊。

    现在这些都已经过时了,我相信finalizers的真正问题是它们在垃圾回收器首次识别要回收的对象的时间和实际回收其内存的时间之间引入了间隙,因为finalization发生在它自己的线程上,独立于垃圾回收器的线程。JVM保证在返回OutMemoryError之前执行一次full collection,但是如果所有符合回收条件的对象都有finalizers,则回收将不起作用:这些对象保留在内存中等待finalization。假设一个标准JVM只有一个线程来处理所有对象的finalization,一些长时间运行的finalization,你就可以看到问题可能会出现。

    以下程序演示了这种行为:每个对象都有一个finalizer休眠半秒钟。不会有很长时间,除非你有成千上万的对象要清理。每个对象在创建后都会立即超出作用域,但是在某个时候你会耗尽内存(如果你想运行这个例子,我建议使用-Xmx64m来使错误快速发生;在我的开发机器上,有3Gb堆,实际上需要几分钟才能失败)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public class SlowFinalizer
    {
        public static void main(String[] argv) throws Exception
        {
            while (true)
            {
                Object foo = new SlowFinalizer();
            }
        }
    
        // some member variables to take up space -- approx 200 bytes
        double a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;
    
        // and the finalizer, which does nothing by take time
        protected void finalize() throws Throwable
        {
            try { Thread.sleep(500L); }
            catch (InterruptedException ignored) {}
            super.finalize();
        }
    }
    

    虚引用知晓之事

    当对象不再被使用时虚引用允许应用程序知晓,这样应用程序就可以清理对象的非内存资源。然而,与finalizers不同的是,当应用程序知道到这一点时,对象本身已经被收集了。

    此外,与finalizers不同,清理由应用程序而不是垃圾回收器来调度。您可以将一个或多个线程专用于清理,如果对象数量需要,可以增加线程数量。另一种方法——通常更简单——是使用对象工厂,并在创建新实例之前清理所有回收的实例。

    理解虚引用的关键点是,你不能使用引用来访问对象: get()总是返回null,即使对象仍然是强可达的。这意味着引用对象持有的不能是要清理的资源的唯一引用。相反,你必须维持对这些资源的至少一个其他强引用,并使用引用队列来通知被引用者已被回收。与其他引用类型一样,您的程序也必须拥有对引用对象本身的强引用,否则它将被回收,资源将内存泄露。

    用虚引用实现连接池

    数据库连接是任何应用程序中最宝贵的资源之一:它们需要时间来建立,并且数据库服务器严格限制它们将接受的同时打开的连接的数量。尽管如此,程序员对它们非常粗心,有时会为每个查询打开一个新的连接,或者忘记关闭它,或者不在finally块中关闭它。

    大多数应用服务部署使用连接池,而不是允许应用直接连接数据库:连接池维护一组打开的连接(通常是固定的),并根据需要将它们交给程序。用于生产环境的连接池提供了几种防止连接泄漏的方法,包括超时(识别运行时间过长的查询)和恢复被垃圾回收器回收的连接。

    下面这个连接池旨在演示虚引用,不能用于生产环境。Java有几个可用于生产环境的连接池,如Apache Commons DBCPC3P0

    后一个特性是虚引用的一个很好的例子。为了使它工作,连接池提供的Connection对象只是实际数据库连接的包装,可以在不丢失数据库连接的情况下回收它们,因为连接池保持对实际连接的强引用。连接池将虚引用与“包装成的”连接相关联,如果引用最终出现在引用队列中,则会将实际连接返回给连接池。

    连接池中最不有趣的部分是PooledConnection,如下所示。正如我说过的,它是一个包装,委派对实际连接的调用。不同的是我使用了反射代理来实现。JDBC接口随着Java的每一个版本而发展,其方式既不向前也不向后兼容;如果我使用了具体的实现,除非你使用了与我相同的JDK版本,否则你将无法编译。反射代理解决了这个问题,也使代码变得更短。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    
    public class PooledConnection
    implements InvocationHandler
    {
        private ConnectionPool _pool;
        private Connection _cxt;
    
        public PooledConnection(ConnectionPool pool, Connection cxt)
        {
            _pool = pool;
            _cxt = cxt;
        }
    
        private Connection getConnection()
        {
            try
            {
                if ((_cxt == null) || _cxt.isClosed())
                    throw new RuntimeException("Connection is closed");
            }
            catch (SQLException ex)
            {
                throw new RuntimeException("unable to determine if underlying connection is open", ex);
            }
    
            return _cxt;
        }
    
        public static Connection newInstance(ConnectionPool pool, Connection cxt)
        {
            return (Connection)Proxy.newProxyInstance(
                       PooledConnection.class.getClassLoader(),
                       new Class[] { Connection.class },
                       new PooledConnection(pool, cxt));
        }
        
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable
        {
            // if calling close() or isClosed(), invoke our implementation
            // otherwise, invoke the passed method on the delegate
        }
    
        private void close() throws SQLException
        {
            if (_cxt != null)
            {
                _pool.releaseConnection(_cxt);
                _cxt = null;
            }
        }
    
        private boolean isClosed() throws SQLException
        {
            return (_cxt == null) || (_cxt.isClosed());
        }
    }
    

    需要注意的最重要的一点是,PooledConnection同时引用了底层数据库连接和连接池。后者用于那些确实记得关闭连接的应用程序:我们希望立即告知连接池,以便底层连接可以立即被重用。

    getConnection()方法也值得一提:它的存在是为了捕捉那些在显式关闭连接后试图使用该连接的应用程序。如果连接已经交给另一个消费者,这可能是一件非常糟糕的事情。因此close()显式清除引用,getConnection()会检查该引用,并在连接不再有效时抛出异常。invocation handler用于所有委托调用。

    现在让我们将注意力转向连接池本身,从它用来管理连接的对象开始。

    1
    2
    3
    4
    5
    6
    
    private Queue<Connection> _pool = new LinkedList<Connection>();
    
    private ReferenceQueue<Object> _refQueue = new ReferenceQueue<Object>();
    
    private IdentityHashMap<Object,Connection> _ref2Cxt = new IdentityHashMap<Object,Connection>();
    private IdentityHashMap<Connection,Object> _cxt2Ref = new IdentityHashMap<Connection,Object>();
    

    当连接池被构造并存储在_pool中时,可用连接被初始化。我们使用引用队列_refQueue来标识已回收的连接。最后,我们有连接和引用之间的双向映射,在将连接返回到连接池时使用。

    正如我之前说过的,实际的数据库连接将在提交给应用程序代码之前被包装在PooledConnection中。这发生在wrapConnection()函数中,也是我们创建虚引用和连接-引用映射的地方。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    private synchronized Connection wrapConnection(Connection cxt)
    {
        Connection wrapped = PooledConnection.newInstance(this, cxt);
        PhantomReference<Connection> ref = new PhantomReference<Connection>(wrapped, _refQueue);
        _cxt2Ref.put(cxt, ref);
        _ref2Cxt.put(ref, cxt);
        System.err.println("Acquired connection " + cxt );
        return wrapped;
    }
    

    wrapConnection对应的是releaseConnection(),该函数有两种变体。当应用程序代码显式关闭连接时,PooledConnection调用第一个,这是“快乐的道路”,它将连接放回连接池中供以后使用。它还会清除连接和引用之间的映射,因为它们不再需要。请注意,此方法具有默认(包)同步:它由PooledConnection调用,因此不能是私有的,但通常不可访问。

    1
    2
    3
    4
    5
    6
    7
    
    synchronized void releaseConnection(Connection cxt)
    {
        Object ref = _cxt2Ref.remove(cxt);
        _ref2Cxt.remove(ref);
        _pool.offer(cxt);
        System.err.println("Released connection " + cxt);
    }
    

    另一个变体使用虚引用来调用,这是“可悲的道路”,当应用程序不记得关闭连接时才会调用。在这种情况下,我们得到的只是虚引用,我们需要使用映射来检索实际连接(然后使用第一个变体将其返回到连接池中)。

    1
    2
    3
    4
    5
    6
    
    private synchronized void releaseConnection(Reference<?> ref)
    {
        Connection cxt = _ref2Cxt.remove(ref);
        if (cxt != null)
            releaseConnection(cxt);
    }
    

    有一种边缘情况:如果引用在应用程序调用close()之后进入队列,会发生什么?这种情况不太可能发生:当我们清除映射时,虚引用应该已经有资格被回收,这样它就不会进入队列。然而,我们必须考虑这种情况,这导致上面的空检查:如果映射已经被移除,那么连接已经被显式返回,我们不需要做任何事情。

    好了,您已经看到了底层代码,现在是时候让应用程序调用唯一的方法了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    public Connection getConnection()
    throws SQLException
    {
        while (true)
        {
            synchronized (this) 
            {
                if (_pool.size() > 0)
                    return wrapConnection(_pool.remove());
            }    
    
            tryWaitingForGarbageCollector();
        }
    }
    

    getConnection()的最佳路径是在_pool中有可用的连接。在这种情况下,一个连接被移除、包装并返回给调用者。不信的情况是没有任何连接,在这种情况下,调用者希望我们阻塞直到有一个连接可用。这可以通过两种方式发生:要么应用程序关闭连接并返回到_pool中,要么垃圾回收器找到一个已被放弃的连接,并将其关联的虚引用加入队列。

    为什么我使用synchronized(this)而不是显式锁?简而言之,这个实现是作为教学辅助工具,我想用最少的样板来强调同步点。在生产环境使用的连接池中,我实际上会避免显式同步,而是依赖并行数据结构,如ArrayBlockingQueueConcurrentHashMap

    在走这条路之前,我想谈谈同步。显然,对内部数据结构的所有访问都必须同步,因为多个线程可能会尝试同时获取或返回连接。只要_pool中有连接,同步代码就能快速执行,竞争的可能性就很低。然而,如果我们必须循环直到连接变得可用,我们希望最大限度地减少同步的时间:我们不希望在请求连接的调用者和返回连接的另一个调用者之间造成死锁。因此,在检查连接时,使用显式同步块。

    那么,如果我们调用getConnection(),并且池是空的,会发生什么呢?这是我们检查引用队列以找到被废弃的连接的时机。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    private void tryWaitingForGarbageCollector()
    {
        try
        {
            Reference<?> ref = _refQueue.remove(100);
            if (ref != null)
                releaseConnection(ref);
        }
        catch (InterruptedException ignored)
        {
            // we have to catch this exception, but it provides no information here
            // a production-quality pool might use it as part of an orderly shutdown
        }
    }
    

    这个函数强调了另一组相互冲突的目标:如果引用队列中没有任何引用,我们不想浪费时间,但是我们也不想在一个循环中重复检查_pool_refQueue。所以我在轮询队列时使用了一个短暂的超时时间:如果没有准备好,它会给另一个线程返回连接的机会。当然,这也带来了一个公平性问题:当一个线程正在等待引用队列时,另一个线程可能会返回一个被第三个线程立即占用的连接。理论上,等待线程可能会永远等待。在现实世界中,由于不太需要数据库连接,这种情况不太可能发生。

    虚引用带来的问题

    前面我提到到finalizers不能保证被调用。虚引用也是这样,原因相同:如果回收器不运行,不可达的对象不会被回收,对这些对象的引用也不会进入队列。考虑一个程序只在循环中调用getConnection(),让返回的连接超出作用域,如果它没有做任何其他事情来让垃圾回收器运行,那么它会很快耗尽连接池然后阻塞,等待永远无法恢复的连接。

    当然,有办法解决这个问题。最简单的方法之一是在tryWaitingForGarbageCollector()中调用System.gc()。尽管围绕这种方法有一些争议,但这是促使JVM回到理想状态的有效方式。这是一种既适用于finalizers也适用于虚引用的技术。

    这并不意味着你应该忽略虚引用,只使用finalizer。例如,在连接池的情况下,你可能希望显式关闭该连接池并关闭所有底层连接。你可以用finalizer来完成,但是需要和虚引用一样多的工作。在这种情况下,通过引用获得的可控因素(相对于任意终结线程)使它们成为更好的选择。

    最后一些思考:有时候你只需要更多内存

    虽然引用对象是管理内存消耗的非常有用的工具,但有时它们是不够的,有时又是过度的。例如,假设你正在构建一些大型对象,其中包含从数据库中读取的数据。虽然你可以使用软引用作为读取的断路器,并使用弱引用将数据规范化,但最终您的程序需要一定量的内存来运行。如果你不能给它足够的内存来实际完成任何工作,那么不管你的错误恢复能力有多强都无济于事。

    应对OutOfMemoryError时你首先应该搞清楚它为什么会发生。可能你有内存泄露,可能仅仅是你内存的设置太低了。

    开发过程中,你应该指定大的堆内存大小——1G或更多——关注程序到底用了多少内存(这种情况jconsole是一个有用的工具)。大多数应用会在模拟的负载下达到一个稳定的状态,这将指引你的生产环境堆配置。如果你的内存使用随时间增长,那很可能你在对象不再使用后仍持有强引用,引用类型可能会有用,但更可能的是有bug需要修复。

    底线是你需要理解你的应用。如果没有重复,canonicalizing map对你没有帮助。如果你希望定期执行数百万行查询,软引用是没有用的。但是在可以使用引用对象的情况下,它们会是你的救命恩人。

    其他信息

    你可以下载这篇文章中的示例代码:
    CircuitBreakerDemo通过模拟数据库的结果集引出内存驱动的断路器。
    WeakCanonicalizingMap 用WeakHashMap创建了典范字符串。这个demo 可能更有趣: 它用极端的长度来触发垃圾回收(注意:在大的堆内存下运行可能不凑效,试试-Xmx100m).
    SlowFinalizer展示了如何在垃圾回收器运行的情况下耗尽内存。
    ConnectionPool 和 PooledConnection 实现了一个简单的连接池。ConnectionPoolDemo 通过内存型的HSQLDB数据库来运用这个连接池(这里 是构建这个和其他例子的Maven POM)。
    “string canonicalizer” 类可以在这下到SourceForge, licensed为Apache 2.0.
    Sun有许多关于调整他们JVM的内存管理的文章。这篇 文章是一篇精彩的介绍,并提供了其他文档的链接。
    Brian Goetz在IBM developerWorks网站上有一个极好的专栏,叫做”Java Theory and Practice”。几年前他写了关于软引用 和 弱引用 的专栏. 这些文章对一些我看过的议题影响很深,例如使用WeakHashMap来用不同生命时期关联对象。

    我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

    展开全文
  • JS数组对象,过滤掉不要的对象
  • 面向对象的概念和应用已超越了程序设计和软件开发,扩展到如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术、人工智能等领域。面向对象是一种对现实世界理解和抽象的方法,是...
  • 聊聊什么是对象存储?

    千次阅读 2020-04-11 09:43:42
    从来没接触过对象存储的可能有点蒙,对象存储是啥,使用场景是啥,还有没有文件系统POSIX哪些接口? 公有云厂商对对象存储的定义 AWS S3 Amazon Simple Storage Service (Amazon S3) 是一种对象存储服务,提供行业...
  • 里面有5-7个不同版本的设计模式ppt 请选择自己适合自己的 ppt讲解 23种设计模式 经典设计模式 面向对象设计模式
  • Java面向对象面试题总结

    万次阅读 多人点赞 2019-02-27 17:22:05
    答:(1)在类的定义中设置访问对象属性(数据成员)及方法(成员方法)的权限,限制本类对象及其他类的对象使用的范围。 (2)提供一个接口来描述其他对象的使用方法 (3)其他对象不能直接修改本对象所拥有的...
  • 实体值对象的含义 我们前面已经讲过领域的概念, 今天来讲讲实体, 实体是我们进行设计领域模型时的基础单元, 与之有关的是值对象, 接下来先梳理一下实体以及值对象的含义,然后讲讲他们俩的关系, 希望通过这篇文章能...
  • 面向对象的四个基本特征

    千次阅读 2017-04-13 20:19:14
    面向对象程序设计具有4个共同特征:抽象性、封装性、继承性和多态性。 1.抽象 抽象是人们认识事物的常用方法,比如地图的绘制。抽象的过程就是如何简化、概括所观察到的现实世界,并为人们所用的过程...
  • Java面向对象三大特性详解

    千次阅读 多人点赞 2019-10-24 11:49:51
    子类如果对继承的父类的方法不满意(不适合),可以自己编写继承的方法,这种方式就称为 方法的重写。当调用方法时会优先调用子类的方法。 重写要注意: a、返回值类型 b、方法名 c、参数类型及个数 ...
  • 【实战】到底什么是C语言对象编程?

    万次阅读 2020-09-01 20:36:40
    ID:技术让梦想更伟大作者:ZhengNL整理:李肖遥前言在之前肖遥分享写过一篇关于面都对象的文章,真的可以,用C语言实现面向对象编程OOP , 本篇肖遥给大家整理了ZhengNL三合一...
  • 本书适合于拥有一到两年开发经验的读者,有助于读者进一步地提升自己的开发能力,拓展和加深对.NET平台技术的认识,最终成长为一名优秀的.NET软件工程师。 掌握本书《基础篇》所介绍的内容,是进一步阅读本书《应用...
  • 面向过程编程不足之处就是它不适合某些种类问题的解决,例如图形化编程,在图形化编程中,客观世界由具体的对象(窗口、标签、按钮等)组成,无法自然的将函数与图形对象一一对应,因此面向过程编程不适合用于图形...
  • 面向对象深度解析对象的继承

    千次阅读 2018-06-21 00:24:41
    大纲介绍对象创建对象原型链区分prototype和__proto__对象的继承一、理解对象1.1 什么是对象对象是无需属性的集合,其属性可以包含基本值、对象、或者函数1.2 对象的属性?对象的属性类型分为数据属性和访问器属性...
  • 数组里面对象去重的3种方法

    万次阅读 多人点赞 2019-06-10 20:54:31
    数组里面对象去重的方法挺多的,下面列了几种方法任君选择! 下面是将要过滤的数据,将arr里面id重复的数据去掉(下面方法中用的arr都是这组数据哦)。 var arr = [ {id: 1, name: '周瑜1'}, {id: 3, name: '...
  • 文章目录C++类的对象和类的指针的区别指向地址的指针指针本身的大小指向数组的指针指针数组指向指针数组的指针多维指针数组函数参数中使用指针数组指针传址实现数组求和函数指针模仿C++ 类别函数指针数组对象指针...
  • c# -- 对象销毁和垃圾回收

    千次阅读 2018-08-09 15:15:45
    有些对象需要显示地销毁代码来释放资源,比如打开的文件资源,锁,操作系统句柄和非托管对象。在.NET中,这就是所谓的对象销毁,它通过IDisposal接口来实现。不再使用的对象所占用的内存管理,必须在某
  • 一、OBS是什么 ...OBS系统和单个桶都没有总数据容量和对象/文件数量的限制,为用户提供了超大存储容量的能力,适合存放任意类型的文件,适合普通用户、网站、企业和开发者使用。由于OBS是一项面向I...
  • 块存储、文件存储、对象存储

    千次阅读 2021-07-09 15:28:54
    区别 针对不同的应用场景,选择的分布式存储方案也会不同,因此有了对象存储、块存储、文件系统存储。这三者的主要区别在于它们的存储接口: 1. 对象存储: 也就是通常意义的键值存储,其接口就是简单的GET,PUT,DEL...
  • 文件存储NAS与对象存储OSS

    千次阅读 2021-02-04 15:44:47
    文件存储与对象存储 摘要:本文主要介绍文件存储NAS与对象存储OSS这2种目前主要的存储技术,以及差异,并介绍了各自的主要使用场景。 一、技术介绍 1.1、文件存储NAS 1.1.1概念 NAS(Network Attached ...
  • POPO(Persistant Object)可以看成是与数据库中的表相映射的java对象。最简单的PO就是对应数据库中某个表中的一条记录,多个记录可以用PO的集合。PO中应该不包含任何对数据库的操作。 好处就是可以把一条记录作为一个...
  • 问题: C4D整理工程技巧经验,C4D快速把对象放到最顶部,C4D快速找到对象位置。 答案: 在对象窗口和视图窗口按s键可以快速定位对象的...那就可以选中某个对象后CTRL+X,再在你想要放置的组里随便一个对象,按CTRL
  • C++语言类和对象有什么区别

    千次阅读 2017-07-07 08:58:53
    起初刚学C++时,很不习惯用new,后来看老外的程序,发现几乎都是使用new...所以,new有时候又不太适合,比如在频繁调用场合,使用局部new类对象就不是个好选择,使用全局类对象或一个经过初始化的全局类指针似乎更加高
  • C++面试题-面向对象-面向对象概念

    千次阅读 2019-01-11 00:16:27
    C++面试题-面向对象-面向对象概念 问:说说C++和C的主要区别? 答: C语言属于面向过程语言,通过函数来实现程序功能。而C++是面向对象语言,主要通过类的形式来实现程序功能。 使用C++编写的面向对象应用程序比...
  • 将JS对象转换为JSON字符串

    千次阅读 2019-12-26 09:23:49
    如果我用以下方法在JS中定义了一个对象: var j={"name":"binchen"}; 如何将对象转换为JSON? 输出字符串应为: '{"name":"binchen"}'

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 375,676
精华内容 150,270
关键字:

怎样选择适合自己的对象