目录
System.gc()的理解
通过System.gc()或者Runtime.getRuntime().gc(),可以显示触发FullGC,同时对年代和新生代的回收,尝试释放被丢弃对象的占用的内存。
System.gc() 附带一个免责声明,无法保证及时进行GC,它只是通知JVM,我们希望它进行一次GC。
一般GC需要JVM调用,我们无须手动触发,除非我们在编写性能基准测试。
当我们调用System.runFinalization() 会强制调用失去引用的对象的finalize()方法。
看下一个有趣案例,大家判断一下该buffer对象是否会被GC。
public void localvarGC1() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
System.gc();
}
这种情况buffer不会被GC,虽然它buffer已经过了其作用域,但是原来的局部变量表1的位置还是存着对象的地址。从而不会被回收。
除非这样:
public void localvarGC1() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
int i = 0;
System.gc();
}
这样局部变量表1的位置的地址值会被0覆盖。从而对象会被GC。
内存溢出
没有空闲内存给新对象分配内存就出现OOM
一般情况下不会出现OOM,一般原因有下:
- 应用程序的内存增长速度非常块,造成垃圾回收速度跟不上内存消耗速度。
- 堆内存设置本身不够
- 出现内存溢出
一般情况OOM之前会触发一次Full GC(除非本次对象比堆还大),如果发现还不够,会进行一次独占式的FullGC。
- 列如它会尝试清除软引用的对象。
内存泄漏
java理解:对象不会再被程序用到,但因为有引用指向,导致GC无法回收,叫内存泄漏。
从C++角度理解,java永远不会内存泄漏,C++出现内存泄漏,我们就要关机重启了。
一般我们理解内存泄漏应该具备逐步蚕食内存的特征,C++很可能因为方法一行局部指针的“=”操作,引起泄漏,导致方法每执行一次就会泄漏一次。这种情况可能是C++程序员忽略或因其他复杂的原因导致的。java一般不会,除非该方法每次都会在一个静态的集合中 add一个对象,又不在去消费它,如果只是写 “=”,是不会蚕食内存。这种事情傻逼才会做,我们java程序员不应该在任何场景这样做。
java出现内存泄漏举例:
1、单例模式
如果我们使用单例模式,由于该对象与Class对象伴生。该类和对象以及被其引用的对象是无法被回收的,如果我们后面不用它了,也可以理解为是内存泄漏,但是这种泄漏不会逐步蚕食内存。
2、一些提供了外部提供close的资源未关闭导致内存泄漏。
比如数据库链接,网络链接,io。这种是与外部程序关联的,不管这个外部程序是何种语言编写,如果外部程序不加以控制则会引起外部程序的内存泄漏。
这种内存泄漏会逐步蚕食内存。
3、傻逼代码,在一个不会被用到的静态集合中不停的add。或者在一个理论上无上限的静态集合中不停add,但是其remove的逻辑欠考虑,导致里面放置的不被用到的对象不会被remove。导致蚕食内存。
Stop The World
GC线程进行时,用户线程暂停的现象。
我们优化回收算法的目的是:
1、使GC每次GC时间更短,使GC总时间更端。
2、GC过程中可以穿插用户线程执行,但是在资源有限的情况下,这点不论怎么优化,STW时间总和还是不变的。因为只要切换到GC线程,就会STW,虽然我们在其中穿插用户线程,总和还是不变的(GC总时间 约等于 STW总时间),因此第1点优化格外重要。
在使用GCroot阶段,肯定是要完全STW的。因为用户线程的穿插会导致GCroot的引用链判断不可信。比如本来C 可以被AB引用,GC判断C是否被A引用时,用户线程将C交给B引用,GC判断C是否被B引用时,用户线程又将其断开交给A引用,这样就会导致后面的NPE。
总结:
任何垃圾回收器都无法避免STW,尤其是标记阶段,是不可能穿插用户线程的。在资源有限的情况下不管单核机器还是多核机器,尤其是单核机器,GC花费的资源,必然是要从用户线程分担的。不管如何穿插用户线程,GC总消耗不变的,不管如何分区回收,GC花费总时间也还是一样的。但是我们在GC的时候穿插用户线程是可以将不好的用户体验平摊的。
垃圾回收的并行与并发
操作系统理解
- 并行(Parallel),AB两个进程或线程在分别在两个核同时进行。
- 串行(Serial),AB两个进程或线程先后执行。
- 并发 = AB两个进程分别在获取到操作系统时间片时先后进行,但是从整个时间段来看,似乎是同时进行的。
只有在多个CUP,或一个CPU多核地情况下,才会出现并行;
垃圾回收地并发与并行
并行:多个线程参与GC
串行:单个线程参与GC
并发:GC线程核用户线程并发执行。
安全点和安全区域
程序在运行时,不是在任何时间点,停下用户线程去进行GC。
只有在安全点才能进程GC。
Safe point
Safe point的选择很重要,如果太少,可能导致GC等待时间太长,如果太频繁可能导致性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令为Safe Point。比如方法的调用,循环跳转,异常跳转等。
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来?
- 抢先式中断(没有采用)
首先中断所有线程。如果还有线程不再安全点,就恢复线程,让线程跑到安全点。
- 主动式占用
当GC需要将要开始时,设置一个中断标志,各个线程运行到safe Point的时候主动轮询这个标志,如果标志为真,则主动自己进行中断挂起。
Safe region
safe point机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的safe point。但是,程序“不执行”的时候呢?列如线程处于Sleep状态或Blocked状态,这时候线程无法响应JVM的中断请求,“走“ 到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域来解决。
安全区域是指在一段代码中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把safe Region看做时扩展了safe point。
具体执行时
1、当线程运行到safe region的代码时,首先标识进入了safe region,如果这段时间内发生GC,JVM会忽略标识为safe region状态的线程
2、当线程即将离开safe region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则继续等待。只有完成GC之后才能安全离开。
各种类型引用
我们希望描述这样一类对象:当内存空间还足够的时候,则可以保留在内存中;如果内存空间不够,在进行垃圾回收收集之后还是很紧张,则可以抛弃这些对象。比如缓存。
jdk1.2之后,java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Plantom Reference)。他们的四者强度依次递减。
- 强引用: 最传统的引用定义,是指在程序代码之中普遍存在的引用赋值。无论任何情况下,只要强引用关系还存在,GC就永远不会回收掉这些对象。
- 软引用: 在系统将要发生内存溢出之前,会将这些对象列入回收范围之中进行第二次进行第二次回收。如果这次回收后还没有足够内存,才会抛出内存溢出异常。
- 弱引用: 被弱引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
- 虚引用: 一个对象是否又虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
除了强引用,其他3种可以在java.lang.ref包中找到。
强引用
最传统的引用定义,是指在程序代码之中普遍存在的引用赋值。无论任何情况下,只要强引用关系还存在,GC就永远不会回收掉这些对象。我们平时 ”类名 引用名 = XXXXX“ 使用的都是强引用。即使OOM,强引用指向的对象都是不会被回收的。
软引用
在系统将要发生内存溢出之前,会将只被软引用引用的对象列入回收范围之中进行第二次进行第二次回收。如果这次回收后还没有足够内存,才会抛出内存溢出异常。适合用于缓存,在资源紧张时回收缓存。
案例:
public class SoftReferenceTest {
private static class User {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return id + " " + name;
}
}
public static void main(String[] args) {
SoftReference<User> userSoftReference = new SoftReference<>(new User(1, "softUser"));
System.out.println(userSoftReference.get());
System.gc();
//显示GC之后, 资源足够软引用不会被回收
System.out.println(userSoftReference.get());
try {
//使用参数 -Xms10m -Xmx10m -XX:PrintGCDetail,构建7m的bytes, 使内存溢出
//byte[] bytes = new byte[1024 * 1024 * 7];
//使用参数 -Xms10m -Xmx10m -XX:PrintGCDetail,构建快要撑破老年代的bytes
byte[] bytes = new byte[1024 * 6340];
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
//在OOM之前, GC会回收软引用的可达对象
System.out.println(userSoftReference.get());
}
}
}
弱引用
只被弱引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。也可以用于缓存,比如缓存图片。
但是由于垃圾回收线程通常优先级很低,因此,并不一定能很快地发现持有弱引用地对象,在这种情况下,对象可以存活较长时间。
弱引用核软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用被回收时,就会加入指定地引用队列,通过这个队列可以跟踪对象地回收情况。
案例
public class WeakReferenceTest {
private static class User {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return id + " " + name;
}
}
public static WeakReference<User> weakUser;
public static void main(String[] args) {
weakUser = new WeakReference<>(new User(1, "hello"));
System.out.println(weakUser.get());
/**
* 这样就强引用指向了
User user = weakUser.get();
System.gc();
System.out.println(user);
*/
System.gc();
System.out.println(weakUser.get());
}
}
weakHashMap?
它内部用的Entry就是继承了 WeakReference
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
虚引用
一个对象是否有虚引用地存在,完全不会决定对象地生命周期。如果一个对象仅仅持有虚引用,那么它就像没有引用一样。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
它一定要配合引用队列进行操作。
/**
PhantomReference 的构造方法
*/
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
案例
public class PhantomReferenceTest {
public static PhantomReferenceTest obj;
static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;
public static class CheckRefQueue extends Thread {
@Override
public void run() {
while (true) {
if (phantomQueue != null) {
PhantomReference<PhantomReferenceTest> objt = null;
try {
objt = (PhantomReference)phantomQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (objt != null) {
System.out.println("追踪垃圾回收过程:PhantomReferenceTest 实例被GC了");
if (objt.get() == null) {
System.out.println("我们依旧拿不到他的实例");
}
}
}
}
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("调用当前类的finalize方法");
obj = this;
}
public static void main(String[] args) {
Thread thread = new CheckRefQueue();
thread.setDaemon(true);
thread.start();
phantomQueue = new ReferenceQueue<>();
obj = new PhantomReferenceTest();
// 当 obj 要被回收时,就会将这个虚引用放到phantomQueue引用队列
PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<>(obj, phantomQueue);
if (phantomRef.get() == null) {
System.out.println("我们拿不到他的实例");
}
try {
obj = null;
System.out.println("第一次GC。。。");
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj == null");
} else {
System.out.println("obj != null");
}
System.out.println("第二次GC。。。");
obj = null;
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj == null");
} else {
System.out.println("obj != null");
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
终结器引用
它用以实现对象的finalize()方法,也可以称终结器引用。
无需手动编码,其内部配合引用队列使用。
在GC时,终结器引用如队列。由Finalizer线程通过终结器引用找到被引用的对象并调用它的finalize()方法,第二次GC的时候才能回收被引用对象。
从它的源码我们可以看到,他是一个default级别的类,我们外部无法new ,它是GC自动调用的。
/**
* Final references, used to implement finalization
*/
class FinalReference<T> extends Reference<T> {
public FinalReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}