java虚拟机入门(三)- 你了解对象吗

  对象对于java程序员来说,那是想要多少就有多少,所以那些嘲笑程序员的单身狗,哼,只有无知使你们快乐,想我大java开发,何曾缺少过对象。我们不仅仅知道创建对象,还知道创建对象的过程是啥样的,不信?往下看。

一、论程序员的对象由来

  我作为java程序员都知道new Object()可以创建一个对象,那么new Object()为啥就能创建一个对象呢,首先我们需要了解对象是怎么一步步创建的:

  1.检查加载

    首先我们要检查这个指令的参数能否在常量池中定位到一个类的符号引用(符号引用我理解就是由于对象具体地址位置,用字面量来代替,等运行时再用实际的引用地址替换),并检查类是否已经被加载,解析,初始化,(作为严谨的程序员,对象还是要先检查有没有男朋友,有没有结婚,坚决不做接盘侠)

  2.分配内存

    确定单身之后,那就要给她一个家,虚拟机会给新生对象分配内存(内存大小是确定的)。内存分配的方式主要有两种流派:指针碰撞,空闲列表

      2.1 指针碰撞:当需要内存时,将指针向后移动一定大小的内存,指针将已用内存和未使用内存分开。这种方式要求内存是完全规整的。

                                

 

 

     2.2 空闲列表:这种方式虚拟机需要维护一个列表,记录哪些内存块可用,在内存分配时从列表中找到一个足够大的内存区域分配给对象实例(必须是连续的内存区域),并更新列表上的记录,这种方式看着就比上述指针碰撞复杂,但是并不是所有的垃圾收集器都可以贤惠的将内存规规整整的分好(具体后面垃圾回收章节会细讲)。

                                     

 

 

 

怎么选择其实上面也算是讲到了,指针碰撞简单粗暴,要求你有个贤惠的垃圾回收器,空闲列表不需要那么高的要求,但是你自己做的就多一点。对象的创建很频繁,可以使用指针碰撞自然最好,否则就用空闲列表,而在如此频繁的对象创建过程,内存分配是否会出现并发安全的问题呢,答案是会,那么我们来了解一下jvm虚拟机是如何处理并发分配内存的问题呢:

  解决这个问题主要有两种方案:1. 对内存分配进行同步处理,其实虚拟机采用cas加失败重试保证更新操作的原子性,

                2. 分配缓冲:默认是打开的,所以除非特殊需求,一般公司用的都是这种方式。分配缓冲就是在每个线程都在java堆中分配一块内存,jvm在线程初始化的时候,都会申请一块内存,只给当前线程使用,如果当前线程分配的内存不够用  了,在重新从Eden区申请一块继续使用,分配缓冲英文:Thread Local Allocation Buffer ,也叫TLAB(是不是看到这个比分配缓冲要熟悉很多),TLAB其实就是用空间换时间,让每个线程都有一块内存(不管你用没用),这样可以减少同步开销,但是也会加大内存开销。其实看到这里我发现jvm越来越趋向于用空间换时间,越新的垃圾回收器内存需求越大,但是吞吐量也越大。

         

  3.内存空间初始化

    小伙伴们,有没有发现实例对象明明只定义了一个引用,但是却依然可以访问(如int为0,普通对象为null),不过局部变量不可以,你想不初始化就用,编译器不会放过你的。这一步基本可以保证所有的没有初始化的对象都可以使用。

 

  4.设置

    这一步虚拟机会对你要创建的对象做必要的设置,比如对象是哪个类的实例,类的元数据信息设置,对象哈希码,GC分代年龄信息等,这些信息存放在对象的对象头中,这一步完成,从虚拟机角度来说,新对象诞生了,也可以看出,这一步之前,除对象头之外的所有信息都创建完成。

 

  5.对象初始化

    这一步才是我们普通码农角度的new对象,就是执行构造方法,进行对象初始化。

  很神奇,我们明明就用了一个new,jvm却做了那么多事情,jvm还是很贴心的,作为java程序员还是很幸福的,但是同时,java程序员的差距也是很大的,正是因为jvm保姆太贴心,导致很多做java的只会写crud,尤其对于我们这些跨专业的人来说,体现的更是淋漓尽致,所以还是加油吧。

 

二、剥开对象的神秘外衣

  对象创建完了,那么对象她的内涵是啥样的呢,是的你没听错,我们程序员最注重的还是内涵,那就让我们剥开她神秘的外衣,一探究竟吧:

                

 

首先说一下,这张图是我凭实力copy过来的,对象的存储布局主要分为对象头,实例数据,对其填充,

对象头包含对象的运行时数据如哈希码,GC分代年龄(这个后面垃圾回收会讲到,结合起来会对对象理解的更透彻),锁状态表示(这块可以了解下synchronized锁优化),线程持有的锁,偏向线程id,偏向时间戳等,对象头另一个重要的元素就是类型指针,字面意思就很好理解,指向这个对象是属于那种类的实例。如果是数组对象,还会额外有记录数组长度的数据(我不知道会不会有其他人跟我一样,在刚开始看源码的时候,看到list对象里面有长度,但是数组却没有,而且也无法看源码,心里很难受,总觉得这个数组多长不知道是谁控制的,后来了解了对象的类型,才算是有所领悟)。

对象的另一个组成部分实例数据应该没什么好说的,就是存储的数据嘛

对其填充其实就是hotsport vm要求对象大小必须为8字节整数倍,所以当不满8字节的时候,就需要这部分填满。

 

三、如何找对象

  单身的朋友看过来了,大型情感类教学文章的精华开始了,请仔细观看,看到这里很多人就蒙了,程序员都这么找对象的吗,明明前面第一节讲了对象的由来,然后又剥开了对象外衣,这到现在才说如何找对象,不过这才是真正强悍的程序员,不是普通码农可比的。见过了强悍的实力,教学才更有说服力。

  言归正传,本节讲的是如何找对象,找对象呢一般分为两种,媒婆介绍和自己直接认识,媒婆介绍呢就是其实就是我不知道我的对象在哪,但是我能找到媒婆,她知道我的对象在哪,这种就是句柄的方式,直接认识就很简单粗暴了,自己认识,直接找就行了,这种在程序员的世界也叫直接指针。

  句柄:java堆中会划分一块内存做句柄池,句柄中包含对象的具体地址信息,我们的引用存储句柄地址

  直接指针:对象引用存储的是对象的地址

  这两种找对象的方式各有优劣,句柄方式由于不直接认识对象,所以想换对象很简单,跟媒婆说一声就行了,但是呢每次都必须通过媒婆去找,浪费时间和金钱,自己认识呢,好处就是找对象直接就可以找,但是想换就必须自己去重新找到下一个对象,还得把这个对象换掉。

  估计要是有弹幕肯定很多人都会打出:裤子都脱了你就给我看这个? 哈哈 那我们聊一块钱正经的。

四、对象的引用

  对象的引用主要分为四种:强引用,软引用,弱引用,虚引用

 

  1.强引用:一般Object obj = new Object();就属于强引用,强引用对象只要有强引用关联(GC Root可达),垃圾回收器就不会回收。这种是我们平时用的最多的,毕竟new对象嘛。

  2.软引用:软引用就是比强引用稍次一点的,它在系统将要发生内存溢出时,这些对象就会被回收。这种比较适合用来做缓存这种可以被随时回收,但是又不会经常被出发回收。我们看下代码:

     

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());
        }

    }
}

  设置vm参数:-Xms20m -Xmx20m -XX:+PrintGCDetails,前俩参数限制堆大小不超过20m,后一个打印gc日志看执行结果:

   

 

   可以看到第一次gc甚至full GC之后,对象依然没有被回收,随后循环多次,执行了多次FullGc,但每次都没回收多少,而且一直内存占用很高,这说明即将oom了,明显报错之后,对象被回收了。

  3.弱引用:弱引用就是更次一点了,用的地方不太多,比如经典的ThreadLocal(也因为这个存在内存泄漏问题),弱引用在每次gc时都会回收对象,看代码:

  

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());
    }
}

代码很简单,创建一个弱引用对象,随后触发gc,然后看看对象是否被回收:

 

 很明显结果是被回收了,所以也印证了上述结果。

 

4.虚引用:也叫幽灵引用,听名字就感觉随时都可能被回收掉,确实是这样,据我所知,只有类似心跳机制监控垃圾回收器是否正常工作会用到,尝试测过,基本上测出来的都是null,被回收掉了。

 

总结:

对象到这里基本上算是介绍完了,考虑过要不要把判断对象存活放到这一章节来讲,想想应该还是属于垃圾回收,避免只看垃圾回收的连根可达介绍都看不到,每一个程序员多多少少都会有求知欲,比如我们为啥我们写了new之后,对象就创建了,其实挖到更深也就是数据以一种什么样的方式放到内存中,看完这一章,对对象也有个基本的认识了,从new对象时jvm做了哪些操作,到对象构成,对象如何访问,以及对象的引用,最后也聊到了垃圾回收的一部分,这也表示我下一章要开始介绍垃圾回收了,垃圾回收和jvm调优密切相关,所以,应该也是jvm中最重要的,但是要彻底了解垃圾回收,还是需要前面这些知识的积累的,java由于非常庞大的生态,更新迭代也是很快的,而在新的版本也是对java做了很多优化,很多时候我们不需要做多余的配置,但是不同的公司,不同的业务场景,计算机也不可能做到每一步都兼顾,所以,这才有程序员----在有限的资源,做最多的事情。

 

posted @ 2020-10-16 01:17  吃肉不长肉的小灏哥  阅读(145)  评论(0编辑  收藏  举报