ThreadLocal类之简单应用示例

在日常开发的系统中,日期处理是非常非常用的一个功能,处理的日期的时候就需要用到SimpleDateFormat对象,但是我们都知道SimpleDateFormat本身不是线程安全的(如果不知道的请看源码),所以就需要频繁创建SimpleDateFormat这个对象。但是我们知道创建这个对象本身不仅是很费时的,而且创建的这些对象存活期很短,导致内存中大量这样的对象需要被GC,所以我们自然而然的想到使用ThreadLocal来给每个线程缓存一个SimpleDateFormat实例,提高性能。下面是一个具体的实现的小例子,其实不仅针对SimpleDateFormat对象,对于数据库连接等等都可以这么使用。 package cn.bridgeli.demo; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Map; public class DateFormatFactory { private static final Map<DatePatternEnum, ThreadLocal<DateFormat>> pattern2ThreadLocal; static { DatePatternEnum[] patterns = DatePatternEnum.values(); int len = patterns.length; pattern2ThreadLocal = new HashMap<DatePatternEnum, ThreadLocal<DateFormat>>(len); for (int i = 0; i < len; i++) { DatePatternEnum datePatternEnum = patterns[i]; final String pattern = datePatternEnum.pattern; pattern2ThreadLocal.put(datePatternEnum, new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat(pattern); } }); } } // 获取DateFormat public static DateFormat getDateFormat(DatePatternEnum patternEnum) { ThreadLocal<DateFormat> threadDateFormat = pattern2ThreadLocal.get(patternEnum); // 不需要判断threadDateFormat是否为空 return threadDateFormat.get(); } } 对应的时间枚举类(如果还有其他格式的时间需要处理,可以直接在这个类里面添加即可): ...

June 18, 2017 · 1 min · 114 words · Bridge Li

ThreadLocal类之简单理解

当年实习的时候,当时公司一个相当有经验的工程师zeak带我们,从他那第一次听说了ThreadLocal类,但由于自己基础薄弱,没有理解到底怎么回事,工作中也没有用过,就一直没有太放在心上,刚好这一段时间不太忙,仔细玩了一下,欢迎高手批评。 ThreadLocal,线程本地变量。他为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。简单理解就是,对于非线程安全的变量在线程内部共享不用每次都new,是一种空间换时间的做法。ThreadLocal类提供的几个方法: public T get() { } public void set(T value) { } public void remove() { } protected T initialValue() { } 看名字就知道这些方法是干嘛的了,下面我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。首先是get方法的实现: public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } 先取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。如果获取成功,则返回value值。如果map为空,则调用setInitialValue方法返回value。那么getMap方法中又做了什么呢? ThreadLocalMap getMap(Thread t) { return t.threadLocals; } 原来是返回当前线程t中的一个成员变量threadLocals,而threadLocals则是: ThreadLocal.ThreadLocalMap threadLocals = null; 那就看看ThreadLocalMap的实现了: ...

June 11, 2017 · 2 min · 222 words · Bridge Li

Java集合类ArrayList删除特定元素

前一段时间入职新公司,熟悉公司系统原有代码的时候,发现公司代码那个烂啊,系统能正常跑,都不能用侥幸来形容,就是创造了一个奇迹。因为里面不仅没有coding style,而且竟然有很明显的常识性错误。其中当我一眼指出最明显的早就应该出过问题的一个地方,项目组几乎所有成员,是的,几乎全部成员,都说这个还真不知道,涨知识了,那就是:Java集合类ArrayList删除特定元素。发现原来不是所有人都知道怎么做,这难道不是最基础的吗?唉,真不知道这些系统是怎么跑起来的。我们首先看两种错误的写法,第一种: @Test public void testRemove1() { List<String> list = new ArrayList<String>() { private static final long serialVersionUID = 1L; { add("cn"); add("bridgeli"); add("blog"); } }; for (int i = 0, len = list.size(); i < len; i++) { String str = list.get(i); if ("cn".equals(str)) { list.remove(str); } } } 这个写法如果你不知道错在哪,那你得真的好好补基础了。由于这个错误比较明显,所以有人搞了下面这种写法,也是我们公司的同事犯的一个错误: @Test public void testRemove2() { List<String> list = new ArrayList<String>() { private static final long serialVersionUID = 1L; { add("cn"); add("bridgeli"); add("blog"); } }; for (String str : list) { if ("cn".equals(str)) { list.remove(str); } } } 跑一下这个例子看看,把cn换成bridgeli试试,出乎不出乎你的意料?下面我们就来简单探究一下原因foreach的原理,其实特别简单: ...

May 28, 2017 · 1 min · 191 words · Bridge Li

关于synchronized用法的简单理解

synchronized 关键字既可以用于声明方法,也可以用于声明代码块,他们之间有什么区别呢?下面让我们逐一测试一下。 先看以第一个例子: package demo; public class SynchronizedDemo1 { public synchronized static void foo1() { } public synchronized static void foo2() { } } 在这个例子中,foo1 和 foo2 是类的两个静态方法。在不同的线程中,这两个方法的调用时互斥的,不仅是他们之间,任何两个不同的线程的调用也互斥。下面看第二个例子: package demo; public class SynchronizedDemo2 { public synchronized void foo3() { } public synchronized void foo4() { } } 在这个例子中,foo3 和 foo4 是类的两个成员方法,在多线程环境中,调用同一个对象的 foo3 或者 foo4 是互斥的,与上一个例子的差别在于,这是针对同一个对象的多线程方法调用互斥。下面再看最后一个例子: package demo; public class SynchronizedDemo3 { public void foo5() { synchronized (this) { } } public void foo6() { synchronized (SynchronizedDemo3.class) { } } } 在这个例子中,synchronized 用来修饰代码块,需要注意的是:synchronized 后面会有一个参数,其实这个就是用于同步的锁所属的对象。在这个例子中 synchronized (this) 与 SynchronizedDemo2 中加 synchronized 的成员方法是互斥的,而 synchronized (SynchronizedDemo3.class) 与 SynchronizedDemo1 中加 synchronized 的静态方法是互斥的。synchronized 用于修饰代码块会更加灵活,因为除了前面的这个例子外,synchronized 后面的参数可以是任意对象。

May 14, 2017 · 1 min · 99 words · Bridge Li

Java GC之常见监控可视化工具总结(下)

上一篇文章总结一下监控和分析的常见命令,那些是基础,但是有些同学看到命令行就害怕,所以这篇文件总计一下两个常用的可视化工具。 JConsole JConsole工具在JDK/bin目录下,启动JConsole后,将自动搜索本机运行的jvm进程,不需要jps命令来查询指定。双击其中一个jvm进程即可开始监控,也可使用“远程进程”来连接远程服务器 进入JConsole主界面,有“概述”、“内存”、“线程”、“类”、“VM摘要”和”Mbean”六个页签: 内存页签相当于jstat命令,用于监视收集器管理的虚拟机内存(Java堆和永久代)变化趋势,还可在详细信息栏观察全部GC执行的时间及次数 线程页签:线程长时间停顿的主要原因有:等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待(活锁和死锁) 最后一个常用页签,VM页签,可清楚的了解显示指定的JVM参数及堆信息 VisualVM:多合一故障处理工具 VisualVM是一个集成多个JDK命令行工具的可视化工具。VisualVM基于NetBeans平台开发,它具备了插件扩展功能的特性,通过插件的扩展,可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等。VisualVM在JDK/bin目录下 ①. 安装插件: 工具- 插件 ②. 监控垃圾回收 在左侧的“Application”测看下,有个“Local”节点,所有本地正在运行的Java应用都将罗列在这里。Java VisualVM是一个Java应用。所以,它将自己也列在这里。为了方便学习,我们将监控Java VisualVM自身的垃圾回收过程。双击“Local”节点下的VisualVM图标,现在,应用监视窗口在右侧打开。我们关注的是“Visual GC”,点击它 再配合其他的标签页,例如“Threads”以及线程转储你,我们就可以深入详细地了解这方面的内容。在“监视”标签页,我们可以监控整个堆内存的使用情况,这些都不贴图了,大家可以随便玩。 ③. 在VisualVM中生成dump文件 参考资料:周志明《深入理解Java虚拟机》第二版第四章

April 4, 2017 · 1 min · 23 words · Bridge Li

Java GC之常见监控与分析命令总结(上)

上一篇文章简单写了几种常见的垃圾收集器的参数设置,设置参数的时候离不开对对系统进行监控和分析,所以总结一下监控和分析的常见命令。 jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程 命令格式: jps \[options\] \[hostid\] hostid为RMI注册表中注册的主机名,其他常用参数如下: -q:只输出LVMID,省略主类的名称 -m:输出虚拟机进程启动的时候传递给主类main()方法的参数 -l:输出主类的全名,如果进程执行的是jar包,输出jar路径 -v:输出虚拟机进程启动时JVM参数 命令执行样例: jps -l jstat:JVM Statistics Monitoring Tool,用于收集Hotspot虚拟机各方面的运行数据 命令格式: jstat \[option vmid [interval[s|ms\] \[count\]]] 对于命令格式中的VMID和LVMID,如过是本地虚拟机进程,VMID和LVMID是一致的,如果是远程虚拟机,那VMID的格式应当是: \[protocol:\] \[//\] lvmid[@hostname[:port]/servername] interval和count代表查询的间隔和次数,如果省略这两个参数,说明只查一次,其他常用参数: -class:监视装载类、卸载类、总空间以及类装载所耗费的时间 -gc:监视java堆状况,包括eden区、两个survivor区、老年代、永久代等的容量、已用空间、GC时间合计信息 -gccapacity:监视内容与-gc基本相同,但输出主要关注java堆各个区域使用到最大、最小空间 -gcutil:监视内容与-gc基本相同,但输出主要关注已使用控件占总空间的百分比 -gccause:与-gcutil功能一样,但是会额外输出导致上一次gc产生的原因 -gcnew:监视新生代GC情况 -gcnewcapacity:监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间 -gcold:监视老年代GC情况 -gcoldcapacity:监视内容与-gcold基本相同,输出主要关注使用到的最大、最小空间 -gcpermcapacity:输出永久代使用到的最大、最小空间 -compiler:输出JIT编译过的方法、耗时等信息 -printcompilation:输出已经被JIT编译过的方法 命令执行样例: jstat -gcutil 2764 1000 jinfo:Configuration Info for Java,显示虚拟机的配置信息 使用jps命令的-v参数可以查看虚拟机启动时显示指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除了去找资料以外,就得使用jinfo的-flag选项,命令格式: jinfo [option] pid 执行样例:查询CMSInitiatingOccupancyFraction参数值 jinfo -flag CMSInitiatingOccupancyFraction 2764 jmap:Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件) 命令格式: ...

March 19, 2017 · 1 min · 159 words · Bridge Li

Java GC之常见垃圾收集器参数总结

上一篇文章简单写了几种常见的垃圾收集器,俗话说,好记性不如烂笔头,今天总结一下这些垃圾收集器的参数总结,供自己和需要的读者将来查阅 -XX:+UseSerialGC : Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 -XX:+UseParNewGC : 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收 -XX:+UseConcMarkSweepGC : 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用 -XX:+UseParallelGC : Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收 -XX:+UseParallelOldGC : 使用Parallel Scavenge + Parallel Old的收集器组合进行回收 -XX:SurvivorRatio : 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1 -XX:PretenureSizeThreshold : 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 -XX:MaxTenuringThreshold : 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代 -XX:UseAdaptiveSizePolicy : 动态调整java堆中各个区域的大小以及进入老年代的年龄 -XX:+HandlePromotionFailure : 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 -XX:ParallelGCThreads : 设置并行GC进行内存回收的线程数 -XX:GCTimeRatio : GC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效 -XX:MaxGCPauseMillis : 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效 ...

March 5, 2017 · 1 min · 96 words · Bridge Li

Java GC之常见垃圾收集器

上一篇文章简单写了JVM的常见垃圾回收算法,今天就让我们看看根据这些算法有哪些常见的垃圾收集器,他们有什么特点,然后根据自己的应用特点和要求组合出各个年代所使用的收集器。 上图展示了JDK1.7Update14之后的HotSpot虚拟机的7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器 Serial收集器 Serial收集器是最基本、发展历史最悠久的收集器,在JDK 1.3.1之前是虚拟机新生代收集的唯一选择。见名知意它是一个单线程的收集器,“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,也就是传说中的Stop The World,简称STW。那么它是不是已经被淘汰的一个垃圾收集器呢,事实上并不是,由于与其他收集器的单线程比简单而高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。所以Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。它的运行示意图如下: ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本,是一个并行垃圾收集器,并行的含义是:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,还有一个和性能无关的但是很重要的原因:除了Serial收集器外,目前只有它能与CMS收集器配合工作。和Serial收集器相比,ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。其运行示意图如下: Parallel Scavenge收集器 Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。那么他有什么应用场景呢?事实上Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,其他收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),所以由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。所以它的应用场景体现在:停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。除此之外和ParNew收集器相比,它具有自适应调节策略。Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics),其运行示意图如下: Serial Old收集器 Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。它的主要应用场景:1. 给Client模式下的虚拟机使用(还记得Client模式下新生代的默认垃圾收集器是啥吗?);2. 在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。运行示意图见Serial垃圾收集器 Parallel Old收集器 Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合。运行示意图见Parallel Scavenge垃圾收集器 CMS收集器 在JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。并发的含义就是:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上,他可不用同于并行,并行会有STW,并发几乎没有STW。CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。它的运行示意图如下: 从图上可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤: ①. 初始标记(CMS initial mark) 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。 ...

February 26, 2017 · 1 min · 152 words · Bridge Li

Java GC之垃圾回收算法

上一篇文章简单写了一下JVM如何判断一个对象是否已经死了,当判断出一个对象已经死了之后,接下来就要进行垃圾回收了,所以在进行垃圾回收之前,先让我们看看垃圾回收的算法有哪些。 标记-清除算法 标记清除见名知意该算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象,至于如何标记就是上一篇文章中所讲的方法了。这种算法比较简单,容易理解,同时也是一个最基础的垃圾回收算法,后面所讲的算法都是对他的改进,至于为什么需要改进,因为他主要存在两个不足: ①. 效率问题,标记和清除两个阶段的效率都不高; ②. 空间问题,标记清除之后会留下大量的不连续的内存碎片,内存碎片会导致当后面在程序的运行过程中可能需要给较大的对象分配空间时,无法找到足够的内存而不得不提前触发另一次垃圾回收。这种算法的执行过程如下图: 复制算法 复制算法为了解决标记清除算法的效率问题:它将内存分为容量大小相同的两块,每次只使用其中一块,当这一块的内存空间用完了,他就讲里面存活着的对象复制到另一块上面,然后再把已使用过的这一块内存空间一次性清理掉,这样使得每次都是对半个内存区域进行会回收,内存分配时也不用考虑空间碎片的问题,只需要移动指针,按顺序分配即可,实现简单,运行高效。但是他也有缺点:每次使用的内存空间只有整个空间的一半,这“浪费”有点高啊。复制算法的执行过程如下图: 不过现代的商业JVM都采用了这种算法来回收新生代,究其原因不仅仅他解决了效率问题,更是经研究表明:新生代中的对象高达98%都是“朝生夕死”的,所以这样一来就不需要1:1来划分空间了。直接将内存空间分为一块较大的Eden区和两块较小的Survivor区,每次使用Eden和其中一块Survivor区,当需要回收时,直接将Eden和Survivor中还存活的对象一次性的复制到另一块Survivor区,最后在清理Eden和刚使用过的Survivor。HotSpot虚拟机Eden和Survivor的大小比例默认为8:1,也就是每次使用的空间大小是90%,只“浪费”了10%。但是98%的对象回收也不能保证,每次存活的对象所使用的空间小于10%,所以当Survivor空间不够时,就需要其他空间(一般是老年代)进行分配担保,如果另一块Survivor空间没有足够的空间存放上一次新生代垃圾回收存活的对象时,这些对象将直接通过分配担保机制进入老年代。 标记-整理算法 复制算法适用于新生代的对象“朝生夕死”,如果一个区域内的对象老是不死,不仅内次都需要复制大量的对象,效率很低,而且还需要额外的空间进行担保(分配担保是一个很复杂的东西,今后有机会会说到),所以对于老年代的对象,这种算法是不适合的,于是就提出了标记-整理算法。 标记-整理算法和标记-清除算法一样,也是分两个阶段,而且第一个阶段也一样,都是标记,所不同的是第二个阶段,不是对可回收的对象进行直接清理,而是让还存活着的对象向一端移动,然后直接清理掉端边界以为的内存空间,该算法的执行过程如下图: 分代收集算法 当前的商业JVM都是采用的这种算法,其实这种算法并没有什么新思想,而是根据对象存活周期的不同将内存划分几块,一般是把Java堆分为两块:新生代和老年代,这样就可以根据各个代的不同特点采取最适当的垃圾收集算法。新生代的对象大多都是“朝生夕死”的,而且还可以有老年代进行担保,那就采用复制算法,只需要付出少量的存活对象的复制成本就可以完成收集,而且一般也不需要启用担保策略,而老年代的对象存活率一般比较高、没有空间进行担保,就只有采用“标记-清理”或者“标记-整理”算法来进行回收了。 参考资料:周志明《深入理解Java虚拟机》第二版第三章

February 19, 2017 · 1 min · 16 words · Bridge Li

Java GC之对象已死吗

差不多两年以前曾经写过一篇文章:JAVA 性能调优,其实在那篇文章中只是简单的说了,对象的分布。这篇文章继续对分布于堆中的对象的生命周期进行说明,也就是确定堆中的这些对象哪些还是“活着”的,哪些是已经“死去”(即不可能再被任何途径使用的对象)的。 引用计数算法 有很多人认为判断对象是否活着的算法是这样的:给对象添加一个引用计数器,每当有一个地方引用他的时候,计数器就加1,引用失效的时候,计数器减1,当计数器的数值为0时就是不可能在被引用的对象,此时就就可以认为是已死的对象。引用计数器算法实现简单,效率也很高,是一个不错的算法,但是主流的Java虚拟机并没有采用这种算法来管理内存,其中最主要的原因就是:它很难解决对象之间循环引用的问题。 举一个简单的例子:对象objA和objB都有字段instance,赋值令,除此之外,这两个对象再无任何引用,实际上他们已经不可能在被访问到,但是他们因为相互引用对方,计数器都不可能为0,计数器算法是无法通知GC收集器收集他们的。 package demo; /** * testGC()方法执行后,objA和objB会不会被GC呢? * * @author BridgeLi * */ public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; // 这个成员的唯一意义就是占用内存,以便能在GC日志中看清楚是否被回收过 private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; // 假设发生了GC,看objA和objB是否能被回收 System.gc(); } public static void main(String[] args) { ReferenceCountingGC.testGC(); } } 从这个例子的运行结果来看,虚拟机并没有这两个对象存在相互引用就不收集他们,从而证明了Java虚拟机不是通过引用计数算法来判断对象是否已死的。 ...

February 11, 2017 · 2 min · 215 words · Bridge Li