【复习】Java基础知识

75 阅读11分钟

并发编程的三要素

  • 原子性:指的是一个操作不能再继续拆分,要么一次操作完成,要么就是不执行;
  • 可见性:指的是一个变量在被一个线程更改后,其他的线程能立即看到最新的值;
  • 有序性:指的是程序的执行按照代码的先后顺序执行。

synchorized保证了可见性、原子性和有序行

1、创建Java线程的方式

1、继承Thread类

2、实现Runnable接口

3、实现Callable接口

4、线程池

Q:Runnable、Callable有什么区别?

接口返回值。Callable接口的call方法支持返回值;

创建方式不一样。Runnable接口创建线程时是newThread(new XXRunnable).start() 发起调用;Callable需要借助FutureTask<返回值类型> ,传递callable实例,使用new Thread(futuretask).start()启动线程,使用future.get()获取返回值

Q:线程池相关知识,见Java高并发程序设计,线程池模块

锁常见问题:

Q:锁的分类

image.png

Q:synchorized的用法?

修饰代码段、修饰类、修饰方法

public void synchorized method() {   // 锁对象
    //....
}

public void method() {
   synchorized(ClassA.class) {  // 锁类
     //....
   }
}

public synchorized static void method() { // 锁类
  //.....
}

Q:JVM对synchorized的优化?

Q:volatile与JVM的关系

Q:volatile与synchorized 的区别

volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

volatile只能作用在变量上面,synchorized可以在变量,方法,类,代码段上

volatile只能保证变量的修改可见,不能保证原子性;synchorized可保证变量的修改可见和原子性

synchorized 不能直接修饰变量,但是可以加在get、set方法上;volatile可以修饰变量

Q:什么是内存屏障

Q:Lock、synchorized的区别

定义不同。 Lock是一个类,synchorized是java的关键字

阻塞方式不同。synchorized线程过来如果已经有线程抢到锁的话,其他的都会进入阻塞;lock不一定,不同的实现阻塞情况不同

锁释放方式。synchorized代码段执行完毕 / 抛出异常的时候释放锁;lock需要手动释放锁

锁状态。synchorized没有锁的状态;lock支持获取锁状态

锁类型。synchronized 是可重入、不可中断和非公平的锁;而 Lock 是可重入、可判断以及公平和非公平均可的锁。

Q:AQS和CAS的区别

CAS - 并发并交换。是cpu一条并发原语【原子指令】。CAS是一种无锁算法,主要有三个数,内存值V,旧的预期值A、要修改的值B,当且仅当内存值V与旧的预期值A一致时才将值修改为B

CAS的缺点:存在ABA问题。解决ABA问题方式,AtomicStampedReference,

AQS是什么?有什么用?在什么地方用?怎么实现的?

AQS - AbstractQueueSynchorizer,抽象队列同步器。java并发包的基础类,常说的ReentrantLock、ReentrantReadWriteLock底层是基于AQS实现的

AQS加锁原理。

AQS中公平锁和非公平锁。

8、ThreadLocal

Q:ThreadLocal是什么?

线程变量。包装的属性属于当前线程,该变量对其他线程而言是隔离的

Q:ThreadLocal的数据结构?平时怎么使用?

ThreadLocal内部有ThreadLocalMap,Map中key是ThreadLocal当前对象,value是Object用来存储变量

Q:有什么用?

存储当前线程的数据信息

Spring的事务中会用到这个实现

Q:ThreadLoacl的实现原理?

存放线程本地变量的,每个线程都有ThreadLocal,存放本地变量的时候,使用的是ThreadLocalMap【归属于Thread】的结构

Q;如何使用本地变量?

ThreadLocal.set(Value) // 设置线程变量值

ThreadLocal.get(Value) // 获取线程变量值

Q:Thread、ThreadLocal、ThreadLoaclMap 之间的关系

image.png Q:ThreadLocal会存在内存泄露的原因?

ThreadLocalMap存放的ThreadLocal,Value的Map中ThreadLocal是一个弱引用,如果ThreadLocal使用结束之后会被GC回收,但是Value是强引用,还会存在下面的引用线路

image.png

  
 public class ThreadLocal {
    public T get() {
         Thread t = Thread.currentThread();   // 获取当前线程
         ThreadLocalMap map = getMap(t);     // 获取当前线程的ThreadLocalMap结构
         if (map != null) {
             ThreadLocalMap.Entry e = map.getEntry(this);     // Entry结构中只有一个value,但是Entry是一个弱引用
             if (e != null) {
                 @SuppressWarnings("unchecked")
                 T result = (T)e.value;
                 return result;
             }
         }
         return setInitialValue();
     }
 }
 

 
 static class ThreadLocalMap {
   // ....
         static class Entry extends WeakReference<ThreadLocal<?>> {
             /** The value associated with this ThreadLocal. */
             Object value;
             Entry(ThreadLocal<?> k, Object v) {
                 super(k);
                 value = v;
             }
         }
   // ....
 }

Q:既然会发生内存泄露,为什么还要使用弱引用呢?

使用强引用的话,如果ThreadLocal对象被回收的话,Map还会持有它的强引用,因为ThreadLoaclMap的声明周期和Thread的一样长,如果都没有手动删除对应key,都会导致内存泄露,如果把Key设置为弱引用的话,Entry中key为null的会在下一次使用时清除掉,value也会被回收

9、Spring中事务使用功能ThreadLocal

Spring中事务传播机制是使用ThreadLocal实现的。

2、Java基础

1、Java基本数据类型

8种基本数据类型:4种整型,2种浮点型,1个bool,1个字符。byte、short、int、long、float、double、boolean、char

short:16位有符号的数据,范围是-32768 ~ 32767

2、类型转换方式

基本类型中,自动类型转换、强制类型转换

自动类型转换:小数值往大数值上转换;整形转浮点型;字符转整型

image.png

强制类型转换:

long b  = 123L;
double c = 2.33;

int a = (int) b;  // 强制类型转换
int d = (int) c;  // 丢弃小数,转为2

3、自动拆箱装箱

基本数据类型都有对应的包装类,两者进行转换时就会用到拆箱装箱

Integer a = 10;  // 自动装箱
int b = a;   // 自动拆箱

Q:如何实现的自动拆装箱?

装箱:包装类的valueOf()方法

拆箱:包装类对象的XXXValue()方法。例如intValue、longValue.......

Q:基本数据类型缓冲池

整型 Integer 对象通过使用相同的对象引用实现了缓存和重用,自动装箱Integer的时候, -128 至 127 之间的整型数字会直接使用缓存中的对象,而不是重新创建一个新对象

4、布尔类型占多少个字节

有多种说法:1bit、1byte、4byte

单boolean是4个字节,boolean数组是1个字节。因为在java虚拟机中,boolean值在编译之后都使用Java虚拟机中的int数据类型来代替。int是4个字节

5、抽象类和接口

抽象类:

a、被abstract修饰的类,抽象类中可以普通方法,也可以定义抽象方法,抽象方法使用abstract修饰;

b、使用关键子是extends

c、有构造函数

接口:

a、interface修饰,只能定义接口,一般是用来定义方法行为,接口中可以定义变量,变量默认是被public static final修饰;

b、使用关键字是implement

c、无构造函数

6、内部类

讲一个类的定义放在另一个类中就是内部类

Q:内部类都有哪些?

静态内部类、成员内部类、局部内部类、匿名内部类

public class InnerClass {
    public static int c = 123;

    static class InnerStaticClass {   // static修饰的内部类
        static int t = 12;

        void method() {
            System.out.println("InnerStaticClass_c:" + c);   // 静态内部类可访问外部类的static变量、方法
        }
    }
  
    class VariableClass {   // 成员内部类,与成员在同一层级的类
        int d = 5;
        int e = 6;

        void method() {
            System.out.println("VariableClass_d" + d);
        }
    }  
  
    public void method6() {
        class MethodClass {   // 外部类方法里定义的class是局部内部类
            int varName = 66;

            void method9() {
                System.out.println("MethodClass_varName:" + varName);  // 默认访问局部内部类的变量
                System.out.println("MethodClass_varName:" + this.varName);  // 访问局部内部类的变量
                System.out.println("MethodClass_varName:" + InnerClass.this.varName); // 访问外部类的变量【可访问外部类的所有非静态变量和方法】
            }
        }

        MethodClass methodClass = new MethodClass();   // 在外部类的方法里创建实例并使用
        methodClass.method9();
    }
  
    public void method10(final int i) {   // 匿名内部类使用的变量需要设置为final
        new AnonyInnerClassInterface() {    // 定义在方法里面,匿名内部类,使用场景最多,需要定义一个接口 / 抽象类
            @Override
            public String methodAnony() {
                for (int t = 0; t< i; t++) {
                    System.out.println("AnonyInnerClass_i:" + t);
                }
                return "finish";
            }
        };
    }
}

public interface InnerClassInterface {
  
}

public class JavaBaseMain {
    public static void main(String[] args) {
        InnerClass.InnerStaticClass innerClass = new InnerClass.InnerStaticClass();  // 创建静态内部类方法
        innerClass.method();
      
        InnerClass innerClass1 = new InnerClass();   // 成员内部类实例化方式;先创建外部类实例,使用外部类实例new 内部类
        InnerClass.VariableClass variableClass = innerClass1.new VariableClass();
    }
}


Q:内部类有哪些好处?

通过内部类可以实现多继承

内部类不会被同一个包下的类所见,封装性好

!!!匿名内部类可以方便定义回调方法

7、Java的引用类型

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

强引用:永远不会被回收♻️,即便抛出OutOfMemory

软引用:内存足够的时候GC时不会回收该引用,内存不够时GC时便会回收,回收了软引用内存仍然不够会抛出OutOfMemory

弱引用:只要发生GC,弱引用的对象就会被回收,java.lang.ref.WeakReference 表示弱引用

虚引用:随时随时可能被回收,PhantomReference 表示,对应的get方法永远返回null。一般虚引用会和 ReferenceQueue 引用队列一起使用

引用队列:垃圾回收器准备回收对象时,发现对象还有引用情况下就会把引用加入到引用队列中,做一些通用的操作

8、类初始化顺序

父类 - 静态变量、代码块

子类 - 静态变量、代码块

父类 - 变量、初始化器

父类 - 构造器

子类 - 变量、初始化器

子类 - 构造器

9、Java中修饰符

访问类修饰符:

public:所有类可见

protected:同一个包下类可见,不同包子类可见

private:本类可见

default:默认访问权限,只对同一个包可见,注意对不同的包的子类不可见。

public 》 protected》default》private

非访问类修饰符:

  • static:修饰类方法、类变量
  • final:修饰类、方法、变量。修饰类时代表类不可被继承;修饰变量时,代表变量不能被修改,同时需要显式初始化;修饰方法时子类不能重新定义该方法
  • synchorized:多线程同步时使用
  • abstract:抽象类、抽象方法
  • volatile:修饰的成员变量每次访问时都从共享内存取最新值;修改时也会强制刷新到内存中
  • transient:修饰的变量不会被序列化

10、Java中取整方式

  • 强制类型转换
  • Math.ceil(double num) // 向上取整;Math.floor(double num) // 向下取整;Math.round(double num) // 四舍五入
  • BigDecimal.setScale
  • String.format

11、switch支持哪些类型

byte、short、int、String【实际还是会将字符串hash之后的值进行判断】、枚举

12、面向对象的特性

封装

把一个对象的属性私有化,提供可以被外界访问的方法

继承

通过子类继承父类的方式,丰富子类的功能,父类可以提供许多子类相似的功能实现

多态

程序中一个引用变量指向的类实例在编程时不确定,需要在程序运行时间才能确定

Q:Java实现多态的方式

两种方式:继承父类、实现接口欧

方法重载是编译时多态性;方法重写是运行时多态性

Q:重写和重载的区别?

重写是子类中存在一个与父类入参、方法名、出参一模一样的方法,子类的实现内容与父类的不同

重载发生在一个类中或者子类与父类中。方法名一致,但是入参不一致,出参不限制,

重写发生在运行时期;重载发生在编译时期

13、Object有哪些公共方法

clone() 

finalize()   // 垃圾收集器确定回收改对象时执行该方法

notify() // 唤醒正在等待锁的方法

notifyAll()  // 唤醒所有正在等待锁的方法

wait()   // 当前线程进入等待,直到发起notify、notifyAll调用

hashCode()   // 对象的hashCode值

equals()   // 判断对象引用是否相等
  
toString()  // 返回对象的字符串表示形式

14、equals和==的区别

equals() 判断对象是否相等

== 判断两个变量的值是否相等

15、hashCode与equals的区别

两个对象相等,equals一定是true,hashCode值也一定相等

两个对象hashCode值一致,不一定相等

16、深拷贝、浅拷贝

浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,引用都指向原来旧对象的引用

深拷贝:被复制对象的所有变量都含有与原来的对象相同的值,引用指向不一致,但是指向的内容是拷贝了一份之后的内容

17、String s = new String("X")

String 对象的值都是常量池里面

创建了几个对象?一个或者两个,String s 是一个,"X" 执行到此处的时候如果已经有"X" 这个变量,则直接使用,没有的话需要新建一个常量对象

intern()  // 首先从常量池中查找是否存在该常量值的字符串,若不存在则先在常量池中创建,否则直接返回常量池已经存在的字符串的引用

18、String、StringBuffer、StringBuilder的区别

String是不可变对象。对String的拼接都会产生新的对象,性能比较差

StringBuffer中每个方法都被synchorized修饰,是线程安全的

StringBuilder 这个非线程安全

3、Java常用集合

集合是用来存储一批相同类型Java对象的数据结构,存放的是对象的引用

容器集合类:Collection 接口、Map 接口

Collection接口下包含:List、Set、Queue 结构

Map接口下包含:

image.png

1、数组和集合的区别?

  • 数组可以存储基本类型,集合只能存储包装类型/对象
  • 数组长度固定,集合长度不固定
  • 数据只能存储同一种类型下数据,集合可存储不同类型数据

!!! List、Set、Map 主要是这三种结构

各种结构常用的实现类

List:ArrayList、LinkedList、Vector

Set:HashSet、LinkedSet、TreeSet

Map:HashMap、LinkedHashMap、TreeMap、HashTable、ConcurrentHashMap

2、List

LinkedList是一个双向链表,可用作堆栈、队列、双端队列

Q:Vector和ArrayList的区别?

线程安全:ArrayList是非线程安全的;Vector是线程安全的,它的方法加了synchorized的修饰

初始容量,扩容机制:Vector默认初始容量是10,扩容时扩到原来的2倍;ArrayList默认初始容量是10,每次扩容扩大到原来的1.5倍

Q:如何删除ArrayList的数据?

不能使用for循环删除list.remove(size) ,只能通过Iterator 遍历列表执行iterator.remove()

Q:ArrayList和LinkedList的区别?

  • 都是线程不安全的

  • ArrayList底层使用的是Object[]数组存储元素;LinkedList底层使用的是双向链表数据结构

  • 查询效率不同:ArrayList查询效率高,基于下标查找数据;LinkedList查询效率低,线性查询,循环链表查

    增加删除效率不同:ArrayList增删效率低,增加删除数据移动的元素多;LinkedList增删效率高,只需断开指针指向,插入链接前后节点指针指向

    空间大小使用:ArrayList是指定空间大小,LinkedList没有长度限制

    ArrayList的插入的效率,由插入位置决定,

Q:平时工作中尽量减少链表结构的使用,原因?

链表的插入和删除只有在头尾节点才效率较高是O(1),其他的效率都是O(n)

双向链表、双向循环链表的区别?

Q:ArrayList、

3、什么是Fail-fast、Fail-safe

4、ArrayList的扩容机制

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);    // 每次扩大到原来的1.5倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)     // 最大是Integer的最大值:2^31-1
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

5、HashMap的存储结构

HashMap底层是数组 + 链表,1.8版本之后 又加了 红黑树结构

数组:哈希值的确定的路由位置

链表:解决哈希冲突

红黑树:提高哈希冲突后的查询效率

什么时候会转成红黑树?array.size > 64 && linked > 8

什么时候从红黑树转链表?链表长度< 6

Q:为什么要转成红黑树?

随机 hashCode 算法下所有桶中节点的分布频率会遵循泊松分布,作者还计算出相关概率,即当链表长度达到 8 个元素的概率为 0.00000006,几乎是不可能事件

Q:解决哈希冲突的几种方式?

链地址法、开放地址法、再哈希法

Q:红黑树、二叉树、平衡树之间的关系?

查询效率:二叉树 < 红黑树 < 平衡树【平衡度更好】

Q:HashMap的put方法

  public V put(K key, V value) {   
        return putVal(hash(key), key, value, false, true);   // 先计算key的hash值
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)   // 桶长度=0,执行resize初始化桶
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)   // 根据hash值计算出桶的下标
            tab[i] = newNode(hash, key, value, null);    // 桶中没有元素,直接new一个桶元素
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))  // 判断桶中元素与当前插入元素一致,直接替换桶中元素
                e = p;
            else if (p instanceof TreeNode)  // 当前元素是树形结构 -》直接往树中添加
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);  // 插入到链表尾部
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  链表元素>=7时,转换为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    // hash值的计算方式
    static final int hash(Object key) {
        int h;
        // hash 值是 hashCode 值的高 16 位和低 16 位的异或结果
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  // key 的 hashCode 值异或 key 的 hashCode 无符号右移 16 位
    }

Q:常用的hash函数?

除留余数法。``H(key) = key % p (p<=n),关键字除以一个不大于哈希表长度的正整数 p

直接定址法。直接根据key找到地址

数字分析法。根据key的某位数字定址

Q:为什么负载因子是0.75

中间值,太大的话,扩容时查找成本比较高

太小的话,还有很多空位的时候就发生了扩容,扩容成本高

Q:resize()扩容机制?

// todo 还没回答完全,图看不懂

初始化桶数组或对数组进行 2 倍的扩容

Q:HashMap是非线程安全的

如何确保线程安全?使用

HashTable:在操作方法上加了synchorized,锁的粒度是整个数组,粒度大

Collections.SynchorizedMap:传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现

ConcurrentHashMap :CAS + Synchorized 实现

Q:ConcurrentHashMap 实现线程安全的原理?

image.png

1.7中使用了分段锁,先定位到具体的 Segment,然后通过 ReentrantLock 去操作

1.8后使用了CAS+Synchorized加锁

image.png

Q:HashMap是否支持节点有序?

无序。有序的可以使用LinkedHashMap、TreeMap

4、Java虚拟机

2、垃圾回收详解

1、堆内存空间划分

Eden、SurviorFrom、SurviorTo

对象在Eden区出生,没经过一次MinorGC Age+1,当Age=15(默认值)的时候会移动到老年代,Age的值通过 -XX:MaxTenuringThreshold设置

  • 动态对象年龄判断 !!!

    HotSpot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的50%(XX:TargetSurvivorRatio=percent)时,取这个年龄和MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值

2、分代收集

  • 新生代收集:MinorGC。对新生代进行垃圾回收
  • 老年代收集:MajorGC。只对老年代进行垃圾收集
  • 整堆收集:FullGC。收集整个Java堆和方法区

3、空间分配担保

只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC

4、判断对象是否已经死亡?

引用计数法:对象中添加一个引用计数器,记录引用该对象的个数

可达性分析算法:通过一系列称为"GC Roots"的对象作为起点,向下搜索,节点走过的路径称为引用链,一个对象没有任何引用链相连的话,表示对象不可达

GC Roots 节点:

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 被同步锁持有的对象

5、不可达的对象一定会被回收吗?

可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法?对象没有覆盖finalize方法 || finalize 已经被虚拟机执行过? -> 没必要执行

6、引用的类型?

强引用:最普遍的引用。不会被回收,即使内存不够

软引用:内存空间不够,才会回收。软引用可以和引用队列联合使用,如果软引用的对象空间被回收,虚拟机会把软引用加入到与之关联的引用队列中

弱引用:只要发生GC就会被回收。弱引用也可以和引用队列联合使用,如果弱引用的对象空间被回收,虚拟机会把弱引用加入到与之关联的引用队列中

虚引用:始终返回的都是null。主要用来跟踪对象垃圾回收的活动。 虚引用必须和引用队列(ReferenceQueue)联合使用。

垃圾回收器回收一个虚引用的对象时,会把虚引用加入到与之关联的引用队列里,程序可以判断引用队列中是否已经加入了虚引用了,加入了的话可以在所引用的对象的内存被回收之前采取必要的行动

7、判断常量是否是废弃常量?

运行时常量池的存储:

  • 1.7之前,运行时常量池包含字符串常量池 整体存放在方法区。方法区是永久代,不会被垃圾回收
  • 1.7 时,字符串常量池被从方法区拿到了堆空间中,运行时常量池的其他内容还是在方法区
  • 1.8,移除了方法区用元空间(Metaspace)取代,字符串常量池在堆,运行时常量池的其他内容还是在元空间

8、方法区回收无用的类

判断类的无用?

  • 类的所有实例被回收
  • 加载类的ClassLoader 被回收
  • 类对应的java.lang.Class 对象没有任何引用,通过反射访问不到

3、垃圾收集算法

1、标记-清理算法

2、标记-复制算法

3、标记-整理算法

4、分代收集算法:根据对象存活周期不一样,内存划分为两部分:新生代、老年代。不同性质的区域管理方式不同。

新生代:对象存活率低。标记-复制算法

老年代:对象存活率高。标记-清理算法

4、垃圾收集器

收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

5、双亲委派模型

Q:双亲委派模型的好处?

避免类的重复加载。判断不同类是 <类文件 + 加载器> ;同时也保证了Java的核心API不被篡写

Q:如何自定义加载器?

需要继承 CalssLoader ,重写类中的findClass()方法,无法被父类加载器加载的类最终会通过这个方法被加载

image.png

6、类加载过程

// todo 总是记不住

7、各个版本Java的优化

Java 8:

  • lambda表达式;Optional、Stream

Java 9:

  • 使用了G1垃圾收集器
  • String 存储结构优化。之前使用的是char[] 存储;现在使用的是byte[]数组存储

Java 10:

  • G1并行Full GC

Java 11:

  • ZGC垃圾收集器。目前还在探索和实践
  • String的功能增强,方法更多