深入理解内存模型及如何判定对象已死
前言
在前面的文章中,我们依次提到了类加载的过程、运行时数据区、Java对象内存模型和JVM内存模型,在JVM内存模型中,通过假设推导知道了为什么这么设计堆内存,但在Java层面我们如何知道这些推论是否真实存在,接下来将用例子证明以上观点,类经过了加载到链接再到使用最后销毁,那整个对象的生命周期又是如何呢,接下来一起来探索吧!
代码层面理解内存模型
堆内存溢出:
public class OOMTest {
public static List<Object> list = new ArrayList<>();
// JVM设置
// -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\jvm.dump 设置该参数可以生成内存溢出时的dump文件,可以导入visualvm工具中查看
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int i = 0;
int j = 0;
while (true) {
list.add(new User(i++, UUID.randomUUID().toString()));
new User(j--, UUID.randomUUID().toString());
}
}
}
虚拟机栈:
public class StackDemo {
public static long count = 0;
public static void method(long i) {
System.out.println(count++);
method(i);
}
public static void main(String[] args) {
method(1);
}
}
我们知道通过-Xss 可以设置栈的大小,JDK5以后默认是1M,以前为256K,栈的单位是栈帧,调用陷入死循环后,栈的深度不够会溢出,同时也能得到一个重要的结论:==栈的大小设置的越大,其在Java层面分配的线程就越少,反之则越多。但不能无限的大或小,设置过小,可能会影响栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。==操作系统对一个进程内的线程数是有限的,不能无限生成,经验值在3000~5000左右!
方法区:
public class MyMetaspace extends ClassLoader {
public static List<Class<?>> createClasses() {
List<Class<?>> classes = new ArrayList<Class<?>>();
for (int i = 0; i < 10000000; ++i) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
mw.visitVarInsn(Opcodes.ALOAD, 0);
mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
mw.visitInsn(Opcodes.RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();
Metaspace test = new Metaspace();
byte[] code = cw.toByteArray();
Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
classes.add(exampleClass);
}
return classes;
}
public static void main(String[] args) {
while (true) {
createClasses();
}
}
}
//错误:
/*java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_191]
at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_191]*/
对象的生命周期
对象生命周期如下图:
创建阶段: 为对象分配存储空间、开始构造对象、从父类到子类对static成员进行初始化、父类成员变量按顺序初始化,递归调用父类的构造方法、子类成员变量按顺序初始化、调用子类构造方法。
应用阶段: 系统至少维护着对象的一个强引用,所有对该对象的引用全部是强应用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))。
在应用的阶段,我们使用这些对象做着业务,往往强软弱虚是面试的重点! 那么何为引用呢?强软弱虚又是什么呢?
引用:首先我们的数据类型必须是引用类型,这个类型的数据所存储的数据必须是另外一块内存的起始地址。
强引用:JVM内存管理器从根引用集合出发遍寻堆中所有可到达对象的路径。当到达某对象的任意路径都不含有引用对象时,堆这个对象的引用就被称为强引用,如new Object();
软引用:是用来描述一些还有用但非必须得对象。对于软引用关联的对象,在系统将于发生内存溢出异常之前,将会把这些对象列进回收范围中进行二次回收。
好处:处理占用内存较大的对象,并且生命周期比较长,不是频繁使用的。
可能引发运行效率和性能低的问题:软引用指向的对象如果初始化很耗时,或者这个对象在进行使用的时候被第三方施加了我们未知的操作。
public class SoftReferenceDemo {
public static void main(String[] args) {
//。。。一堆业务代码
User a = new User();
//。。业务代码使用到了我们的User实例
// 使用完了a,将它设置为soft 引用类型,并且释放强引用;
SoftReference sr = new SoftReference(a);
a = null;
//这个时候他是有可能执行一次GC的
System.gc();
// 下次使用时
if (sr != null) {
a = (User) sr.get();
System.out.println(a );
} else {
// GC由于内存资源不足,可能系统已回收了a的软引用,
// 因此需要重新装载。
a = new User();
sr = new SoftReference(a);
}
}
}
public class User {
private int id;
private String name;
public User(int i, String toString) {
}
public User() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
弱引用:与软引用对象最大的不同就在于GC进行回收时,需要通过算法检查是否回收软引用对象,而对于weak引用对象,GC总是进行回收。
public class WeakReferenceDemo {
public static void main(String[] args) throws InterruptedException {
//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
//将缓存数据用软引用持有
WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
System.out.println("第一次GC前" + cacheData);
System.out.println("第一次GC前" + cacheRef.get());
//进行一次GC后查看对象的回收情况
System.gc();
//因为我们不确定我们的System什么时候GC
Thread.sleep(1000);
System.out.println("第一次GC后" + cacheData);
System.out.println("第一次GC后" + cacheRef.get());
//将缓存数据的强引用去除
cacheData = null;
// System.out.println("第二次GC前" + cacheData);
System.gc(); //默认通知一次Full GC
//等待GC
Thread.sleep(500);
System.out.println("第二次GC后" + cacheData);
System.out.println("第二次GC后" + cacheRef.get());
}
}
虚引用:为一个对象设置虚引用关联的唯一目的及时能在这个对象被回收时收到一个系统通知。如果一个对象被设置上了一个虚引用,实际上跟没有设置没有任何区别。
总结 : 在开发过程中强引用和虚引用还是经常会被用到,虚引用主要可以用来做缓存,如浏览器的回退操作,如果对象被回收了,直接重新加载,有缓存会更快。
不可见阶段: 不可见阶段的对象在虚拟机的对象根引用集合中再也找不到直接或者间接的强引用,最常见的就是线程或者函数中的临时变量。程序不在持有对象的强引用。 (但是某些类的静态变量或者JNI是有可能持有的 )【也就是没经历过可达性分析算法,不可见】
不可达阶段: 指对象不再被任何强引用持有,GC发现该对象已经不可达。【经历过可达性分析算法,知道不可达】
收集阶段: 对象经历了创建阶段、应用阶段、不可见阶段、不可达阶段,那对象就真的要进入终结阶段等待垃圾回收吗?答案是不一定!如果,该对象的finalize()函数被重写,则执行该函数!案例如下:
public class Finalize {
private static Finalize save_hook = null;//类变量
public void isAlive() {
System.out.println("我还活着");
}
@Override
public void finalize() {
System.out.println("finalize方法被执行");
Finalize.save_hook = this;
}
public static void main(String[] args) throws InterruptedException {
save_hook = new Finalize();//对象
//对象第一次拯救自己
save_hook = null;
System.gc();
//暂停0.5秒等待他
Thread.sleep(500);
if (save_hook != null) {
save_hook.isAlive();
} else {
System.out.println("好了,现在我死了");
}
//对象第二次拯救自己
save_hook = null;
System.gc();
//暂停0.5秒等待他
Thread.sleep(500);
if (save_hook != null) {
save_hook.isAlive();
} else {
System.out.println("我终于死亡了");
}
}
}
根据上图中,我们知道finalize()方法只能自救一次,如果重写了finalize()方法;在执行阶段如果和外部对象建立了连接,会重新复活。但这玩意不推荐使用,会打破原有对象的生命周期,会影响到JVM的对象以及分配回收速度,可能造成对象再次复活!其实它的作用就和try…catch…finally中finally作用相同。
终结阶段: 对象的finalize()函数执行完成后,对象仍处于不可达状态,该对象进程终结阶段!
空间重分配阶段: GC对该对象占用的内存空间进行回收或者再分配,该对象彻底消失!
如何确定一个对象是垃圾
要想进行垃圾回收,得先知道什么样的对象是垃圾!有2种算法可以确定一个对象是垃圾。
引用计数法:对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。虽然效率很高,但是存在相互引用,永远不能回收的问题!具体案例如下:
public class GCDemo {
public static void main(String[] args) {
GcObject obj1 = new GcObject();
GcObject obj2 = new GcObject();
obj1.instance = obj2;
obj2.instance = obj1;
obj1 = null;
obj2 = null;
}
}
class GcObject {
public Object instance = null;
}
计数不为0,存在相互引用问题,多了会导致内存泄漏问题,但在实际的生产环境中,更多的是由于本地方法指向堆的引用无法置为空而产生的OOM。
可达性分析算法: 通过GC Root的对象,开始向下寻找,看某个对象是否可达。
GC Root: 类加载器、Thread、虚拟机栈本地变量表、static成员、常量引用、本地方法栈的变量等!
GC Root是根对象吗? 答案毋庸置疑是错误的,它不是对象,GC Root本质是一组活跃的引用!
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
总结
大家看到最后了,想必都清楚了本篇文章的重点了,接下来跟着我一起梳理下自己所学的吧!接着前面所讲的运行时数据区,深入理解了JVM内存模型,开头由GC 过于频繁、方法区溢出、栈深度不够等实例代码让大家知道了具体是操作导致问题产生。又聊到了对象的生命周期,从创建阶段【初始化操作、空间内存分配】、应用阶段【强软弱虚】、不可见阶段【未经历GC Root 扫描不可见】、不可达阶段【经历过GC Root扫描 ,不可达】、(中间有介绍了2中垃圾回收算法【引用计数法、可达性分析算法】)收集阶段【finalize()方法,作用,缺点】、终结阶段。