我的CSDN博客同步发布:JVM对象引用与内存分配策略
前两天对《深入理解虚拟机》一书做了个总结:《JVM理解其实并不难! 》,今天针对对象引用和内存分配做个稍微深入的了解。
关于引用
在《JVM理解其实并不难! 》一文中提到,JVM是通过可达性分析来判断对象是否需要被回收,这可以弥补引用计数法的不足。即就算两个对象相互引用,只要这两个对象没有引用链连接GC Roots,这两个对象都会被判定为可回收的对象!注意,这里是指被判定位可回收的对象,并不是说他们就一定会被回收!这相当于“标记”的过程,即标记这个对象为可以回收的对象。
什么意思呢?既然被标记为可回收的对象,难道不就是要对他们回收吗?且继续往下看~
JVM中真正将一个对象判死刑至少需要经历两次标记过程:
第一个过程,就是我们所熟知的可达性分析。
第二个过程,就是判断这个对象是否需要执行finalize()
方法。
第一个过程无需再述,我们看看第二个过程,什么叫判断这个对象是否需要执行finalize()
方法呢?在学习Java时,我们都知道,如果我们重写Object
的finalize()
方法时,在当前这个对象消亡之前会执行finalize()
方法。然后我们就将一些资源在finalize()
中释放。其实这种做法并不正确,至于为什么不正确,我们来看看finalize()
方法在垃圾回收时是怎么触发的。
如果一个对象被判定为有必要执行finalize()方法,那么这个对象会被放入F-Queue队列中,JVM中有一个优先级比较低的线程去执行这个队列中的对象的finalize()方法。这里的执行是指JVM会触发这个方法,但不会承诺会等到它运行结束,这主要是防止某个对象的finalize过于耗时(比如死循环),导致队列中其他的对象无法被执行,最终使得整个内存回收系统崩溃~
那么什么样的对象会被判定为有必要执行finalize()方法呢?首先,这个对象必须是通过可达性分析判定为没有引用链连接到GC Roots;其次,这个对象重写了finalize()方法并且没有执行过finalize()方法。也就是说finalize()只会被执行一次。
既然如此,我们可以写一个对象自救的测试,让一个对象自己拯救自己,我们去看看一个对象是如何“耍流氓”的:
public class Test {
public static Test self;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("嗯,已经执行了finalize()方法了");
Test.self = this;
}
private static void save() throws Exception {
self = null;
System.gc();
//Finalizer线程优先级比较低,我们稍等一小会
Thread.sleep(500);
if (self != null) {
System.out.println("我还活着~你咬我啊~");
} else {
System.out.println("啊哦,我挂了~");
}
}
public static void main(String[] args) throws Exception {
self = new Test();
save();
save();
}
}
运行结果是:
嗯,已经执行了finalize()方法了
我还活着你咬我啊
啊哦,我挂了~
我们看到,同样是执行save方法,第一次对象成功拯救了自己,第二次却无法拯救自己,finalize方法也仅仅只被执行了一次而已!
软引用、弱引用、虚引用
我们知道,只有当对象存在引用链连接GC Roots时才确保不会被回收,即对象为强引用。那么有些对象,我们希望在内存充足的情况下不要回收,在内存不足的时候再将其回收掉。如果只有强引用,那这个对象永远都不会被回收。于是引入了软引用
、弱引用
、虚引用
。
软引用:用来描述一些还有用但并非必须的对象。对于软引用对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才抛出溢出异常。
怎么理解呢?其实就是说,如果我的内存不足了,JVM有点想骂人(抛异常)了,JVM会先看看哪些对象时软引用对象,先把这些软引用对象给回收掉,再看看内存是不是够用,如果还是不够用,那JVM就真的发飙了(抛异常)。
弱引用:同样,弱引用也是描述非必须的对象,但是它的强度更弱一点,被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉弱引用的对象。
有了软引用的理解基础,我们理解起来就不费劲了。就是说,弱引用对象能生存到下次垃圾回收,它比软引用活的时间要短。爱思考的你肯定会想问:那这跟直接将对象赋null
值有什么区别啊,如果一个对象“切断”掉对对象的引用,那个对象也是活到下次垃圾回收啊。可是,有没有想过,假设引用变量A a=new A()。然后你直接a=null,那如果在a之前所引用的对象被回收之前,我还想引用它怎么办?你已经没有办法找到它了~~~,但是弱引用就不一样,我们可以先判断a引用的对象有没有被回收即if(a!=null),如果没有被回收,我们就还可以利用它啦
虚引用:它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对这个对象的生死有影响。你无法通过虚引用来取得一个对象实例。为一个对象设置虚引用管理的唯一目的就是能在这个对象呗收集器回收时收到一个系统通知。
我们来讨论一下,什么时候该用软引用,什么时候该用弱引用。首先,软引用比弱引用活的时间长一点,当你不希望某个对象轻易被回收,但是呢由于这个对象比较占用内存,为了防止OOM,你可以将它声明为软引用。那什么时候用弱引用呢?当某个对象,你后面基本上不去用它了,但是又有可能会用它,而频繁的创建这个对象又比较耗资源,可以声明为弱引用。其实我觉得,大部分时间用弱引用就行了,软引用更多是为了在内存溢出之前多回收点内存。当然了,具体该使用软引用还是弱引用,需要根据实际需要决定。
说那么多引用,比较抽象,我们看看如何声明使用这些引用,在心里将它们与我们平时所用的强引用的用法上默默地做个比较:
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class Test {
SoftReference<RefObject> softRefObject;
WeakReference<RefObject> weakRefObject;
RefObject softObject;
RefObject weakObject;
public Test() {
softObject = new RefObject();
weakObject = new RefObject();
softRefObject = new SoftReference<Test.RefObject>(softObject);
weakRefObject = new WeakReference<Test.RefObject>(weakObject);
}
public static void main(String args[]) {
Test test = new Test();
RefObject softRefObject = test.softRefObject.get();
RefObject weakRefObject = test.weakRefObject.get();
if(softRefObject!=null){
softRefObject.hello();
}
if(weakRefObject!=null){
weakRefObject.hello();
}
}
class RefObject {
public void hello(){
System.out.println("hello reference");
}
}
}
内存分配策略
内存的分配,主要是在堆里面进行分配(也有可能经过JIT编译后被拆散为标量类型并间接地在栈上分配),堆里面主要是发生在新生代的Eden区中,少数情况下是在老年代中,分配的规则不是固定的,这需要根据当前使用的是哪种垃圾收集器组合还有虚拟机与内存相关的参数设置。
对象优先在Eden分配
大多数情况下,对象在新生代Eden区进行分配。当Eden区没有足够空间进行分配时JVM发生一次Minor GC。什么叫Minor GC呢?Minor GC是指发生在新生代的垃圾收集动作,因为Java对象大多具备朝生夕死的特性,所以Minor GC非常频繁,当然了,其回收速度肯定也是比较快的~,与之对应,还有个Full GC或者称为Major GC,是指老年代中的GC,经常会伴随一次Minor GC,Major GC速度一般会比Minor GC速度慢10倍以上!
大对象直接进入老年代
所谓的大对象,是指占用大量连续内存空间的Java对象。最经典的大对象就是那种很长的字符串和数组。大对象对于虚拟机来说是个坏消息~我们写程序时,尽量要避免出现一群朝生夕死的大对象。经常出现大对象容易导致内存还有不少空间时就得提前触发垃圾收集以获取足够的空间来存放大对象。
长期存活的对象将进入老年代
JVM采用分代收集思想来管理内存,就要去区分哪些是年轻的对象,哪些是老年的对象。我们知道,刚创建的对象肯定是年轻的对象,那么怎么将对象判断为老年呢?
在Eden区出生,并经过一次Minor GC后仍然存活,并且能被To Suvivor容纳,移动到To Suvivor区后,年龄设置为1。以后每经历一次Minor GC就将年龄加1,当它的年龄达到一个阀值(默认15,也可以更改-XX:MaxTenurinigThreshold来设置),就会被晋级到老年代中。
动态对象年龄判定
为了更好地适应不同程序内存情况,JVM并不一定是等到对象年龄达到阀值才将对象晋级到老年代。如果在Survivor空间中的相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年的对象就可以直接进入老年代,无需等到指定的阀值。这句话可能有点绕,不太好理解,我来再解释一下,就是说,假设Survivor的空间大小为max,年龄为y的对象总共有n个,如果y*n>max/2,那么所有年龄大于y的对象全部进入到老年代。
网友评论