JVM 总结

164 阅读35分钟

image.png

image.png

image.png

image.png

java内存分配

寄存器器:我们⽆无法控制 静态域:static定义的静态成员

常量量池:编译时被确定并保存在.class⽂文件中的(final)常量量值和⼀一些⽂文本修饰的符号引⽤用 (类和接 口的全限定名,字段的名称和描述符,方法和名称和描述符)

非ram存储:硬盘等永久存储空间 堆内存:new创建的对象和数组,由java虚拟机⾃自动垃圾回收器器管理理,存取速度慢

栈内存:基本类型的变量量和对象的引⽤用变量量(堆内存空间的访问地址),速度快,可以共 享,但是⼤大⼩小与⽣生存期必须确定,缺乏灵活性

Java堆的结构是什么样子的?什么是堆中的永久代(Perm Gen space)?

JVM的堆是运⾏时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时 候 被创建。 对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。 堆内存是由存活和死亡的对象组成的。存活的对象是应⽤用可以访问的,不会被垃圾回收。 死亡的对象是应⽤用不可访问尚且还没有被垃圾收集器器回收掉的对象。


如何判断⼀一个对象是否存活?(或者GC对象的判定⽅方法)(判断⼀一个对象是否存活有两种⽅方法)

引⽤用计数法:

所谓引⽤用计数法就是给每⼀一个对象设置⼀一个引⽤用计数器器,每当有⼀一个地⽅方引⽤用这个对 象时,就将计数器器加⼀一,引⽤用失效时,计数器器就减⼀一。 当⼀一个对象的引⽤用计数器器为零时,说明此对象没有被引⽤用,也就是“死对象”,将会被垃 圾回收.引⽤用计数法有⼀一个缺陷就是⽆无法解决循环引⽤用问题,也就是说当对象A引⽤用对 象B,对象B⼜又引⽤用者对象A,那么此时A,B对象的引⽤用计数器器都不不为零,也就造成⽆无法 完成垃圾回收,所以主流的虚拟机都没有采⽤用这种算法。

可达性算法(引⽤用链法) 该算法的思想是:从⼀一个被称为GC Roots的对象开始向下搜索,如果⼀一个对象到GC Roots没有任何引⽤用链相连时,则说明此对象不不可⽤用。

在java中可以作为GC Roots的对象有以下⼏几种:

  • 虚拟机栈中引⽤的对象
  • 方法区类静态属性引用的对象
  • ⽅法区常量量池引⽤的对象
  • 本地方法栈JNI引⽤的对象

虽然这些算法可以判定⼀一个对象是否能被回收,但是当满⾜足上述条件时,一个对象⽐不一 定会被回收。当⼀个对象不可达GC Root时,这个对象并不会⽴马被回收,⽽是出于⼀个死 缓的阶段,若要被真正的回收需要经历两次标记. 如果对象在可达性分析中没有与GC Root的引⽤用链。

那么此时就会被第⼀次标记并且进⾏一次筛选,筛选的条件是是否有必要执⾏finalize()方法。 当对象没有覆盖finalize()⽅法或者已被虚拟机调⽤过,那么就认为是没必要的。 如果该对象有必要执行finalize()方法,那么这个对象将会放在⼀个称为F-Queue的对 队列列中,虚拟机会触发⼀个Finalize()线程去执行,此线程是低优先级的,并且虚拟机 不会承诺一直等待它运⾏完 这是因为如果finalize()执⾏缓慢或者发⽣了死锁,那么就会造成F-Queue队列一直等 待,造成了了内存回收系统的崩溃。 GC对处于F-Queue中的对象进⾏第⼆次被标记,这时,该对象将被移除”即将回收”集 合,等待回收。

垃圾回收器的基本原理理是什么? 垃圾回收器器可以⻢上回收内存吗?有什么办法主动通知虚拟机进⾏垃圾回收

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、⼤小以及使用情况。 通常,GC采⽤有向图的⽅式记录和管理堆(heap)中的所有对象。 通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。 当GC确定⼀些对象为"不可达"时,GC就有责任回收这些内存空间。可以。 程序员可以⼿动执⾏System.gc(),通知GC运⾏,但是Java语言规范并不保证GC⼀定会执 ⾏。

内存泄漏排查

内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小,目前来说,常遇到的泄漏问题如下:

1. 年老代堆空间被占满

年老代堆空间被占满 异常: java.lang.OutOfMemoryError: Java heap space

这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。这种情况一般来说是因为内存泄漏或者内存不足造成的。某些情况因为长期的无法释放对象,运行时间长了以后导致对象数量增多,从而导致的内存泄漏。另外一种就是因为系统的原因,大并发加上大对象,Survivor Space区域内存不够,大量的对象进入到了老年代,然而老年代的内存也不足时,从而产生了Full GC,但是这个时候Full GC也无发回收。这个时候就会产生java.lang.OutOfMemoryError: Java heap space

解决方案如下:

  1. 代码内的内存泄漏可以通过一些分析工具进行分析,然后找出泄漏点进行改善。
  2. 第二种原因导致的OutOfMemoryError可以通过,优化代码和增加
  3. Survivor Space
  4. 等方式去优化。

2. 持久代被占满

持久代被占满 异常:java.lang.OutOfMemoryError: PermGen space

Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。 解决方案:

  1. 增加持久代的空间 -XX:MaxPermSize=100M。
  2. 如果有自定义类加载的需要排查下自己的代码问题。

堆栈溢出

堆栈溢出 异常:java.lang.StackOverflowError

一般就是递归没返回,或者循环调用造成

3. 线程堆栈满

线程堆栈满 异常:Fatal: Stack size too small

java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。 解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。

4. 系统内存被占满

系统内存被占满 异常:java.lang.OutOfMemoryError: unable to create new native thread

这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。 分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。 解决:

  1. 重新设计系统减少线程数量。 2. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。

内存泄漏常用分析工具🔧

0x01:JDK自带工具

在处理内存泄露方面JDK本身就自带了大量监控、分析工具,主要有如下一些:

  • jps:可查看当前系统运行的所有java进程
  • jstat:查看具体某个Java进程的GC情况
  • jmap: 查看某个Java进程的堆内存使用情况
  • jvisualvm:可视化查看堆内存与metaspace占用情况
  • jstack:查看具体某个java进行的线程堆栈情况

0x02:Eclipse Memory Analyzer

官网有如下一段英文介绍:

The Eclipse Memory Analyzer is a fast and feature-rich Java heap analyzer that helps you find memory leaks and reduce memory consumption.

Use the Memory Analyzer to analyze productive heap dumps with hundreds of millions of objects, quickly calculate the retained sizes of objects, see who is preventing the Garbage Collector from collecting objects, run a report to automatically extract leak suspects.

大致的意思如下:

Eclipse Memory Analyzer(简称MAT)是一个快速且功能丰富的Java堆分析器,可帮助您查找内存泄漏并减少内存消耗。使用Memory Analyzer分析具有数亿个对象的高效堆转储,快速计算对象的保留大小,查看谁阻止垃圾收集器收集对象,运行报告以自动提取泄漏嫌疑者。

官网地址:www.eclipse.org/mat/ 复制代码

这款工具在分析内存泄露方面非常好用,可以图形化展示通过jmap命令打印出来的内存快照,而且是免费的。

0x03:JProfiler

JProfiler 是一个商用的主要用于检查和跟踪系统(限于Java开发的)的性能的工具。JProfiler可以通过实时的监控系统的内存使用情况,随时监视垃圾回收,线程运行状况等手段,从而很好的监视JVM运行情况及其性能。专用于分析J2SE和J2EE应用程序。它把CPU、执行绪和内存的剖析组合在一个强大的应用中。JProfiler提供许多与IDE整合和应用服务器整合的插件。JProfiler可视化的让用户可以找到效能瓶颈、抓出内存漏失(memory leaks)、并解决执行绪的问题。

官网地址:www.ej-technologies.com/products/jp… 复制代码

JProfiler唯一的不好就是需要购买版权;但是功能还是杠杠的。

0x04:Arthas

首先阿里出品必属精品,Arthas当然也非常好用。

Arthas是一款阿里巴巴开源的 Java 线上诊断工具,功能非常强大,可以解决很多线上不方便解决的问题。Arthas诊断使用的是命令行交互模式,支持JDK6+,Linux、Mac、Windows 操作系统,命令还支持使用 tab 键对各种信息的自动补全,诊断起来非常利索。

代码仓库地址:github.com/alibaba/art… 复制代码

从官网可以知道Arthas有以下一些特征

  • 检查是否加载了类,或在哪里加载了类。(对于解决jar文件冲突很有用)
  • 反编译一个类以确保代码按预期运行。
  • 查看类加载器统计信息,例如,类加载器的数量,每个类加载器加载的类的数量,类加载器的层次结构,可能的类加载器泄漏等。
  • 查看方法调用详细信息,例如方法参数,返回对象,引发的异常等。
  • 检查指定方法调用的堆栈跟踪。当开发人员想知道所述方法的调用者时,这很有用。
  • 跟踪方法调用以查找慢速子调用。
  • 监视方法调用统计信息,例如qps,rt,成功率等。
  • 监视系统指标,线程状态和cpu使用情况,gc统计信息等。
  • 支持命令行交互模式,并启用了自动完成功能。
  • 支持telnet和websocket,可通过命令行和浏览器启用本地和远程诊断。
  • 支持分析器/火焰图
  • 支持JDK 6+。
  • 支持Linux / Mac / Windows。

0x05:操作系统命令

无论怎么说,操作系统本身的命令是基础。通过Linux本身的一些命令也是可以对java进行一定程度的检查与检测的。不过根据不同的Linux版本,使用的命令可能存在一些差异,下面以CentOS为例说明:

  • free:查看内存占用、剩余情况
  • top:实时监控所有进程的内存、CPU、IO等情况
  • lsof:该命令的功能很多,其中有一项功能可以查看某个进程打开的文件句柄情况
  • Linux操作系统的命令非常多,涉及到内存、io、网络、磁盘等情况都可以找到相关命令来监控。具体场景再去查找相关资料进行相关处理。

0x06:GCViewer

GCViewer是一款实用的GC日志分析软件,免费开源使用,不过需要先安装jdk或者java环境才可以使用,因为它本身就是Java语言开发的。软件为GC日志分析人员提供了强有力的功能支持,大大提高分析GC日志的效率。作者:BUG弄潮儿链接:juejin.cn/post/690942…

finalize()⽅法什么时候被调用?析构函数(finalization)的⽬的是什么?

垃圾回收器(garbage colector)决定回收某对象时,就会运⾏该对象的finalize()⽅法 但是在 Java中很不幸,如果内存总是充足的,那么垃圾回收可能永远不会进⾏,也就是说filalize() 可能永远不不被执⾏,显然指望它做收尾⼯作是靠不住的。 那么finalize()究竟是做什什么的呢? 它最主要的用途是回收特殊渠道申请的内存。 Java程序有垃圾回收器,所以一般情况下内存问题不⽤程序员操心。 但有一种JNI(Java Native Interface)调用non-​Java程序(C或C++),finalize()的工作就是回收这部分的内存。

官网地址:www.tagtraum.com/gcviewer.ht…

如果对象的引⽤被置为null,垃圾收集器是否会立即释放对象占⽤的内存?

不会,在下⼀个垃圾回收周期中,这个对象将是可被回收的。

疑问:GC Root 要经历两次标记的原因吗

垃圾收集器

0

上面是目前比较常用的垃圾收集器,和他们直接搭配使用的情况,上面是新生代收集器,下面则是老年代收集器,这些收集齐都有自己的特点,根据不同的业务场景进行搭配使用。

收集器

1.1 Serial收集器

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)也就是传说中的Stop The World。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

1.2 ParNew收集器

ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。 使用方式可以使用-XX:+UseConcMarkSweepGC,或者是使用-XX:+UseParNewGC来强制开启,可以通过-XX:ParallelGCThreads 来调整或者限制垃圾收集的线程数量。

1.3 Parallel Scavenge收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

Parallel Scavenge提供了两个参数用来精确控制,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间

Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。除上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

1.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中详细讲解。

1.5 Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择(还记得上面说过Parallel Scavenge收集器无法与CMS收集器配合工作吗?)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。

直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

1.5CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。

CMS是一款基于“标记—清除”算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

1.6 G1收集器

G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点。

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

2 总结

各个收集器都有自己适应的业务场景,一般来说在生产中都会使用ParNew+CMS的方式分代收集回收。针对于新生代来说,根据其特性,总的来说还是使用复制算法。而针对于老年代来说算法一般会使用两种,标记-清除和标记-整理,每一种都有自己适用的场景。但是在目前我遇到的生产中还是使用CMS 标记清除的这种比较多,也是比较常用的,虽然标记-清除这种算法会导致大量的内存碎片,但是也是可以通过一种暴力的方式解决,就是FULL GC,G1收集器是比较完美的一种收集器,但是在目前的生产中依旧用得很少。

什么是分布式垃圾回收(DGC)?它是如何⼯作的?

DGC叫做分布式垃圾回收。 RMI使⽤用DGC来做自动垃圾回收。 因为RMI包含了跨虚拟机的远程对象的引用,垃圾回收是很困难的。 DGC使⽤引⽤计数算法来给远程对象提供⾃动 内存管理。

强引用、弱引用、软引用和虚引用

Java执行GC判断对象是否存活有两种方式其中一种是引用计数。

引用计数:Java堆中每一个对象都有一个引用计数属性,引用每新增1次计数加1,引用每释放1次计数减1。

在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于(reachable)可达状态,程序才能使用它。

从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

正文

(一) 强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:

Object strongReference = new Object();

当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

如果强引用对象不使用时,需要弱化从而使GC能够回收,如下:

strongReference = null;

显式地设置strongReference对象为null,或让其超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于GC算法。

public void test() { Object strongReference = new Object(); }

在一个方法的内部有一个强引用,这个引用保存在Java栈中,而真正的引用内容(Object)保存在Java堆中。

当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象会被回收。

但是如果这个strongReference是全局变量时,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。

ArrayList的Clear方法:

在ArrayList类中定义了一个elementData数组,在调用clear方法清空数组时,每个数组元素被赋值为null。

不同于elementData=null,强引用仍然存在,避免在后续调用add()等方法添加元素时进行内存的重新分配。

使用如clear()方法内存数组中存放的引用类型进行内存释放特别适用,这样就可以及时释放内存。

(二) 软引用(SoftReference)

如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

软引用可用来实现内存敏感的高速缓存。

// 强引用

String strongReference = new String("abc");

// 软引用

String str = new String("abc"); SoftReference softReference = new SoftReference(str);

软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

ReferenceQueue referenceQueue = new ReferenceQueue<>();

String str = new String("abc");

SoftReference softReference = new SoftReference<>(str, referenceQueue);

str = null;

// Notify GC

System.gc();

System.out.println(softReference.get()); // abc

Reference reference = referenceQueue.poll();

System.out.println(reference); //null

注意:软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的。就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。

当内存不足时,JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:


if(JVM内存不足) {

// 将软引用中的对象引用置为null

str = null;

// 通知垃圾回收器进行回收

System.gc();

}

也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的“较新的”软对象会被虚拟机尽可能保留,这就是引入引用队列ReferenceQueue的原因。

应用场景:

浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;

如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。

这时候就可以使用软引用,很好的解决了实际的问题:

// 获取浏览器对象进行浏览

Browser browser = new Browser();

// 从后台程序加载浏览页面

BrowserPage page = browser.getPage();

// 将浏览完毕的页面置为软引用

SoftReference softReference = new SoftReference(page);

// 回退或者再次浏览此页面时

if(softReference.get() != null) {

// 内存充足,还没有被回收器回收,直接获取缓存

page = softReference.get();

} else {

// 内存不足,软引用的对象已经回收

page = browser.getPage();

// 重新构建软引用

softReference = new SoftReference(page);

}


(三) 弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

String str = new String("abc");

WeakReference weakReference = new WeakReference<>(str);

str = null;



JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:

str = null;

System.gc();


注意:如果一个对象是偶尔(很少)的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用Weak Reference来记住此对象。

下面的代码会让一个弱引用再次变为一个强引用:

String str = new String("abc");WeakReference weakReference = new WeakReference<>(str);// 弱引用转强引用

String strongReference = weakReference.get();

同样,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

简单测试:

GCTarget.java

public class GCTarget {

// 对象的ID

public String id;

// 占用内存空间

byte[] buffer = new byte[1024];

public GCTarget(String id) {

this.id = id;

}

protected void finalize() throws Throwable {

// 执行垃圾回收时打印显示对象ID

System.out.println("Finalizing GCTarget, id is : " + id);

}

}


GCTargetWeakReference.java

public class GCTargetWeakReference extends WeakReference {

// 弱引用的ID

public String id;

public GCTargetWeakReference(GCTarget gcTarget,

ReferenceQueue queue) {

super(gcTarget, queue);

this.id = gcTarget.id;

}

protected void finalize() {

System.out.println("Finalizing GCTargetWeakReference " + id);

}

}

WeakReferenceTest.java

public class WeakReferenceTest {

// 弱引用队列

private final static ReferenceQueue REFERENCE_QUEUE = new ReferenceQueue<>();

public static void main(String[] args) {

LinkedList gcTargetList = new LinkedList<>();

// 创建弱引用的对象,依次加入链表中

for (int i = 0; i < 5; i++) {

GCTarget gcTarget = new GCTarget(String.valueOf(i));

GCTargetWeakReference weakReference = new GCTargetWeakReference(gcTarget,

REFERENCE_QUEUE);

gcTargetList.add(weakReference);

System.out.println("Just created GCTargetWeakReference obj: " +

gcTargetList.getLast());

}

// 通知GC进行垃圾回收

System.gc();

try {

// 休息几分钟,等待上面的垃圾回收线程运行完成

Thread.sleep(6000);

} catch (InterruptedException e) {

e.printStackTrace();

}

// 检查关联的引用队列是否为空

Reference reference;

while((reference = REFERENCE_QUEUE.poll()) != null) {

if(reference instanceof GCTargetWeakReference) {

System.out.println("In queue, id is: " +

((GCTargetWeakReference) (reference)).id);

}

}

}

}

运行WeakReferenceTest.java,运行结果如下:

可见WeakReference对象的生命周期基本由垃圾回收器决定,一旦垃圾回收线程发现了弱引用对象,在下一次GC过程中就会对其进行回收。

(四) 虚引用(PhantomReference)

虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

应用场景:

虚引用主要用来跟踪对象被垃圾回收器回收的活动。

虚引用与软引用和弱引用的一个区别在于:

虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

String str = new String("abc");

ReferenceQueue queue = new ReferenceQueue();

// 创建虚引用,要求必须与一个引用队列关联

PhantomReference pr = new PhantomReference(str, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

总结

Java中4种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用

当垃圾回收器回收时,某些对象会被回收,某些不会被回收。垃圾回收器会从根对象Object来标记存活的对象,然后将某些不可达的对象和一些引用的对象进行回收。

通过表格来说明一下,如下:

引用类型 被垃圾回收时间 用途 生存时间

引用类型被垃圾回收时间用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用当内存不足时对象缓存内存不足时终止
弱引用正常垃圾回收时对象缓存垃圾回收后终止
虚引用正常垃圾回收时跟踪对象的垃圾回收垃圾回收后终止

分代是什么?

代是Java垃圾收集的⼀一⼤大亮点,根据对象的⽣生命周期⻓长短,把堆分为3个代:

Young Old Permanent,

Young(年年轻代) 年轻代: 分三个区。⼀个Eden区,两个Survivor区。 大部分对象在Eden区中⽣成。 当Eden区满时,还存活的对象将被复制到Survivor区 (两个中的⼀个),当这个Survivor 区满时,此区的存活对象将被复制到另外⼀个Survivor区,当这个Survivor去也满了了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年⽼区(Tenured)”

Tenured(年老代)

年老代存放从年轻代存活的对象。⼀般来说年老代存放的都是⽣命期较长的对象。

Permanent(持久代)

⽤于存放静态⽂文件,如今Java类、⽅法等。 持久代对垃圾回收没有显著影响,但是有些应⽤用可能动态⽣生成或者调⽤用⼀一些class,例例如 Hibernate等, 在这种时候需要设置⼀一个⽐较大的持久代空间来存放这些运⾏过程中新增的类。 持久代⼤小通过-XX:MaxPermSize=进行设置。

Gc的基本概念 gc分为full gc 跟 minor gc,当每一块区满的时候都会引发gc。 Scavenge GC 一般情况下,当新对象⽣生成,并且在Eden申请空间失败时,就触发了了Scavenge GC 然后堆Eden区域进⾏行行GC,清除⾮存活对象,并且把尚且存活的对象移动到Survivor区。 接着整理Survivor的两个区。

Full GC 对整个堆进⾏整理,包括Young、Tenured和Perm。Full GC比Scavenge GC要慢,因此应该尽可能减少Full GC。 有如下原因可能导致Full GC: 1. Tenured被写满 2. Perm域被写满 3. System.gc()被显示调⽤ 4. 上一次GC之后Heap的各域分配策略动态变化

简述java内存分配与回收策率以及Minor GC和Major GC 对象优先在堆的Eden区分配。 ⼤对象直接进⼊⽼年代. ⻓期存活的对象将直接进⼊⽼年代. 当Eden区没有⾜够的空间进⾏行分配时,虚拟机会执⾏一次Minor GC.Minor Gc通常发生在 新⽣代的Eden区,在这个区的对象⽣存期短,往发生Gc的频率较⾼高,回收速度比较快;Full Gc/Major GC 发⽣生在⽼年代,一般情况下,触发⽼年代GC的时候不会触发MinorGC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快⽼年代的回收速度。

java中垃圾收集的⽅法有哪些?

标记-清除: 这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统⼀一回收。 这种⽅法很简单,但是会有两个主要问题:

1.效率不高,标记和清除的效率都很低; 会产⽣生⼤量不连续的内存碎⽚片,导致以后程序在分配较大的对象时,由于没有充足的连续内存⽽提前触发一次GC动作。

复制算法:

为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分 然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内

存上,接着一次性清楚完第一块内存,再将第二块上的对象复制到第一块。

但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。

  1. 于是将该算法进⾏了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存交Eden区,其余是两块较小的内存区叫Survior区。

  2. 每次都会优先使⽤Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区。

  3. 如果此时存活的对象太多,以⾄于Survivor不够时,会将这些对象通过分配担保机制复制到⽼年代中。(java堆⼜又分为新生代和⽼年代)

标记-整理: 该算法主要是为了解决标记-清除,产⽣⼤量内存碎⽚的问题;当对象存活率较高时,也解决了复制算法的效率问题。 它的不同之处就是在清除对象的时候现将可回收对象移动到⼀端,然后清除掉端边界以外的对象,这样就不会产⽣生内存碎⽚了。

分代收集 :

现在的虚拟机垃圾收集大多采用这种⽅式,它根据对象的⽣存周期,将堆分为新⽣代 和⽼年代。

在新⽣代中,由于对象⽣存期短,每次回收都会有⼤量对象死去,那么这时就采用复制算法。 ⽼代里的对象存活率较高,没有额外的空间进⾏分配担保;