阅读本文 你能学到什么?

  • 基本数据类型有哪些
  • java中的四种引用类型
  • 四种引用的应用场景
  • ThredLocal是什么?
  • ThredLocal是如何产生内存泄漏的?
  • ThredLocal的内存泄漏如何避免?
    以上问题 本章都有大致的讲解 请认真阅读吧!

 Java的数据类型有几种?

沐雪告诉你,就2种,基本数据类型引用数据类型,别扯那些有的没的

  • 基本数据类型就8种:byte short char int float long double boolean
package cn.allms.base.type;

/**
 * 基本数据类型
 * @author qfmx
 */
public class BaseDataType {
    public static void main(String[] args) {
        /*8种基本数据类型*/
        short a = 1;
        byte b = 1;
        char c = 1;
        int d = 1;
        float e = 1f;
        long f = 1L;
        double g = 1.00;
        boolean h = true;
        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
        System.out.println(d);
        System.out.println(e);
        System.out.println(f);
        System.out.println(g);
        System.out.println(h);
        /*8种数据类型对应的包装类型*/
        Short m1 = 1;
        Byte m2 = 1;
        Character m3 = 1;
        Integer m4 = 1;
        Float m5 = 1f;
        Long m6 = 1L;
        Double m7 = 1.00;
        Boolean m8 = true;
        System.out.println(m1);
        System.out.println(m2);
        System.out.println(m3);
        System.out.println(m4);
        System.out.println(m5);
        System.out.println(m6);
        System.out.println(m7);
        System.out.println(m8);
    }
}
  • 引用数据类型就4种:强引用、软引用、弱引用、虚引用

怎么区分基本数据类型与引用数据类型

  • 将变量重新赋值为null,报错无法编译的为基本数据类型,能正常编译的为引用数据类型
  • 引用数据类型的默认值都可以设置为null,基本数据不可以哦
  • 常见的引用数据类型有:String 、File、8种数据类型对应的包装类型、对象、等

解释一下Java种的引用有哪些?

  • 强引用、软引用、弱引用、虚引用

引用数据类型之强引用

要意: 强引用就是直接指向对象开辟处理的空间的引用

// 来一段入门代码,一切对象的祖宗Object
Object o = new Object();
//这里面的 o 就是引用数据类型种的强引用
1. 怎么理解强引用?从对象加载来看
    首先 new Object() 
    首先在内存中开辟一快内存空间
    加载构造方法
    成员变量赋初始值
    加载方法 变量赋值等
    最后再把o的引用指向这块内存空间
 2. 由于 o 引用 是直接指向这块内存空间的,所以o就是强引用      
  • 小Demo: 通过下面的小示例可以看到只有当p的引用指向其他 p = null;时 强引用才会消失 P对象才会被垃圾回收机制回收,否则,宁可产生OOM(内存泄漏)也不会回收强引用对象。

    package cn.allms.base.cite;
    
    /**
     * @author qfmx
     */
    public class P {
        /**
         * 垃圾回收机制 回收完了会调用这个对象
         * 当对象被回收时,会被调用
         */
        @Override
        public void finalize() throws Throwable{
            System.out.println("垃圾已经回收完了");
        }
    }
package cn.allms.base.cite;

import java.io.IOException;

/**
 * Java四大引用之 强引用:普通的引用,对象变成垃圾时候回收
 *
 */
public class StrengthCite {
    public static void main(String[] args) throws IOException {
        // 创建一个强引用对象,p
        P p = new P();
        // p的引用指向了null ,new P()就会成为垃圾 被回收检测到
        p = null;
        // 显式调用垃圾回收 回收P对象
        System.gc(); // 垃圾回收不是在main线程调用,是自己开的一个独立线程
        // p对象执行null p引用不属于任何对象
        System.out.println(p);
        // 阻塞,便于查看结果
        System.in.read();
    }
}

引用数据类型之软引用

  • SoftReference
  • softReferenceQ 指向 SoftReference 软引用指向 new byte[1024 1024 10] 10M的字节数组
  • 特点:内存不够了,会被GC垃圾回收机制回收
  • 应用场景:做缓存,把资源放在内存,增加访问速度 内存不够时候才干掉资源
package cn.allms.base.cite;

import java.lang.ref.SoftReference;

/**
 * Java四大引用之 软引用: 垃圾回收器会在 内存不够时候回收:如你堆内存只有20M,软引用对象10M,
 * 现在来了一个15M的新对象,此时堆内存不够了,就会把软引用指向的对象回收掉,保证新对象能够被创建
 * 软引用实用场景:做缓存。把资源放在内存,增加访问速度 内存不够才干掉资源
 * 本次实例需要初始化堆内存大小 设置JVM Xmx参数的大小 为20M,一些讲解如下
 * -Xms:20M 初始化堆内存大小
 * -Xmx:20M 堆内存最大为20M
 * -Xmn:20M 堆内存最大值
 * -Mmn:10M 新生代内存设置
 * -XX:+PrintGcDetails 用于打印GC的日志信息
 * -verbose:gc 用于查看Java垃圾收集的结果
 * @author qfmx
 */
public class SoftCite {
    public static void main(String[] args) {
        // 创建一个软引用 SoftReference 指向一个 10M数组
        // softReferenceQ 指向 SoftReference 指向 byte[] 数组
        SoftReference<byte[]> softReferenceQ = new SoftReference<>(new byte[1024 * 1024 * 10]);
        System.out.println(softReferenceQ.get());
        System.gc();
        try {
            Thread.sleep(500);
        }catch (Exception e){
            e.printStackTrace();
        }
        // 再拿一次对象
        System.out.println(softReferenceQ.get());
        // 创建15M数组
        byte[] bytes = new byte[1024 * 1024 * 12];//12M 15M 为了使得堆内存不够
        // 再拿一次对象,此时返回null
        System.out.println(softReferenceQ.get());
    }
}

引用数据类型之弱引用

  • WeakReference
  • 特点: 一遇到GC就会被回收
  • 应用: 解决内存泄漏问题
  • 弱引用还用于ThreadLocal中,Entry 就是集成一个弱引用对象
package cn.allms.base.cite;

import java.lang.ref.WeakReference;

/**
 * Java四大引用之 弱引用:垃圾回收器看见就回收,一遇到gc就会被回收
 * 使用场景:弱引用一般用于解决某些地方的内存泄漏问题
 * 使用场景:弱引用还用于ThreadLocal中
 * @author qfmx
 */
public class WeakCite {
    public static void main(String[] args) {
        /**
         * 创建一个弱引用 pWeakReference 指向 WeakReference 指向一个P对象
         */
        WeakReference<P> pWeakReference = new WeakReference<>(new P());
        System.out.println(pWeakReference.get());
        System.gc(); // 显示调用gc垃圾回收
        // 弱引用一遇到gc就会被回收
        System.out.println(pWeakReference.get());
        /*===================================*/

    }
}

引用数据类型之虚引用

* PhantomReference

  • 特点:虚引用:二话不说 直接回收 就跟没有一样
  • 应用:管理堆外内存,jvm垃圾回收无法直接回收堆外内存,但是可以通过虚引用去处理操作堆外内存

    1. 当虚引用指向的对象需要被释放时,把它加到QUEUE中
    2. 当QUEUE中有值时调用垃圾回收,此时可以把虚引用指向的堆外内存也删除(也是gc线程来回收这个堆外内存)
    3. 这样就间接达到了管理堆外内存的目的
package cn.allms.base.cite;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;

/**
 * Java 四大引用之 虚引用:二话不说 直接回收 就跟没有一样
 * 应用:管理堆外内存,jvm垃圾回收无法回收堆外内存,但是可以通过虚引用去处理操作堆外内存
 *      1. 当虚引用指向的对象需要被释放时,把它加到QUEUE中
 *      2. 当QUEUE中有值时调用垃圾回收,此时可以把虚引用指向的堆外内存也删除(也是gc线程来回收这个堆外内存)
 *      3. 这样就间接达到了管理堆外内存的目的
 *
 * @author qfmx
 */
public class VainCite {
    /**
     * 用于撑满内存 new List
     */
    private static final List<Object> LIST = new LinkedList<>();
    /**
     * 引用存放的队列
     * 当一个虚引用指向的对象被回收时 它会把这个对象放在这个队列里面
     */
    private static final ReferenceQueue<P> QUEUE = new ReferenceQueue<>();

    public static void main(String[] args) {
        PhantomReference<P> pPhantomReference = new PhantomReference<>(new P(), QUEUE);

        new Thread(() -> {
            // 不断在内存家数据
            while (true) {
                LIST.add(new byte[1024 * 1024]);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // 当内存被占满时拿到虚引用对象,但是找不到
        }).start();
        // 垃圾回收线程
        new Thread(() -> {
            while (true){
                Reference<? extends P> poll = QUEUE.poll();
                if(poll != null){
                    System.out.println("虚引用对像被垃圾回收机制回收了"+poll);
                }
            }
        }).start();
    }
}

ThreadLocal与弱引用

什么是ThreadLocal?

要义:ThreadLocal中填充的线程变量属于当前线程,该变量对其他线程而言是隔离的。怎么理解呢?也就是说,每个线程里面的线程变量是独立的,隔离的,它为每个线程创建该对象的副本

ThreadLocal的应用场景

  • Spring中的事务,数据库连接,保证每个线程操作的是同一个链接;或者可以理解为,经过多次方法与方法之间的调用,每次获取到的还是同一个连接(同一个数据源)
  • Mybatis分页原理也用到了ThreadLocal
  • 小Demo
package cn.allms.base.cite;

import java.util.concurrent.TimeUnit;

/**
 * @author qfmx
 * 应用场景: 如果有1个客户端频繁的使用数据库,
 * 那么就需要建立多次链接和关闭,
 * 我们的服务器可能会吃不消,怎么办呢?如果有一万个客户端,那么服务器压力更大。
 * 这时候最好ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,
 * 且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,
 * 也不会严重影响程序执行性能。
 */
public class ThreadLocalTest {
    public static void main(String[] args) {
        ThreadLocal<M> tl = new ThreadLocal<>();
        ThreadLocal<N> tp = new ThreadLocal<>();

        // 线程 1 10s后读
        new Thread(()->{
            try{
                TimeUnit.SECONDS.sleep(10);
            }catch (Exception e){
                e.printStackTrace();
            }
            if(tl.get() != null && tp.get()!=null){
                System.out.println(Thread.currentThread().getName()+"我是线程1"+tl.get().name);
                System.out.println(Thread.currentThread().getName()+"我是线程1"+tp.get().name);
            }else{
                System.out.println(Thread.currentThread().getName()+"我是线程1,拿不到name的值"+tl.get());
                System.out.println(Thread.currentThread().getName()+"我是线程1,拿不到name的值"+tp.get());
            }
        }).start();
        // 线程 2 1秒后写
        new Thread(()->{
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch (Exception e){
                e.printStackTrace();
            }
            tl.set(new M());
            tp.set(new N());
            if(tl.get() != null && tp.get()!=null){
                System.out.println(Thread.currentThread().getName()+"我是线程2"+tl.get().name);
                System.out.println(Thread.currentThread().getName()+"我是线程2"+tp.get().name);
            }else{
                System.out.println(Thread.currentThread().getName()+"我是线程2,拿不到name的值"+tl.get());
                System.out.println(Thread.currentThread().getName()+"我是线程2,拿不到name的值"+tp.get());
            }
        }).start();

        // 线程3 5s后读
        new Thread(()->{
            try{
                TimeUnit.SECONDS.sleep(5);
            }catch (Exception e){
                e.printStackTrace();
            }
            if(tl.get() != null && tp.get()!=null){
                System.out.println(Thread.currentThread().getName()+"我是线程3"+tl.get().name);
                System.out.println(Thread.currentThread().getName()+"我是线程3"+tp.get().name);
            }else{
                System.out.println(Thread.currentThread().getName()+"我是线程3,拿不到name的值"+tl.get());
                System.out.println(Thread.currentThread().getName()+"我是线程3,拿不到name的值"+tp.get());
            }

        }).start();

        /*
        *   控制台会打印如下,说明只有当前线程才能够拿到,线程之间是独立隔离的
        *   Thread-1我是线程2李白
            Thread-1我是线程2疾风贱豪
            Thread-2我是线程3,拿不到name的值null
            Thread-2我是线程3,拿不到name的值null
            Thread-0我是线程1,拿不到name的值null
            Thread-0我是线程1,拿不到name的值null
        *
        * */
    }
}

class M {
    public String name="李白";
}

class N {
    public String name="疾风贱豪";
}

ThreadLocal 是怎么解决内存泄漏的

  • ThreadLocal 内存泄漏是怎么发生的?
  • 首先先来读一读set()源码
  // ql.set(new Q()); 程序首先会调用ThreadLocal的set()
  public void set(T value) {
      // 拿到当前线程
      Thread t = Thread.currentThread();
      // 拿到当前线程的map:Thread线程 的成员变量 threadLocals
      ThreadLocalMap map = getMap(t);
      if (map != null)
          // 以当前ThreadLocal的引用为key,对象为value
          map.set(this, value);
      else
          createMap(t, value);
  }
  //到的当前线程的map的方法
  ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
  }
  // ThreadLocal.ThreadLocalMap threadLocals = null;
  // ThreadLocalset的set方法
  private void set(ThreadLocal<?> key, Object value) {
      
      Entry[] tab = table;
      int len = tab.length;
      int i = key.threadLocalHashCode & (len-1);
  
      for (Entry e = tab[i];
           e != null;
           e = tab[i = nextIndex(i, len)]) {
          ThreadLocal<?> k = e.get();
  
          if (k == key) {
              e.value = value;
              return;
          }
  
          if (k == null) {
              replaceStaleEntry(key, value, i);
              return;
          }
      }
      // 通过Entry来设置map
      tab[i] = new Entry(key, value);
      int sz = ++size;
      if (!cleanSomeSlots(i, sz) && sz >= threshold)
          rehash();
  }
  // Entry继承弱引用类 WeakReference<ThreadLocal<?>>,所以ThreadLocal为弱引用的,在GC扫描时会被清除
  // 但当以ThreadLocal被GC清除时,key为null,此时根据key拿不到对应的value,这里就出现了内存泄漏
  static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;
  
      Entry(ThreadLocal<?> k, Object v) {
          super(k);
          value = v;
      }
  }
  // 来看看 get()源码 以当前ThreadLocal的引用为key来获取value
   public T get() {
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          if (map != null) {
              ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {
                  @SuppressWarnings("unchecked")
                  T result = (T)e.value;
                  return result;
              }
          }
          return setInitialValue();
      }

简述一下过程:

    1. 调用set()方法,拿到当前线程的一个Map
    2. 如果map不为null,就set(this,value)以当前ThreadLocal的引用为key,对象为value的一个map
    3. set方法使用一个Entry来设置值
    4. 由于Entry继承 WeakReference<ThreadLocal<?>>弱引用对象,即ThreadLocal的引用就为一个弱引用对象
    5. 当遇到GC时 ThreadLocal的引用会被回收,回收后key变成了null
    6. 调用get()方法时,通过key=null 拿不到对应value对象 ,value对象一直不会被回收,这里就产生了内存溢出
    • ThreadLocal 内存泄漏是怎么解决的

    使用ThreadLocal的remove方法,观察源码,简述一下处理过程:

    1. 拿到当前线程的Map 存放key value的记录 有可能某些key已经为null
    2. 调用remove方法进行清除记录
      public void remove() {
              // 得到当前线程的Map对象
               ThreadLocalMap m = getMap(Thread.currentThread());
               if (m != null)
                   m.remove(this);
           }
      // 清除
      private void remove(ThreadLocal<?> key) {
                  Entry[] tab = table;
                  int len = tab.length;
                  int i = key.threadLocalHashCode & (len-1);
                  for (Entry e = tab[i];
                       e != null;
                       e = tab[i = nextIndex(i, len)]) {
                      if (e.get() == key) {
                          e.clear();
                          expungeStaleEntry(i);
                          return;
                      }
                  }
              }

    解决方案:set完后必须remove

      package cn.allms.base.cite;
      
      import java.util.concurrent.TimeUnit;
      
      /**
       * @author qfmx
       */
      public class TheadLocalTest2 {
          public static void main(String[] args) {
              ThreadLocal<Q> ql = new ThreadLocal<>();
              // 单线程
              new Thread(()->{
                  try{
                      TimeUnit.SECONDS.sleep(1);
                  }catch (Exception e){
                      e.printStackTrace();
                  }
                  ql.set(new Q());
                  if(ql.get() != null){
                      System.out.println(Thread.currentThread().getName()+"我是线程2,name为:"+ql.get().name);
                  }else{
                      System.out.println(Thread.currentThread().getName()+"我是线程2,拿不到name的值"+ql.get());
                  }
                  // 解决内存泄漏
                  ql.remove();
              }).start();
          }
      
      
          /*
           *  打印
           *  Thread-0我是线程2,name为:情人节快乐
           *
           * */
      }
      class Q{
          public String name = "情人节快乐";
      }
    

    四种引用的总结

    • 强引用:一般会一直存在,不会被GC回收,仅当强引用对象变成垃圾时,才会被GC回收,或者程序结束时。
    • 软引用:当jvm堆内存不够时,会被gc回收
    • 弱引用:一遇到gc就会被回收
    • 虚引用:完全感觉不到存在,对象要被回收时,会给一个通知,把虚引用对象加入一个队列,只要这个队列里面有值(这时可以操作删除对应的堆外I内存等),立马会被真正回收

    下期聊一聊 Java 数组