大家好,又见面了,我是你们的朋友全栈君。
1. Java中的四种引用类型
在Java中,对于引用最基本的解释就是:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用(有点指针的意味)。后来Java还将引用划分为了4种,根据被GC回收的时机可以分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantorm Reference)。这4种引用的强度依次渐弱。
1.1 强引用
Object o = new Object();
这里的o
就是一个强引用,也是我们用得最多的引用,在实例化类的时候经常会用到。遇到这类引用,GC(垃圾回收器)是绝对不会回收它的。当遇到内存不足的情况,JVM会抛出OOM异常。所以,在不使用这类对象的时候要注意释放它,以便让系统回收。
1.2 软引用
SoftReference<String> s = new SoftReference<>(new String("Hello"));
System.out.println(w.get());
此时会输出
Hello
这里的s
就是一个软引用,它是用来描述一些有用但非必需的对象,当系统内存出现不足的时候,会立即把这些对象进行回收。
软引用可以用来实现内存的缓存,如图片缓存。
1.3 弱引用
WeakReference<String> w = new WeakReference<>(new String("Hello"));
System.out.println(w.get());
System.gc();
System.out.println(w.get());
此时会输出:
Hello
null
弱引用的强度比软引用低一些,一旦GC开始执行,便会对这类对象进行回收。也就是说无论内存足够与否,弱引用对象都只能生存到下一次GC之前。
1.4 虚引用
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> pr = new PhantomReference<>(new String("Hello"), queue);
System.out.println(pr.get());
此时会输出:
null
虚引用也称为幽灵引用,它的强度是最低的,一个对象是否有虚引用的存在,完全不会对它的生存时间构成影响,当然也无法通过虚引用获取一个类的实例,GC任何时候都有可能会回收此对象。它唯一的作用就是就是用于追踪,让我们能够在这个对象被回收的时候收到一个通知。
2.如何判断对象需要被回收
2.1 引用计数法
比较尴尬的是以前刚上Java课的时候,我去问老师JVM是如何确定需要被回收的对象的时候,老师就给我讲了引用计数法,但后来随着学习的深入,我发现主流的JVM都没有采用这一方法,原因是它有很大的一个缺陷,就是难以解决对象之间相互引用问题。
所谓引用计数法,就是在对象内部会有一个引用计数器,一旦某个地方引用它时,计数器就加1,表面上看这是一个效率非常高的方式,但是如果遇到如下情形时,采用了这种方式判断对象是否存活的GC就难以发挥作用了。
public static void main(String[] args) {
A a = new A();
B b = new B();
//a,b循环引用对方
a.intance = b; //b的引用计数加1
b.intance = a; //a的引用计数加1
//虽然a和b都被设置成null了,但它们的引用计数仍然是1,不会被GC回收
a = null;
b = null;
System.gc();
}
2.2 可达性分析法
相比之下,可达性分析算法就要靠谱得多。所谓可达性分析就是通过一系列被称为“GC Roots”的点作为起始点,从这些节点开始向下搜索,搜索的路径称为引用链,当一个对象到GC Roots不可达的时候,则证明此对象是可回收的。
上图中,从GC Root到ObjectC、ObjectE、ObjectF都不可达,所以这三个对象就是会被GC回收的对象。
3.垃圾回收算法
3.1 标记清除法
标记清楚法有两个过程,一是标记过程,即判定需要回收的对象的过程,通过可达性分析便可分析出来。二是清除阶段,这个阶段就是对标记了的对象进行回收。
这种回收算法非常简单粗暴,也很好理解,但是暴露出来问题还是比较大的,需要优化的地方还有很多。一是标记和清除的效率都不是很高。二是执行完GC后会造成大量的内存碎片,如果以后分配大内存对象的的时候无法找到足够大的连续内存,就会频繁触发GC。
3.2 复制算法
为了解决内存碎片问题,复制算法出现了,它将可用内存平均划分为两块,每次只使用其中一块。当这一块用完了,就将还存活的对象复制到另一块,然后再把已使用的内存空间一次清理掉。这样每次GC的时候都直接回收半个内存空间的大小,不必考虑碎片问题。但这种方法带来的代价也不小,牺牲了一半的存储空间。
3.3 标记整理法
如果在对象存活率较高的时候采用复制算法的话,复制的操作就会有很多,对程序的运行效率也会产生一定的影响,此时就应该考虑标记整理法。标记整理法的执行过成分为3步:1.用与“标记清除法”一样的操作标记存活的对象;2.将被标记的对象全部移动到内存的某一端;3.清除边界以外部分的内存。
3.4 分代回收法
在分代回收算法中,会根据对象的存活周期,将内存划分为几块,一般是新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation)。这样就可以根据不同内存区域的特点执行采用不同的回收算法。像新生代这种经常有大批对象死去的区域,就适合用复制算法。而对于老年代这种对象生存周期较长和永久代这种内存存活率较高,又没有其他担保空间的地方就用标记清除法或标记整理法就行了。
3.4.1 新生代
对于新生代,往往会分为三个区:一个Eden区和两个Survivor区(图中是S0和S1,其实这两个区域没有任何区别,只是取个名字方便叙述)。这样划分后结合复制算法对垃圾进行回收效果和效率都会比较高。具体的工作流程如下:
程序中大部分对象都是在新生代的Eden区域中,当GC来临时,会遵循如下机制:
1.当Eden区域触发GC的时候,将Eden中还存活的对象复制到S0中,再清空Eden区。
2.然后当S0触发GC时候,将S0和Eden中还存活的对象复制到S1中,再清空S0和Eden区。
3.当S1也触发GC的时候,就将此区域内的从S0复制过来并且GC后还存活的对象复制到老年代,再清空S1。
值得注意的是:GC发生的时机是根据剩余内存的大小和具体的算法来决定的,不一定是当内存存满了才触发一次GC。并且这里的GC指的是Minor GC,也就是用一些快速回收算法实现的高效率GC,这种方式只会对新生代进行回收。
3.4.2 老年代
老年代中则存放着新生代复制过来的对象,这些一般都是经历过多次GC存活下来的对象,所以这里的对象生存的时间都比较长。当老年代存满了时候,会调用Full GC也就是对整个堆(包括新生代、老年代、永久代)进行回收,这样的回收更彻底,但是时间也消耗更多。
3.4.3 永久代
永久代一般用来存放一些静态变量,静态方法等,这里一般发生GC的概率比较小,但是一旦存满,也是会直接触发Full GC的。
4.内存分配策略
JVM的一大优势是解决的内存方面的两个重要的问题:自动给对象分配内存 和 自动回收分配给对象的问题。一般来说,分配对象需要符合以下原则:
4.1 对象优先在Eden分配
当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
4.2 大对象直接进入老年代
所谓的大对象是指:需要大量连续内存空间的Java对象。最典型的大对象就是那种很长的字符串以及数组。这样可以有效避免大对象在Eden和Survivor之间的频繁复制操作带来的性能开销。(当然我们在写代码的时候也要注意避免写出生命周期太短的大对象,因为这并不符合老年代的特点)
4.3 长期存活的对象将会进入老年代
根据老年代的特点,每当对象熬过了一次Minor GC的时候,它的age就将会增加1,当年龄增长到一定的值时(默认为15岁),它就将进入老年代。
4.4 动态对象年龄判定
对于“老年对象”的判定并不一定是要根据”默认的年龄要求(15岁)“,如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半的时候,年龄大于或等于该年龄的对象将会直接进入老年代。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/132168.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...