JVM 对象的创建和垃圾回收机制

一、虚拟机中对象的创建过程
在这里插入图片描述
类的符号引用:用一组符号描述这个类的信息(com.lgj.Demo 类似这种)
1、当Object o = new Object();首先在方法区中的常量池是否能够定位到这个类的符号引用
2、指针碰撞:堆空间是非常规整的,有一个指针,指向最后一个对象分配的地址位置,然后当新new 一个对象的时候,这个指针移动一个对象大小的距离。如下图所示,红色代表已经占了的内存,白色代表还没占的内存。
在这里插入图片描述
3、空闲列表:当堆内存经过垃圾回收之后,内存就有可能出现,下图所示,然后,专门有一张表来记录,那块内存是没有被分配。(对象占据的内存一定是连续的)
在这里插入图片描述
4、JVM同样有可能多线程去分配内存,怎样解决多线程安全问题,第一种方案CAS,Compare and Swipe,比较和交换,首先会拿到一个 old的值,假设old值为空, (CAS 在CPU执行是原子操作)当King老师先占据了这个内存之后,此时比较和交换就会是成功的,然后分配King的对象,接下来LGJ老师去占据这个内存,发现原始值不为空,然后就会占据失败,接着就会重试,然后获取这块内存之后发现此时不为空,紧接着就会寻找下一块内存,这就是CAS 分配内存的重试机制。
5、本地线程分配缓冲,英文 Thread Local Allocation Buffer 简称 TLAB,线程本地分配的时候分配一个缓冲,具体做法就是在堆空间的Eden区,分别给每个线程要创建的对象分配一块内存空间。(它的效率是高的,让我们的对象分配更快)如下图所示
在这里插入图片描述
6、内存空间初始化,也就是默认初始化,int值默认为0,Boolean值默认为false。
7、设置:你这个对象是属于哪个实例的,指向对应的Class。以及对象头中的设置,比如hashcode、gc年龄等
8、对象初始化:开始执行构造方法。

二、对象的内存布局
在这里插入图片描述
1、对象头:不要去深入研究,类型指针指向对应的Class字节码。
2、实例数据:对象中实例字段的数据(也就是成员变量)
3、对象填充:假设对象头加实例数据刚好是 30 字节,然后它就会自动填充 2个字节,使整个大小是 8 字节的整数倍。

三、对象的访问定位
在这里插入图片描述
使用句柄:在堆空间划分一块区域叫做句柄池,句柄池里面存放对象实例的指针,中间做一个中转。缺点需要2次转化,使用直接指针,中间不需要进行二次转化。

四、判断对象的存活
在这里插入图片描述

1、引用计数法:如果这个对象被别人引用,计数器加一,如果没有引用的话计数器为0,此时可以当成垃圾,但是有一个问题是,如果这个对象相互引用的话,此时就有问题,代码如下:

/**
 * VM Args:-XX:+PrintGC
 * 判断对象存活
 */
public class Isalive {
    public Object instance =null;
    //占据内存,便于判断分析GC
    private byte[] bigSize = new byte[10*1024*1024];

    public static void main(String[] args) {
        Isalive objectA = new Isalive();//objectA 局部变量表 GCRoots
        Isalive objectB = new Isalive();//objectB 局部变量表
        //相互引用
        objectA.instance = objectB;
        objectB.instance = objectA;
        //切断可达
        objectA =null;
        objectB =null;
        //强制垃圾回收,在Hotspot虚拟机中用的是 可达性分型算法,垃圾可以被回收
        System.gc();
    }
}

运行结果:
[GC (System.gc()) 24478K->832K(151552K), 0.0032882 secs]
[Full GC (System.gc()) 832K->755K(151552K), 0.0066910 secs]

2、可达性分析法:如果这个对象,没有在这条根引用连上,那么这个对象就是垃圾
3、finalize 方法,拯救该对象,但是这个不靠谱,(因为finalize方法执行的线程优先级比较低)而且只能拯救一次,因为这个方法只能执行一次。

/**
 * 对象的自我拯救
 */
public class FinalizeGC {
    public static FinalizeGC instance = null;
    public void isAlive(){
        System.out.println("I am still alive!");
    }
    @Override
    protected void finalize() throws Throwable{
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeGC.instance = this;
    }
    public static void main(String[] args) throws Throwable {
        instance = new FinalizeGC();
        //对象进行第1次GC
        instance =null;
        System.gc();
        Thread.sleep(1000);//Finalizer方法优先级很低,需要等待
        if(instance !=null){
            instance.isAlive();
        }else{
            System.out.println("I am dead!");
        }
        //对象进行第2次GC
        instance =null;
        System.gc();
        Thread.sleep(1000);
        if(instance !=null){
            instance.isAlive();
        }else{
            System.out.println("I am dead!");
        }
    }
}

五、各种引用关系
1、强引用就是你的女儿(谁也别想抢走你的女儿)
2、软引用(SoftReference)就是你的老婆(她非常有用,但是也非必须,如果你的系统即将发生outOfMemory,她就会被垃圾回收器回收掉,比喻你和你的老婆吵架吵的不开开交,过不下去了,那就只能离婚)
3、弱引用(WeakReference)就是你的女朋友(只要有GC,就会被回收,也就是只要吵架,你们就会分手)
4、虚引用(PhantomReference)就是13号技师,你就是洗了个脚,随时被回收掉(作用是监控垃圾回收器是否正常运行)

/**
 * 软引用
 * -Xms20m -Xmx20m
 */

public class TestSoftRef {
	//对象
	public static class User{
		public int id = 0;
		public String name = "";
		public User(int id, String name) {
			super();
			this.id = id;
			this.name = name;
		}
		@Override
		public String toString() {
			return "User [id=" + id + ", name=" + name + "]";
		}
	}
	//
	public static void main(String[] args) {
		User u = new User(1,"King"); //new是强引用
		SoftReference<User> userSoft = new SoftReference<User>(u);//软引用
		u = null;//干掉强引用,确保这个实例只有userSoft的软引用
		System.out.println(userSoft.get()); //看一下这个对象是否还在
		System.gc();//进行一次GC垃圾回收  千万不要写在业务代码中。
		System.out.println("After gc");
		System.out.println(userSoft.get());
		//往堆中填充数据,导致OOM
		List<byte[]> list = new LinkedList<>();
		try {
			for(int i=0;i<100;i++) {
				//System.out.println("*************"+userSoft.get());
				list.add(new byte[1024*1024*1]); //1M的对象 100m
			}
		} catch (Throwable e) {
			//抛出了OOM异常时打印软引用对象
			System.out.println("Exception*************"+userSoft.get());
		}
	}
}
/**
 * 弱引用,只要发生垃圾回收,弱引用引用的对象就会被回收
 */
public class TestWeakRef {
	public static class User{
		public int id = 0;
		public String name = "";
		public User(int id, String name) {
			super();
			this.id = id;
			this.name = name;
		}
		@Override
		public String toString() {
			return "User [id=" + id + ", name=" + name + "]";
		}
	}
	public static void main(String[] args) {
		User u = new User(1,"King");
		WeakReference<User> userWeak = new WeakReference<User>(u);
		u = null;//干掉强引用,确保这个实例只有userWeak的弱引用
		System.out.println(userWeak.get());
		System.gc();//进行一次GC垃圾回收,千万不要写在业务代码中。
		System.out.println("After gc");
		System.out.println(userWeak.get());
	}
}
输出结果
User [id=1, name=King]
After gc
null
public class TestPhantomRef {
    public static void main(String[] args) {
        ReferenceQueue<String> queue = new ReferenceQueue<String>();
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
        System.out.println(pr.get());
    }
}
输出结果为 null 

六、对象的分配策略
在这里插入图片描述
1、逃逸分析:满足逃逸分析,不会逃出方法体,其他线程也不会用它,还有这个对象占据空间不大,满足这些条件,这个对象就会在栈上分配。

/**
 * 逃逸分析-栈上分配
 * -XX:-DoEscapeAnalysis  关闭栈上分配
 */
public class EscapeAnalysisTest {
    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 50000000; i++) {//5千万的对象,为什么不会垃圾回收
            allocate();
        }
        System.out.println((System.currentTimeMillis() - start) + " ms");
        Thread.sleep(600000);
    }
    static void allocate() {//满足逃逸分析(不会逃逸出方法)也只有main线程在用
        MyObject myObject = new MyObject(2020, 2020.6);
    }
    static class MyObject {
        int a;
        double b;
        MyObject(int a, double b) {
            this.a = a;
            this.b = b;
        }
    }
}

2、如果不符合逃逸分析,那么就考虑本地线程分配缓冲,默认情况下占据Eden区的 1%。
3、如果是大对象,直接放到老年代,因为Eden区放不下。(大对象:字符串、数组)老年代默认情况下占据 堆空间的 2/3,-XX:PretenureSizeThreshold 10M可以设置大对象的值,表示超过 10M 的对象就会被认为是大对象,直接放到老年代。
4、长期存活的对象进入到老年代,当经过多轮的垃圾回收之后,对象头会记录该对象的GC年龄,当年龄超过阈值之后,那么该对象就会进入到老年代。最大预知为 15,该参数可以进行设置:-XX:MaxTenuringThreshold 10,CMS的阈值为 6 。
5、空间分配担保:当from或者to区的对象晋级到老年代的时候,假设老年代的内存只剩下 1M了,但是该对象大小为2M,那么每次就会触发FullGC,这种的话明显影响JVM性能,所以只有当老年代放不下新对象的时候才触发,这种机制就是空间分配担保机制。
6、动态年龄判断:From 和 To区也称之为 Survivor区,幸存者区域也不够大,假设 这里面有 5 个对象,年龄是 5,占据了 Survivor区域的一般内存,这个时候也会走一个动态年龄判断,直接进入到老年代,这个目的是为了,Eden区的对象可以正常进入Survivor区。

七、分代收集理论
绝大部分对象都是朝生夕死 --区域 新生代
对象熬过多次垃圾回收,越难回收 – 区域 老年代

在这里插入图片描述
1、触发MinorGC是,我们不断的分配对象,当发现Eden区域不够的时候就会触发 MinorGC
2、当方法区或者老年代内存不够的时候就会触发 FullGC
3、新生代用的算法都是复制算法,新生代 Eden :From :To = 8:1:1
在这里插入图片描述
复制算法用在 From 和 To 区域,假设现在第一次触发MinorGC,首先会把Eden区域存活的对象移到From区域,并且年龄该对象的年龄加一,假设只有一个对象到From区域,当第二次触发MinorGC,那么From 区域存活的对象会直接到 To区并且GC年龄再次加一,Eden区存活的对象直接到To区,紧接着格式化From区域,这个就是标准的赋值回收算法
在这里插入图片描述
4、会产生内存碎片,就是堆空间不是连续的,可能导致提前GC,因为分配一个大对象的时候,发现内存不够就会GC。执行效率不稳定,假设在标记之后发现 90% 的对象都需要清除,这种效率不高。所以比较适合老年代,因为老年代的对象都是那种很难回收的对象
在这里插入图片描述
5、标记整理算法,没有内存碎片,但是涉及对象的移动,以及引用的更新。引用更新就需要用户线程暂停。整体效率是偏低的。

八、JVM常见的垃圾回收器
在这里插入图片描述

1、JVM诞生的时候,最开始的线程回收 是单线程的 Serial (复制算法) 和SerialOld (标记整理算法)内存不大的
2、然后在发展出现 Parallel Scavenge [ˈpærəlel] 并行的意思 [ˈskævɪndʒ] 觅食的意思 和 Parallel Old 算法和单线程的算法一致。
注意:上面这些回收垃圾的时候,必须暂停用户线程,就好比:你妈打扫卫生的时候,你必须停止乱扔垃圾的操作,如下图:
在这里插入图片描述
3、出现第一个并发垃圾回收器是CMS(Concurrent Mark Sweep),也就是业务线程和垃圾回收线程可以同时工作。
在这里插入图片描述
4、初始标记是标记GCRoots直接关联的对象,GCRoots的数据量不是很多,所以这次标记速度很快。接着并发标记,标记根可达的其他对象,这个就比较深了,耗时时间长,那么就需要用户线程和GC同时运行。这样会导致垃圾永远回收不完,此时就要重新标记,并且暂停所有用户线程,做完之后,清理(时间长需要走并发),然后重置线程。
优点:最大响应时间,卡顿的时间会比较短,用户体验感比较好,缺点CPU敏感,如果CPU的核心数 小于4的话,对用户的影响就比较大。
浮动垃圾:在并发清理的时候,你清理你的我继续产生垃圾,这块垃圾就成为浮动垃圾,只能等待下次就行清理了。
如果浮动垃圾过多,假设浮动垃圾预留占据老年代的 20%,此时刚好产生的浮动垃圾已经占满,JVM就会把CMS垃圾回收器切换成Serial Old垃圾回收器,当内存碎片过多的时候,也会切换成Serial Old垃圾回收器。

5、Stop The World 的现象,以及代码演示
在这里插入图片描述

/**
 *
 * VM参数: -XX:+PrintGC
 */
public class StopWorld {

    /*不停往list中填充数据*/
    //就使用不断的填充 堆 -- 触发GC
    public static class FillListThread extends Thread{
        List<byte[]> list = new LinkedList<>();
        @Override
        public void run() {
            try {
                while(true){
                    if(list.size()*512/1024/1024>=990){
                        list.clear();
                        System.out.println("list is clear");
                    }
                    byte[] bl;
                    for(int i=0;i<100;i++){
                        bl = new byte[512];
                        list.add(bl);
                    }
                    Thread.sleep(1);
                }

            } catch (Exception e) {
            }
        }
    }
    /*每100ms定时打印*/
    public static class TimerThread extends Thread{
        public final static long startTime = System.currentTimeMillis();
        @Override
        public void run() {
            try {
                while(true){
                    long t =  System.currentTimeMillis()-startTime;
                    System.out.println(t/1000+"."+t%1000);
                    Thread.sleep(100); //0.1s
                }

            } catch (Exception e) {
            }
        }
    }

    public static void main(String[] args) {
        //填充对象线程和打印线程同时启动
        FillListThread myThread = new FillListThread(); //造成GC,造成STW
        TimerThread timerThread = new TimerThread(); //时间打印线程
        myThread.start();
        timerThread.start();
    }
}

打印线程正常的话,应该是每隔 100ms 打印一次,但是当碰到垃圾回收的时候,中间间隔时间就比较长,用户感知的话,就是界面比较卡。
9.708
[GC (Allocation Failure) 477736K->478344K(927744K), 0.1342019 secs]
9.934
垃圾回收用了 100多毫秒
九、常量池
常量池属于方法区,分为静态常量池和运行时常量池。命令 javap -v Person.class
静态常量池:下面这个就是我们说的静态常量池,存放,字面量 String i = “lgj”,这个 lgj 就是字面量,符号引用:调用方法的时候,比如说String类,java.lang.String就是符号引用,前面这些都是在我们的class里面。
运行时常量池:类加载器 – 运行数据 – 方法区
比如说某个对象(13号技师实例),我们持有符号引用 -->直接引用(13号技师的hash值),来找到13号技师
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值