简单的垃圾回收机制,一句话概括“看得见的垃圾”

    垃圾回收

          1、垃圾回收前需要先了解的知识:

    public static void main(String[] args) {
        int i = 0;
        try {
        } catch (Throwable e){
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }

             1、增加-XX:+PrintStringTableStatistics  展示字符串池中的个数 以及大小信息

              2、增加-XX:+PrintGCDetails -verbose:gc 打印垃圾回收的详细信息

              3、 heap:内存占用情况

                1、PSYoungGen新生代:总大小:2560K    使用了:1566k

                2、ParOldGen老年代:总大小7168k,使用正常

              

                3、SymbolTable statistics:符号表,类的字节码中有哪些类名,方法名等

              

                4、StringTable statistics:字符串池的统计信息(重点

            

               字符串池在底层是hashTable的格式,hashTable是数组加链表的格式,数组的个数,称之为筒

               Number of buckets(初始化数组 ): 60013个

               Number of entries(当前数组的个数):1732个

               Number of literals(字符串个数):1732个

               Total footprint(总占用):676240 bytes

               当此时新增100个对象intern()后,就会变成1832个,很明显看出区别。如果不intern()则不会进入常量池也就没有区别

   public static void main(String[] args) {
        int i = 0;
        try {
            for (int j = 0; j < 100; j++) {
                String.valueOf(j).intern();
            }
        } catch (Throwable e){
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }

           2、查看垃圾回收效果

                   1、将堆内存调整为10m:-Xmx10m

                   2、尝试往字符串常量池中存入10000个对象

    public static void main(String[] args) {
        int i = 0;
        try {
            for (int j = 0; j < 10000; j++) {
                String.valueOf(j).intern();
            }
        } catch (Throwable e){
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }

                 3、正常字串符个数应该增加10000个,可结果为以下2520个,结果不对,所以,是出发了垃圾回收,查看最上方

                

                4、最上方发现GC为垃圾回收,下方解释为:新生代垃圾回收,从2048回收到了488k,times为时间

        3、StringTable性能调优

                  1、数组数量越少,hash碰撞的可能性就越大,hash碰撞后,形成了链表,链表越长,查询速度就会收到影响

                  增加数组的容量  -XX:StringTableSize=200000 之前默认为60000个

                  2、如果可能会有重复,内存占用优化,减少堆占用,存入StringTable中

                   如果有10万条数据,如果是存入堆,那就是30万数据。由于字符串池中是不重复的,所以将数据存入字符串池中,

                  重复数据过滤,也就实现了内存占用优化。调用intern()

        4、直接内存调优

            1、什么是直接内存

                     直接内存是操作系统内存,不受JVM内存回收管理,分配回收需要自己来实现,读写性能高,复制以下代码测试直接内存和io的速度

    private static String FROM = "文件位置";
    private static String TO = "赋值到哪";
    private static int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byteBuffer(); //196.8509
        io(); //520.2458
    }

    private static void byteBuffer() {
        long startTime = System.nanoTime();
        try (
                FileChannel from = new FileInputStream(FROM).getChannel();
                FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1MB);
            while (true) {
                int len = from.read(byteBuffer);
                if (len == -1) {
                    break;
                }
                byteBuffer.flip();
                to.write(byteBuffer);
                byteBuffer.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("直接内存用时:" + (System.nanoTime() - startTime) / 1000_000.0);
    }

    private static void io() {
        long startTime = System.nanoTime();

        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO)) {
            byte[] buf = new byte[_1MB];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("io用时:" + (System.nanoTime() - startTime) / 1000_000.0);
    }

           2、为什么直接内存会比io读取会快 

                  io:下图为例,java读取文件时,会首先调用操作系统提供的方法,也就是本地方法区的方法,之后将磁盘文件分次读取到系统缓冲区中,然后将系统缓冲区的文件读取到java的缓冲区中,由于是两块缓冲区,也就导致了慢

           

           直接内存:当调用ByteBuffer.allocateDirect方法时,也就意味着在系统内存这边划分一块直接内存区域,这块区域的 特点是可以在java代码可以直接访问,磁盘文件可以直接将文件读取到直接内存中,java可以直接从直接内存中获取到,少了一步读取到java缓冲期中,也就更快了

           

          3、不受jvm垃圾回收管理

    static int _1G = 1024 * 1024 * 1024;
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1G);
        System.out.println("分配了直接内存1G");
        System.in.read();
        byteBuffer = null;
        System.out.println("开始释放内存");
        System.gc();
        System.in.read();
    }

            1、上方代码分配了直接内存之后,发现idea新增了1g的内存,执行了垃圾回收之后,发现就会释放了

           

            2、执行完垃圾回收后,直接内存释放的原因

                1、实际上ByteBuffer内部是调用Unsafe类来执行的对直接内存的操作,而unsafe必须调用freeMemory方法才可以释                放内存那接下来就看下ByteBuffer.allocateDirect内部是如何操作的

  static int _1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        //获取操作直接内存的类
        Unsafe unsafe = getUnsafe();
        //分配1g直接内存 返回分配的地址
        long base = unsafe.allocateMemory(_1GB);
        unsafe.setMemory(base,_1GB,(byte) 0);
        System.in.read();

        //根据地址释放内存  直接内存必须调用释放内存的方法才可以释放
        unsafe.freeMemory(base);
        System.in.read();
    }

    public static Unsafe getUnsafe() {
        try {
            //获取到theUnsafe字段
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            Unsafe o = (Unsafe)theUnsafe.get(null);
            return o;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

                  2、allocateDirect 只new了一个DirectByteBuffer

                   

                   可以看到这里面直接调用了unsafe方法,写的和上面的unsafe调用类一模一样,下方还调用了一个叫Cleaner类,

                  这个类为虚引用对象,虚引用的特点为,当虚引用对象被垃圾回收时,会执行类中的run方法

                  

                  此时看到run方法就恍然大悟了,就是调用了freeMemory方法来回收的直接内存,而不是用的jvm的垃圾回收

                  

   2、垃圾回收

               1、判断对象是否可以回收的算法

                        1、引用计数法

                            只要一个对象被其他对象所引用,就让这个对象计数+1,如果其他对象不再引用,这个对象计数-1,当计数                               为0时,就代表可以回收了

                            弊端: 

                              当a对象引用b,b对象引用a,这样就会导致两个对象引用计数都是1,也就造成了a,b对象内存不会被回收

                        2、可达性分析算法 

                              1、首先要确定根对象,在垃圾回收前,会对堆中所有的对象进行扫描,查看每个对象是否被根对象直接或 者间接的引用,如果是,不能被回收,否则,就可以被回收

                              2、哪些对象可以被认定为根对象

                                     1,栈帧中的引用对象

                                     2,方法区中的类引用的对象

                                     3,本地方法栈中引用的对象。

              2、四种引用对象的方式

                        1、强引用

                            String a = "a";  这种就是强引用

                            特点:是当根对象正在关联该对象,则不会被垃圾回收

                        2、软引用

                            1、当第一次垃圾回收后,发现内存还是不够,还会再次进行垃圾回收,此时会把软引用的对象回收。

                            2、可以配合引用队列进行操作,当引用的对象回收后, 自己本身虽然为null,但也是一个对象,配合引用队列后,可以将自身清理

                            3、举例:从下图中可以看出第四次存入软引用对象时发现内存不足了,也就导致了第一次垃圾回收,新生代从4774K 清理成了 4558K,老年代从12476K清理成了12444K,发现还不足,也就导致了第二次的垃圾回收,这次回收就将软引用对象清理了,可以看出新生代直接清理为了0,老年代也直接清理为了601k。二次调用查看对象时可以发现也只有第5个存成功了

    static int _4M = 1024 * 1024 * 4;
    public static void main(String[] args) throws IOException {
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            //SoftReference 弱引用对象  当内存不足时 会释放该对象
            SoftReference softReference = new SoftReference<>(new byte[_4M]);
            list.add(softReference);
            System.out.println("添加【"+(list.size())+"】时 : " + softReference.get());
        }

        System.out.println("===============");
        for (SoftReference<byte[]> softReference : list) {
            System.out.println("调用时:" + softReference.get());
        }
    }

                       4、配合引用队列,将为null的也就是已经被垃圾回收的引用队列从list中清除

 static int _4M = 1024 * 1024 * 4;
    public static void main(String[] args) {
        //引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            //当软引用所关联的byte数组被回收时,软引用本身会加到ReferenceQueue中
            SoftReference softReference = new SoftReference<>(new byte[_4M],queue);
            list.add(softReference);
            System.out.println("添加【"+(list.size())+"】时 : " + softReference.get());
        }

        //会返回第一个传入的引用对象
        Reference<? extends byte[]> poll = queue.poll();
        //当引用对象不为null时,也就证明引用对象已经被清理而且加入到了引用队列中
        while (poll != null){
            //删除被清理的引用对象
            list.remove(poll);
            //返回下一个引用对象
            poll = queue.poll();
        }

        System.out.println("===============");
        for (SoftReference<byte[]> softReference : list) {
            System.out.println("调用时:" + softReference.get());
        }
    }

               3、弱引用 

                       当垃圾回收时,不管内存是否充足,都会把弱引用垃圾回收,也可以和引用队列配合使用

                       举例:发现第四次内存不足时,直接调用了垃圾回收,但这次没有像软引用一样,第二次才将对象清理,而是第一次就将对象直接垃圾回收了

    static int _4M = 1024 * 1024 * 4;
    public static void main(String[] args) {
        //引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            //当弱引用所关联的byte数组被回收时,弱引用本身会加到ReferenceQueue中
            WeakReference softReference = new WeakReference<>(new byte[_4M],queue);
            list.add(softReference);
            System.out.println("添加【"+(list.size())+"】时 : " + softReference.get());
        }

        //会返回第一个传入的引用对象
        Reference<? extends byte[]> poll = queue.poll();
        //当引用对象不为null时,也就证明引用对象已经被清理而且加入到了引用队列中
        while (poll != null){
            //删除被清理的引用对象
            list.remove(poll);
            //返回下一个引用对象
            poll = queue.poll();
        }

        System.out.println("===============");
        for (WeakReference<byte[]> softReference : list) {
            System.out.println("调用时:" + softReference.get());
        }
    }

                 4、虚引用

                      必须配合引用队列来使用,例如之前说的直接内存调用,ByteBuffer.allocateDirect中使用的Cleaner,就是虚                             引用对象,当虚引用对象被清理时,虚引用本身会进入引用队列中,随后会被一个专门的线程去查找引用队列中新添加的虚引用,调用虚引用对象的clean方法

        3、垃圾回收算法

                1、标记清除算法

                       先标记出没有引用的对象标记出来,之后再清除

                       优点:清除速度快,因为只需要标记后清除

                       弊端:清除后会有多个内存碎片,清除后不会整理,再想放进去只能寻找足够的空闲位置

                      

                 2、标记整理

                        先标记出没有引用的对象标记出来,再整理有用的对象,将对象按顺序排列,最后清除没有引用的对象

                         优点:不会有内存碎片

                         缺点:有了整理这一步,会将对象地址改变,效率就会慢

                      

                3、复制

                       会将内存区域分为两部分,一个是from,一个是to,首先将不被引用的对象标记为垃圾,然后将from存活的对象                       复制到to中,复制到to中的会整理,然后清除from中的对象,最后将to和from交换

                        优点:不会产生碎片

                        缺点:会占用双倍的内存空间

                     

        4、JVM分代垃圾回收

                      1、伊甸园

                        刚创建好的对象就会进入到伊甸园中,当执行新生代(Minor GC)垃圾回收时,没有引用的对象就会被清理

                      2、幸存区

                        当执行完一次新生代垃圾(Minor GC)回收后,还有引用存活的对象会进入到FROM中,然后标记FROM中没有引用的对象,之后将有引用的放入到TO中,会将有引用的对象寿命+1,清除from中的对象,然后将TO的内存和From进行交换 

                     3、老年代

                         1、当幸存区中的对象寿命超过了预值(默认为15)后,会将幸存区对象晋升到l老年代中,当老年代对象也满了,此时会执行老年代垃圾回收(Full GC),老年代垃圾回收会将所有的内存进行回收

                          2、大对象直接进入老年代

                    4、特点

                       当执行垃圾回收时,所有线程都会停止,等垃圾线程执行完其他线程才能恢复运行

                    5、垃圾回收参数

                 

                

         5、垃圾回收器

                  1、串行(标记+整理)

                      1、是单线程的

                      2、堆内存小,适合个人电脑

                      3、由于是单线程的,当执行垃圾回收时,其他线程都会到阻塞状态

                      4、-XX:+UseSerialGC  新生代为复制算法,老年代为标记整理算法

                     

                  2、吞吐量优先(1.8默认回收器)(标记+整理)

                      1、多线程并行执行,执行时垃圾回收器一拥而上,一起处理垃圾,可能会出现cpu突然增高的情况

                      2、堆内存大,适合服务器

                      3、每次工作时间比较长,然后一次休息够,就好像一次充满,用完再充电

                      4、-XX:+UseParallelGC 或者 -XX:+UseParallelOldGC,开启其中一个就可以 新生代为复制算法,老年代为标记整理算法

                      5、-XX:+UseAdaptiveSizePolicy 动态调整新生代的内存大小,还有晋升阈值等

                      6、-XX:GCTimeRatio=1 调整垃圾回收时间和总时间的占比  1/(1+ratio)公式,ratio的默认值为99, 1/(1+99) = 0.01 也就是垃圾回收时间不能超过总时间的百分之1,

                            100分钟只能有1分钟在垃圾回收,如果超过这个比例,垃圾回收器就会自动调整堆的大小已达到目的,一般是将堆增大  一般可以设置为19

                      7、-XX:MaxGCPauseMillis=100 每次最大垃圾回收时间

                      8、-XX:ParallelGCThreads 同时执行垃圾回收器的线程数

                       

                  3、响应时间优先(cms)(标记+清除)

                       1、由于是标记清除算法,会产生内存碎片,在内存碎片比较多的情况下,造成将来分配对象时,新生代内存和老年代内存都不足,会造成并发失败,采取补救措施.让cms进行一次串行的垃圾回收(标记+整理),然后再进行cms清理,调用串行垃圾回收叫fullGC

                       2、运行流程 

                              1、初始标记,只初始化一些根对象,其他线程都是阻塞的,速度比较快,基本不会影响用户使用

                              2、并发标记,用户线程恢复运行,不影响用户使用,垃圾回收线程还可以继续标记那些剩余的没有标记的垃圾

                              3、重新标记,所有线程都会重新标记,重新标记的意义在于并发标记时其他线程也在运行,其他线程工作时也可能有产生一些垃圾,此时把那些产生的垃圾标记

                              4、并发清理,用户线程可以正常运作,垃圾回收线程把标记好的垃圾回收。由于其他用户线程还在继续运行,用户线程运行时可能会出现新的垃圾,此时创建的新的垃圾只能下次垃圾回收再清理

                        3、-XX:+UseConcMarkSweepGC(标记清除)  ~ -XX:UseParNewGC(复制)  垃圾回收时其他用户线程某些时间也可同时运行

                             -XX:ParallelGCThreads=n  同时执行垃圾回收器的线程数

                             -XX:ConcGCThreads=threads  同时执行垃圾回收的线程数  建议设置1 比 4的比例  1个垃圾回收服务4个线程

                             -XX:CMSInitatingOccupancyFraction=percent  执行垃圾回收的内存占比  只要老年代的垃圾到达一定程度  就会执行垃圾回收

                            -XX:CMSScavengeBeforeRemark  初始标记前,先执行一次新生代垃圾回收

                     

                  4、G1(jdk9默认)(整体是标记+整理算法,两个区域之间是标记+复制算法

                      1、同时注重吞吐量和响应时间

                      2、-XX:MaxGCPauseMillis=ms设置最大暂停时间 

                      3、-XX:UseG1GC 开启g1

                      4、-XX:G1HeapRegionSize=size  每个区域的大小

                      5、当垃圾回收速度比创建垃圾速度慢时,会调用串行垃圾回收,此时,才会产生fullGC,否则不会产生fullGC

                      6、会将堆划分为三个阶段,三个阶段是一个循环的过程,每个阶段都可以划分为伊甸园  幸存区  老年代   

                          1、新生代垃圾回收阶段(Young Collection)(新生代对象占满会产生)

                                1、白色代表空闲区域,E为伊甸园,当E占满后,会触发新生代垃圾回收。

                                2、S为幸存区,清理时会将未清理对象以复制的算法复制到幸存区中。

                                3、O为老年代,当幸存区也不足时,幸存区到达阈值的会进入老年代,大对象也会直接进入老年代

                                  

                                 

                        2、新生代垃圾回收 同时 并发标记阶段(Young Collection + Concurrent Mark)(老年代对象内存占到一定阈值)

                                 1、垃圾回收过程中,会对对象进行初始标记和并发标记

                                 2、初始标记

                                      标记根对象,会在新生代垃圾回收时

                                 3、并发标记

                                       根对象下的关联的对象,当老年代内存比例到达阈值,才会发生。标记时不会产生线程暂停 

                                 4、-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

                                       老年代占用到百分之45时就会并发标记

                                

                          3、混合回收 (Mixed Collection )(伊甸园,幸存区,老年代全面垃圾回收)

                                1、重新标记,和cms一样,并发标记时用户线程也会产生新的对象,将并发标记过程中,可能会漏的垃圾对象,进行最终标记,会STW

                                2、拷贝存活,伊甸园会用复制算法复制到新的幸存区,幸存区中没有到达年龄的也会复制到新的幸存区,老年代也会复制到新的老年代。优先会根据最大暂停时间将老年代垃圾最多的的进行复制,复制的区域少,也就能达到最大暂停时间,此段时间会STW 

                                3、当垃圾回收速度比创建垃圾速度慢时,才可以理解为full GC

                               

               7、新生代回收跨代引用(cart表)

                   1、要想查找对象,首先要找根对象,跟对象可能会来自于老年代,老年代存活对象可能会比较多,也就导致效率查找老年代对象效率低

                      解决方案:使用了一个cart表的技术,将老年代继续细分,分成一个个的cat如果老年代引用了新生代对象,将老年代的cart标记为脏cart,下次直接找根对象就可以直接找脏cart区域就可以了,减少了查找的范围

                   2、如何标记脏cart

                      每次对象的引用发生变更,异步更新脏cart

                    

         6、类加载器

                 类加载器会有层级的关系,会优先上级加载,如果上级加载过该类,则由上级加载类

                 双亲委派就是上下级的关系,上级加载了类就不会加载下级的类

                 双亲委派好处:保证加载了上级就不会再加载下级。    也防止了 如果下级写了同样的类,绝对会加载的是上级的类

                 1、Bootstrap ClassLoader 启动类加载器 (jre/lib下的类)       无上级

                    打印为null,这就是启动类加载器加载的

    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("java.lang.Integer");
        //打印 null
        System.out.println(aClass.getClassLoader());
    }

                 2、Extension ClassLoader 拓展类记载器(jre/lib/ext下的类)       上级为Bootstrap ClassLoader

                     将类达成jar包放入ext包中,测试拓展类加载器

                     打包:jar -cvf my.jar 文件class  并放入 ext文件夹下,注意打包后的类包名要一致

                 

                    打印结果为extClassLoader,也就测试出了如果拓展类加载器有,则不调用应用程序类加载器

    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("template.P");
        //sun.misc.Launcher$ExtClassLoader@7f31245a  也就是ExtClassLoader
        System.out.println(aClass.getClassLoader());
    }

                 3、Application ClassLoader 应用程序类加载器 (类路径下的类)  上级为Extension ClassLoader

                  打印为AppClassLoader,是由应用程序类加载器加载的

    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("template.P");
        //sun.misc.Launcher$AppClassLoader@18b4aac2  也就是AppClassLoader
        System.out.println(aClass.getClassLoader());
    }

                 4、自定义类加载器   上级为Application ClassLoader

                    1、什么情况下会使用自定义类加载器

                       1、加载任意路径下的类

                       2、希望可以实现解耦

                       3、可以隔离不同应用的同名类都可以加载,例如Tomcat

public class MainTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        MyClassLoader myClassLoader = new MyClassLoader();
        Class<?> p = myClassLoader.loadClass("P");
        P o = (P)p.newInstance();
        System.out.println(o);
    }

}

class MyClassLoader extends ClassLoader{

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //自己的类文件位置
        String fileUrl = name = "d:\\" + name + ".class";
        //获取Path类
        Path path = Paths.get(fileUrl);
        System.out.println("path : " + path);

        //将路径下的文件复制到输出流中
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            Files.copy(path,os);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //输出流获取到字节数组
        byte[] bytes = os.toByteArray();

        //父类的defineClass可以将流转换为class文件
        Class<?> aClass = defineClass(name, bytes, 0, bytes.length);

        return aClass;
    }
}

                     2、步骤

                        1、继承ClassLoader

                        2、重新findClass方法,不能重新loadClass  否则不走双亲委派

                        3、读取类文件字节码

                        4、调用父类defindClass加载类

                        5、调用该类的loadClass来加载类

                 5、类加载器源码

                     先看前半部分

                     

                     1、第一遍加载P这个类,是使用的app类加载器

                     2、发现有上级ext,就执行了ext的loadClass

                     3、ext再执行到parent时,发现没有上级了,执行boot启动类加载器

                    后半部分

                   

                      3、boot不会执行下半部分代码,因为是c代码。也就返回到了ext执行findClass代码,ext执行findClass发现没有找到P对象也就返回到了app加载器中

                      4、app加载器执行findClass对象时,找到了P,也就返回了

   //加载类  第一次加载使用的是应用程序加载器
    protected Class<?> loadClass(String name, boolean resolve){
        synchronized (getClassLoadingLock(name)) {
            // findLoadedClass是先从缓存中查找是否有个这个类,第一次肯定没有 
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //判断是否有上级,应用程序加载器上级为拓展类加载器
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果没有了上级,那就找启动类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    //如果没有找到  不做处理
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    //以上都没有找到,到当前类加载器的findClass里找
                    c = findClass(name);

                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

                6、线程上下文类加载器

                  1、拿DriverManager类来举例,发现打印后是启动类加载器加载的DriverManager,因为mysql-connector-java包不在启动类加载器中,DriverManager是如何加载到一个应用类加载器中的com.mysql.jdbc.Driver的?

System.out.println(java.sql.DriverManager.class.getClassLoader()); // null  启动类加载器

                2、实际上使用的app应用程序类加载器加载的

                 ClassLoader.getSystemClassLoader():这个就是应用程序类加载器

               

               3、ServiceLoader  

                 创建 META-INF/service/java.sql.Driver 里面填写接口的实现类

            

                 遍历之后就可以拿到该接口下所有的实现类了,这里其实用的是线程类上下文类加载器,也不是启动类加载器,所以就可以实现了调用应用程序类

    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    while(driversIterator.hasNext()) {
       driversIterator.next();
    }

       7、反射优化

              1、调用invoke()方法时,内部会调用MethodAccessor方法访问器,该方法访问器16次内调用效率是比较低,原因是会调用本地方法                 invoke0,当执行了大于15次后,会将本地方法付访问器替换成运行期间动态组成的新的方法访问器,替换了原来的方法访问器,原                因是生成的代码内部直接会执行方法,直接调用方法会速度快

              2、15次是可以直接设置的,也是可以直接生成新的方法访问器,但是不建议修改,因为生成新的方法访问器需要时间,如果访问次                数少就没有必要

         

public class MainTest {
    public static void foo(){
        System.out.println("foo........");
    }
    public static void main(String[] args) throws  IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, IOException {

        Method foo = MainTest.class.getMethod("foo");
        for (int i = 0; i < 16; i++) {
            foo.invoke(null);
        }
        System.in.read();
    }
}

          8、JMM java内存模型

                  定义了一套在多线程读写共享数据时,对数据的可见性,有序性,和原子性的规则和保障,和内存结构没有什么关系

                  1、 原子性

                    使用synchronize关键字,同一时间段只有一个线程可访问,既能保证可见性,也能保证原子性

                  2、可见性(volatile

                   1、t1线程会频繁的去主内存中获取run的值,运行时编译就会做优化,将run放入了t1的高速缓存中,提高效率,之后主线程将主线程中的run改成了false,t1还是在拿高速缓存中的run,也就不会停止了

                   2、可以使用volatile修饰的变量是每次都读的主线程的run volatile是保证的可见性,一个线程对变量的修改对下一个线程是可见的,不能保证原子性

public class MainTest {
    volatile static boolean run = true;
    public static void main(String[] args) throws InterruptedException {

        new Thread(() ->{
            //正常主线程将run修改为false,这里会跳出循环,如果不加volatile关键词,就退不出
            while (run){}
        },"t1").start();
        Thread.sleep(1000);
        run = false;

    }
}

                  3、有序性

                    jvm可能会对比较耗时的操作往后排,例如num + num,出现这种情况称之为“指令重排”

                    1、举例下方代码,可能会出现num = 0,可能线程2执行了ready = true,又执行了num = num + num,结果就为0

                     解决方案:ready增加volatile关键字,就不会再出现指令重写的可能了

    int num = 0;
    boolean ready = false;

    void run1(){
        if (ready){
            num = num + num;
        }else{
            num = -1;
        }
    }

    void run2(){
        num = 2;
        ready = true;
    }

                 4、CAS

                  CAS(比较与交换),体现了乐观锁的思想,比如多个线程对一个变量执行+1的操作

                  1、每次修改会判断传入值+1是否等于结果值,如果是等于,那么返回false,如果为true,则会继续循环,直到结果正确

                  2、为了保证可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核cpu的情况,因为没有多核cpu,可能会导致其他线程在阻塞

                  3、因为对比sync少了切换锁的时间,所以效率是比sync效率高的

        int params = 0;
        while (true){
            int result = params + 1;
            //每次修改会判断传入值+1是否等于结果值,如果是等于,那么返回false,如果为true,则会继续循环,直到结果正确 
            if(compareAndSwap(params,result)){
                break;
            }
        }

               5、乐观锁是悲观锁

                  1、乐观锁

                      CAS就是乐观锁,不怕别的人修改,哪怕你改了,我再重试一次就行

                 2、悲观锁

                      synchronize就是悲观锁,拒绝别的线程修改,只要自己拿到,你们都别动

               6、原子操作类

                    java.util.concurrent提供了一系列安全的操作类,可以提供线程安全的操作,例如AtomicInteger,AtomicLong,底层都是用CAS +volatile实现的

               7、synchronize优化

                   在JDK1.6以后,为了减少消耗,锁进行了很多的升级。并且有了四种状态,从低到高

                 1、无锁状态

                   锁标志头为01,是否偏移锁为0,也就是不属于偏移锁

                 2、偏向锁

                   研究发现,一些锁不仅不会被多线程竞争,并且每次都是被同一个线程锁使用,为了让这个线程获得这个锁的代价变低,而引入了偏向锁。

                   顾名思义,偏向就是偏心的意思,为这一个线程偏心的锁

                   过程:

                        1、当锁被这个线程获取到的时候,将线程id记录到偏向锁中,下次判断还是这个线程id,就直接进行同步。

                        2、如果有其他线程要访问时,也就发生了竞争,此时会将轻量锁升级为轻量锁

                 3、轻量锁

                       1、当偏向锁发生竞争升级为轻量锁,轻量锁每次退出都会释放锁,而偏向锁退出不会释放锁,退出才会释放

                       2、当有其他锁竞争时,会先进行自旋,多次判断锁是否为占用状态,此时线程不会阻塞,如果多次自旋还是没有拿到锁,才发生阻塞,此时会将轻量锁升级为重量锁

                 4、重量锁   

                     没有自旋,当有其他线程竞争时,直接进入阻塞

      9、逃逸分析

               jvm执行分为解释器转换为字节码,和即使编译器(JIT)转换为字节码

               解释器转换为字节码:每次遇到同样的代码,都会重复的解释

               即使编译器转换字节码:下次遇到同样的代码,直接执行,不需要重复编译

               代码举例: 打印结果 刚开始是327100,到最后只需要3500,证明后面根本没有执行new Object,因为即使编译器判断这段代码无用,就不再编译了

               关闭逃逸分析:-XX:-DoEscapeAnalysis  

    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 10000; j++) {
                new Object();
            }
            System.out.println(System.nanoTime() - start);
        }
    }

         

猜你喜欢

转载自blog.csdn.net/qq_38384460/article/details/112067478
今日推荐