一、System.gc()理解
- 默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
- 然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。
- JVM实现者可以通过System.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。
二、内存溢出与内存泄漏
2.1 内存溢出
概述:
- 内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样,内存溢出也是引发程序崩溃的罪魁祸首之一。
- 程序占用内存的速度大于垃圾回收的速度,会很容易出现OOM。
- 大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行就进行Full GC操作,这时候会释放大量内存,供应用程序继续使用。
- javadoc中多OOM解释是:没有空闲内存,并且垃圾回收器也无法提供更多内存。
原因:
- java虚拟机的堆内存设置不足。(比如,可能存在内存泄漏;可能堆内存大小不合理)
- 代码中创建了大量的大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
- 老版本永久代默认值较小容易发生OOM,新版本改为元空间(直接内存)概率小。
- 当然在OOM之前,通常会进行垃圾回收,尽可能清理空间。例如:不可达对象,还有软引用对象等。
- 不一定所有情况都进行垃圾回收。比如:大对象大小超过堆的最大值,直接OOM。
2.2 内存泄漏(Memory Leak)
- 对象不会再被程序调用到,但是GC又无法回收它们,这就是内存泄漏。
- 尽管内存泄漏并不会立即导致程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直到耗尽内存,最终出现OOM。
注意:这里的内存空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
内存泄漏的例子:
- 单例模式
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用,那么这个外部对象是不能被回收的,则会导致内存泄漏产生。 - 一些提供close的资源未关闭导致内存泄漏
数据库连接,网络连接,io连接必须手动close,否则是不会被回收。
三、Stop The World
- Stop The World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿,所有应用程序线程被暂停。这样做的目的是保证可达性分析的结果的准确性。
- 在GC完成后会恢复线程,频繁中断会让系统卡顿,所以必须要减少STW发生。
- 任何GC都会发生STW,不同GC回收器只是回收效率不同。
- STW时JVM后台自动发起和完成的。用户不可见情况下,停掉所有应用程序线程。
- 开发中不要用System.gc();会导致STW发生。
四、垃圾回收的并行与并发
4.1 并发(Concurrent)
- 在操作系统中,是指在一个时间段中有几个程序都处于已经启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。
- 并发不是真正意义上的“同时执行”,只是CPU把一个时间段划分成几个时间片段,然后在几个时间区间来回切换,由于CPU处理速度快,感觉像是多个程序同时进行。
4.2 并行(Parallel)
- 当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程之间互不抢夺CPU资源,可以同时进行,称之为并行。
- 其实决定并行的因素并不是CPU的数量,而是CPU的核数。
4.3 并行 VS 并发
二者对比:
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的。
只有在多个CPU或者一个CPU多核的情况中,才会发生并行。
否则,看似同时发生的事情,其实都是并发执行的。
4.4 垃圾回收的并发和并行
并发和并行,在谈论垃圾收集器的上下文语境中,它们可以解释如下:
- 并行:指多个垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
如:ParNew、Parallel Scavenge、Parallel Old - 串行
相较于并行的概念,单线程执行。
如果内存不足,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。
- 并发:指用户线程与垃圾收集线程同时执行(单不一定是并行,可能会交替执行),垃圾回收线程在执行时不会暂停用户程序的运行。
- 用户程序在继续运行,而垃圾程序线程运行于另一个CPU上。CMS、G1
- 用户程序在继续运行,而垃圾程序线程运行于另一个CPU上。CMS、G1
五、安全点与安全区域
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
5.1 安全点
- 抢先试中断:(目前没有虚拟机采用)
首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。 - 主动式中断:
设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。
5.2 安全区域
Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是程序“不执行”的时候呢?例如线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把Safe Region看做是被扩展的SafePoint。
六、引用
6.1 强引用-不回收
- java中99%引用都是强引用,常见的普通对象,默认就是强引用。
- new一个新对象,并将对象赋值给一个变量的时候,这个变量就是指向该对象的强引用。
- 强引用是可触及的,垃圾收集器就永远不会回收被强引用的对象。
- 对于一个普通对象,如果没有其他引用关系,只要超过引用作用域或者显示给强引用赋值为null,就可以被回收。
- 相对的,软引用、弱引用和虚引用的对象是软可触及、若可触及和虚可触及。在一定条件下都可以被回收。所以,强引用是造成Java内存泄漏的主要原因之一。
强引用的特点:
- 强引用可以直接访问目标对象。
- 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM,也不会回收强引用对象。
- 强引用可能导致内存泄漏。
6.2 软引用(Soft Reference)-内存不足即回收
- 软引用是用来描述一些还有用,但非必需的对象。只被软引用关联的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收后还没有足够的内存,才会抛出OOM。
- 软引用通常用来实现内存敏感的缓存,比如:高速缓存就用到了软引用(mybatis)。
- 类似于弱引用,只不过java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。
6.3 弱引用(Weak Reference)-发现即回收
- 弱引用也是用来描述非必要对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
- 由于垃圾回收器线程通常优先级很低,因此,并不一定很快发现持有弱引用的对象,这种情况下,弱引用对象可以存在较长的时间。
- 弱引用和软引用一样,在构造弱引用时,也可以指定一个应用队列,当引用对象回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象回收情况。
- 软引用、弱引用都非常合适来保存那些可有可无的缓存数据。起到加速系统作用。
弱引用与软引用最大不同在于,当GC进行回收时,需要通过算法检查是否回收软引用,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。
面试题:你在开发中使用过WeakHashMap吗?
6.4 虚引用(Phantom Reference)-对象回收跟踪
- 虚引用又称“幽灵引用”,“幻影引用” ,是所有引用类型种最弱的。
- 如果一个对象仅持有虚引用,和没有引用几乎一样。
- 它不会单独使用,也无法通过引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。
- 虚引用的唯一目的:跟踪垃圾回收过程。比如:能在这个对象被回收器回收时收到一个系统通知。
- 虚引用必须和引用队列一起使用。当垃圾回收器准备回收一个对象时,如果发现是虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知引用程序对象回收情况。
- 由于虚引用可以跟踪对象回收时间,因此,可以将一些资源释放操作放在虚引用中执行和记录。
6.5 终结器引用(Fail reference)-了解
- 它用以实现对象finalize()方法,也可以称为终结器引用。
- 无需手动编码,其内部配合引用队列使用。
- 在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象。