Java八股文总结大全(新版)

1、为什么要使用线程池    难度系数:⭐
  1. 线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最 大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
  2. 主要特点:线程复用;控制最大并发数:管理线程。

第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进 行统一的分配,调优和监控

2、线程池底层工作原理    难度系数:⭐

  1. 第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程
  2. 第二步:调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务
  3. 第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存
  4. 第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务
  5. 第五步:如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常
3、ThreadPoolExecutor对象有哪些参数 怎么设定核心线程数和最大线程数 拒绝策略有哪些    难度系数:⭐

参数与作用:共7个参数

  1. corePoolSize:核心线程数,

在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false),当allowCoreThreadTimeOut为false时,核心线程会一直存活,哪怕是一直空闲着。而当allowCoreThreadTimeOut为true时核心线程空闲时间超过keepAliveTime时会被回收。

  1. maximumPoolSize:最大线程数

线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为LinkedBlockingDeque时,这个值将无效。

  1. keepAliveTime:存活时间,

当非核心空闲超过这个时间将被回收,同时空闲核心线程是否回收受allowCoreThreadTimeOut影响。

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

  1. unit:keepAliveTime的单位。
  2. workQueue:任务队列

常用有三种队列,即SynchronousQueue,LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。

  1. threadFactory:线程工厂,

ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。

  1. RejectedExecutionHandler:拒绝策略

也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution法。默认是抛出一个运行时异常。

线程池大小设置:

  1. 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
  2. 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系

如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1 如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。

一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/
线程 CPU 时间 )* CPU 数目

这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner测试大量运行次数求出平均值)

拒绝策略:

  1. AbortPolicy:直接抛出异常,默认策略;
  2. CallerRunsPolicy:用调用者所在的线程来执行任务;
  3. DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  4. DiscardPolicy:直接丢弃任务;当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
4、常见线程安全的并发容器有哪些    难度系数:⭐
  1. CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap
  2. CopyOnWriteArrayList、CopyOnWriteArraySet采用写时复制实现线程安全
  3. ConcurrentHashMap采用分段锁的方式实现线程安全
5、Atomic原子类了解多少 原理是什么    难度系数:⭐

Java 的原子类都存放在并发包 java.util.concurrent.atomic下,如下图:

基本类型

  • 使用原子的方式更新基本类型
  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型

  • 使用原子的方式更新数组里的某个元素
  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新引用类型里的字段原子类
  • AtomicMarkableReference :原子更新带有标记位的引用类型
  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,以及解决使用 CAS 进行原子更新时可能出现的 ABA 问题
  1. AtomicInteger 类利用 CAS (Compare and Swap) + volatile + native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
  2. CAS 的原理,是拿期望值和原本的值作比较,如果相同,则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是个本地方法,这个方法是用来拿“原值”的内存地址,返回值是 valueOffset;另外,value 是一个 volatile 变量,因此 JVM 总是可以保证任意时刻的任何线程总能拿到该变量的最新值。
6、synchronized底层实现是什么 lock底层是什么 有什么区别    难度系数:⭐⭐⭐

Synchronized原理:

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

 

Lock原理:

  1. Lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
  2. Lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
  3. Lock释放锁的过程:修改状态值,调整等待链表。
  4. Lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。

Lock与synchronized的区别:

  1. Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的
  2. 当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的     条件下提供一种退出的机制。
  3. 一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronize只有一路条件队列;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以    及设置等待时限等方式退出条件队列。
  4. synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式

synchronized
 

Lock
 

关键字
 


 

自动加锁和释放锁
 

需要手动调用unlock方法释放锁
 

jvm层面的锁
 

API层面的锁
 

非公平锁
 

可以选择公平或者非公平锁
 

锁是一个对象,并且锁的信息保存在了对象中
 

代码中通过int类型的state标识
 

有一个锁升级的过程
 


 

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

7、了解ConcurrentHashMap吗 为什么性能比HashTable高,说下原理    难度系数:⭐⭐

ConcurrentHashMap是线程安全的Map容器,JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全,JKD8的版本取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率。

hashtable类基本上所有的方法都是采用synchronized进行线程安全控制,高并发情况下效率就降低 ,ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化

8、ConcurrentHashMap底层原理    难度系数:⭐⭐⭐
  1. Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。

     
 
  1. public V put(K key, V value) {

  2.     Segment<K,V> s;

  3.     if (value == null)

  4.         throw new NullPointerException();

  5.     int hash = hash(key);

  6.     // hash 值无符号右移 28位(初始化时获得),然后与 segmentMask=15 做与运算

  7.     // 其实也就是把高4位与segmentMask(1111)做与运算

  8.   // this.segmentMask = ssize - 1;

  9.    //对hash值进行右移segmentShift位,计算元素对应segment中数组下表的位置

  10.    //把hash右移segmentShift,相当于只要hash值的高32-segmentShift位,右移的目的是保留了hash值的高位。然后和segmentMask与操作计算元素在segment数组中的下表

  11.     int j = (hash >>> segmentShift) & segmentMask;

  12.    //使用unsafe对象获取数组中第j个位置的值,后面加上的是偏移量

  13.     if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck

  14.          (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment

  15.         // 如果查找到的 Segment 为空,初始化

  16.         s = ensureSegment(j);

  17.    //插入segment对象

  18.     return s.put(key, hash, value, false);

  19. }

  20. /**

  21.  * Returns the segment for the given index, creating it and

  22.  * recording in segment table (via CAS) if not already present.

  23.  *

  24.  * @param k the index

  25.  * @return the segment

  26.  */

  27. @SuppressWarnings("unchecked")

  28. private Segment<K,V> ensureSegment(int k) {

  29.     final Segment<K,V>[] ss = this.segments;

  30.     long u = (k << SSHIFT) + SBASE; // raw offset

  31.     Segment<K,V> seg;

  32.     // 判断 u 位置的 Segment 是否为null

  33.     if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {

  34.         Segment<K,V> proto = ss[0]; // use segment 0 as prototype

  35.         // 获取0号 segment 里的 HashEntry<K,V> 初始化长度

  36.         int cap = proto.table.length;

  37.         // 获取0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的

  38.         float lf = proto.loadFactor;

  39.         // 计算扩容阀值

  40.         int threshold = (int)(cap * lf);

  41.         // 创建一个 cap 容量的 HashEntry 数组

  42.         HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];

  43.         if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck

  44.             // 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作

  45.             Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);

  46.             // 自旋检查 u 位置的 Segment 是否为null

  47.             while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))

  48.                    == null) {

  49.                 // 使用CAS 赋值,只会成功一次

  50.                 if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))

  51.                     break;

  52.             }

  53.         }

  54.     }

  55.     return seg;

  56. }

  57. final V put(K key, int hash, V value, boolean onlyIfAbsent) {

  58.     // 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。

  59.     HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);

  60.     V oldValue;

  61.     try {

  62.         HashEntry<K,V>[] tab = table;

  63.         // 计算要put的数据位置

  64.         int index = (tab.length - 1) & hash;

  65.         // CAS 获取 index 坐标的值

  66.         HashEntry<K,V> first = entryAt(tab, index);

  67.         for (HashEntry<K,V> e = first;;) {

  68.             if (e != null) {

  69.                 // 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value

  70.                 K k;

  71.                 if ((k = e.key) == key ||

  72.                     (e.hash == hash && key.equals(k))) {

  73.                     oldValue = e.value;

  74.                     if (!onlyIfAbsent) {

  75.                         e.value = value;

  76.                         ++modCount;

  77.                     }

  78.                     break;

  79.                 }

  80.                 e = e.next;

  81.             }

  82.             else {

  83.                 // first 有值没说明 index 位置已经有值了,有冲突,链表头插法。

  84.                 if (node != null)

  85.                     node.setNext(first);

  86.                 else

  87.                     node = new HashEntry<K,V>(hash, key, value, first);

  88.                 int c = count + 1;

  89.                 // 容量大于扩容阀值,小于最大容量,进行扩容

  90.                 if (c > threshold && tab.length < MAXIMUM_CAPACITY)

  91.                     rehash(node);

  92.                 else

  93.                     // index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头

  94.                     setEntryAt(tab, index, node);

  95.                 ++modCount;

  96.                 count = c;

  97.                 oldValue = null;

  98.                 break;

  99.             }

  100.         }

  101.     } finally {

  102.         unlock();

  103.     }

  104.     return oldValue;

  105. }

Java

  1. Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

  1. public V put(K key, V value) {

  2.     return putVal(key, value, false);

  3. }

  4. /** Implementation for put and putIfAbsent */

  5. final V putVal(K key, V value, boolean onlyIfAbsent) {

  6.     // key 和 value 不能为空

  7.     if (key == null || value == null) throw new NullPointerException();

  8.     int hash = spread(key.hashCode());

  9.     int binCount = 0;

  10.     for (Node<K,V>[] tab = table;;) {

  11.         // f = 目标位置元素

  12.         Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值

  13.         if (tab == null || (n = tab.length) == 0)

  14.             // 数组桶为空,初始化数组桶(自旋+CAS)

  15.             tab = initTable();

  16.         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

  17.             // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出

  18.             if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))

  19.                 break;  // no lock when adding to empty bin

  20.         }

  21.         else if ((fh = f.hash) == MOVED)

  22.             tab = helpTransfer(tab, f);

  23.         else {

  24.             V oldVal = null;

  25.             // 使用 synchronized 加锁加入节点

  26.             synchronized (f) {

  27.                 if (tabAt(tab, i) == f) {

  28.                     // 说明是链表

  29.                     if (fh >= 0) {

  30.                         binCount = 1;

  31.                         // 循环加入新的或者覆盖节点

  32.                         for (Node<K,V> e = f;; ++binCount) {

  33.                             K ek;

  34.                             if (e.hash == hash &&

  35.                                 ((ek = e.key) == key ||

  36.                                  (ek != null && key.equals(ek)))) {

  37.                                 oldVal = e.val;

  38.                                 if (!onlyIfAbsent)

  39.                                     e.val = value;

  40.                                 break;

  41.                             }

  42.                             Node<K,V> pred = e;

  43.                             if ((e = e.next) == null) {

  44.                                 pred.next = new Node<K,V>(hash, key,

  45.                                                           value, null);

  46.                                 break;

  47.                             }

  48.                         }

  49.                     }

  50.                     else if (f instanceof TreeBin) {

  51.                         // 红黑树

  52.                         Node<K,V> p;

  53.                         binCount = 2;

  54.                         if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,

  55.                                                        value)) != null) {

  56.                             oldVal = p.val;

  57.                             if (!onlyIfAbsent)

  58.                                 p.val = value;

  59.                         }

  60.                     }

  61.                 }

  62.             }

  63.             if (binCount != 0) {

  64.                 if (binCount >= TREEIFY_THRESHOLD)

  65.                     treeifyBin(tab, i);

  66.                 if (oldVal != null)

  67.                     return oldVal;

  68.                 break;

  69.             }

  70.         }

  71.     }

  72.     addCount(1L, binCount);

  73.     return null;

  74. }

Java

9、了解volatile关键字不    难度系数:⭐
  1. volatile是Java提供的最轻量级的同步机制,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。
  2. volatile禁止了指令重排,可以保证程序执行的有序性,但是由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱
10、synchronized和volatile有什么区别    难度系数:⭐⭐
  1. volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能用在变量级别,而synchronized可以使用在变量、方法、类级别。
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
  4. volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。
  5. volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。
11、Java类加载过程    难度系数:⭐
  1. 加载 加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:

通过一个类的全限定名获取该类的二进制流。

将该二进制流中的静态存储结构转化为方法去运行时数据结构。 

在内存中生成该类的Class对象,作为该类的数据访问入口。

  1. 验证 验证的目的是为了确保Class文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:

文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。

符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

  1. 准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

  1. 解析

该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

  1. 初始化

初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

12、什么是类加载器,类加载器有哪些  难度系数:⭐

类加载器就是把类文件加载到虚拟机中,也就是说通过一个类的全限定名来获取描述该类的二进制字节流。

  1. 主要有以下四种类加载器

启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用

扩展类加载器(extension class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类

系统类加载器(system class loader)也叫应用类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它

用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现

  1. 什么时候会使用到加载器?java中的加载器是按需加载,什么时候用到,什么时候加载
    • new对象的时候
    • 访问某个类或者接口的静态变量,或者对该静态变量赋值时
    • 调用类的静态方法时
    • 反射
    • 初始化一个类的子类时,其父类首先会被加载
    • JVM启动时标明的启动类,也就是文件名和类名相同的那个类
13、简述java内存分配与回收策略以及Minor GC和Major GC(full GC)     难度系数:⭐⭐
  1. 内存分配

栈区:栈分为java虚拟机栈和本地方法栈

堆区:堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。堆区是gc的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区,主要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1。

方法区:被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)

程序计数器:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。线程私有的。

  1. 回收策略以及Minor GC和Major GC
    • 对象优先在堆的Eden区分配
    • 大对象直接进入老年代
    • 长期存活的对象将直接进入老年代

当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC.Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度。

14、如何查看java死锁     难度系数:⭐
 
  1. ####演示死锁

  2. package com.ssg.mst;

  3. public class 死锁 {

  4.     private static final String lock1 = "lock1";

  5.     private static final String lock2 = "lock2";

  6.     public static void main(String[] args) {

  7.         Thread thread1 = new Thread(() -> {

  8.             while (true) {

  9.                 synchronized (lock1) {

  10.                     try {

  11.                         System.out.println(Thread.currentThread().getName() + lock1);

  12.                         Thread.sleep(1000);

  13.                         synchronized (lock2){

  14.                             System.out.println(Thread.currentThread().getName() + lock2);

  15.                         }

  16.                     } catch (InterruptedException e) {

  17.                         throw new RuntimeException(e);

  18.                     }

  19.                 }

  20.             }

  21.         });

  22.         Thread thread2 = new Thread(() -> {

  23.             while (true) {

  24.                 synchronized (lock2) {

  25.                     try {

  26.                         System.out.println(Thread.currentThread().getName() + lock2);

  27.                         Thread.sleep(1000);

  28.                         synchronized (lock1){

  29.                             System.out.println(Thread.currentThread().getName() + lock1);

  30.                         }

  31.                     } catch (InterruptedException e) {

  32.                         throw new RuntimeException(e);

  33.                     }

  34.                 }

  35.             }

  36.         });

  37.         thread1.start();

  38.         thread2.start();

  39.     }

  40. }

Java

死锁代码演示

  1. 程序运行,进程没有停止。

  1. 通过jps查看java进程,找到没有停止的进程

  1. 通过jstack 9060 查看进程具体执行信息

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

21、Java死锁如何避免     难度系数:⭐

造成死锁的几个原因

1.一个资源每次只能被一个线程使用

2.一个线程在阻塞等待某个资源时,不释放已占有资源

3.一个线程已经获得的资源,在未使用完之前,不能被强行剥夺

4.若干线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中

1.要注意加锁顺序,保证每个线程按同样的顺序进行加锁

2.要注意加锁时限,可以针对锁设置一个超时时间

3.要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决

第三章-java框架篇
1、简单的谈一下SpringMVC的工作流程    难度系数:⭐
  • 用户发送请求至前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping处理器映射器。
  • 处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
  • DispatcherServlet调用HandlerAdapter处理器适配器
  • HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
  • Controller执行完成返回ModelAndView
  • HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
  • DispatcherServlet将ModelAndView传给ViewReslover视图解析器
  • ViewReslover解析后返回具体View
  • DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
  • DispatcherServlet响应用户
2、说出Spring或者SpringMVC中常用的5个注解    难度系数:⭐
  1. @Component  基本注解,标识一个受Spring管理的组件
  2. @Controller    标识为一个表示层的组件
  3. @Service       标识为一个业务层的组件
  4. @Repository    标识为一个持久层的组件
  5. @Autowired     自动装配
  6. @Qualifier("")    具体指定要装配的组件的id值
  7. @RequestMapping()  完成请求映射
  8. @PathVariable    映射请求URL中占位符到请求处理方法的形参

只要说出几个注解并解释含义即可,如上答案只做参考

3、简述SpringMVC中如何返回JSON数据    难度系数:⭐

Step1:在项目中加入json转换的依赖,例如jackson,fastjson,gson等

Step2:在请求处理方法中将返回值改为具体返回的数据的类型, 例如数据的集合类List<Employee>等

Step3:在请求处理方法上使用@ResponseBody注解

4、谈谈你对Spring的理解    难度系数:⭐

Spring 是一个开源框架,为简化企业级应用开发而生。Spring 可以是使简单的JavaBean 实现以前只有EJB 才能实现的功能。Spring 是一个 IOC 和 AOP 容器框架。

Spring 容器的主要核心是:

控制反转(IOC),传统的 java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。而在 spring 开发模式中,spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用spring 提供的对象就可以了,这是控制反转的思想。

依赖注入(DI),spring 使用 javaBean 对象的 set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。

面向切面编程(AOP),在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。AOP 底层是动态代理,如果是接口采用 JDK 动态代理,如果是类采用CGLIB 方式实现动态代理。

5、Spring中常用的设计模式    难度系数:⭐
  1. 代理模式——spring 中两种代理方式,若目标对象实现了若干接口,spring 使用jdk 的java.lang.reflect.Proxy类代理。若目标兑现没有实现任何接口,spring 使用 CGLIB 库生成目标类的子类。
  2. 单例模式——在 spring 的配置文件中设置 bean 默认为单例模式。
  3. 模板方式模式——用来解决代码重复的问题。

比如:RestTemplate、JmsTemplate、JpaTemplate

  1. 工厂模式——在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象。Spring 中使用 beanFactory 来创建对象的实例。
6、Spring循环依赖问题    难度系数:⭐⭐
常见问法

请解释一下spring中的三级缓存

三级缓存分别是什么?三个Map有什么异同?

什么是循环依赖?请你谈谈?看过spring源码吗?

如何检测是否存在循环依赖?实际开发中见过循环依赖的异常吗?

多例的情况下,循环依赖问题为什么无法解决?

什么是循环依赖?

两种注入方式对循环依赖的影响?

官方解释

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependency-resolution

相关概念

实例化:堆内存中申请空间

初始化:对象属性赋值

三级缓存

名称

对象名

含义

一级缓存

singletonObjects

存放已经经历了完整生命周期的Bean对象

二级缓存
 

earlySingletonObjects
 

存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完)

三级缓存

singletonFactories

存放可以生成Bean的工厂

四个关键方法

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

package org.springframework.beans.factory.support;

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

    /**

    单例对象的缓存:bean名称—bean实例,即:所谓的单例池。

    表示已经经历了完整生命周期的Bean对象

    第一级缓存

    */

    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    /**

    早期的单例对象的高速缓存: bean名称—bean实例。

    表示 Bean的生命周期还没走完(Bean的属性还未填充)就把这个 Bean存入该缓存中也就是实例化但未初始化的 bean放入该缓存里

    第二级缓存

    */

    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

    /**

    单例工厂的高速缓存:bean名称—ObjectFactory

    表示存放生成 bean的工厂

    第三级缓存

    */

    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

}

debug源代码过程

需要22个断点(可选)

1,A创建过程中需要B,于是A将自己放到三级缓里面,去实例化B

2,B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A

3,B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)

然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。

总结

1,Spring创建 bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化。

2,每次创建 bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个。

3,当创建 A的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了B,接着就又去创建B,同样的流程,创建完B填充属性时又发现它依赖了A又是同样的流程,不同的是:这时候可以在三级缓存中查到刚放进去的原始对象A。

所以不需要继续创建,用它注入 B,完成 B的创建既然 B创建好了,所以 A就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成

Spring解决循环依赖依靠的是Bean的"中间态"这个概念,而这个中间态指的是已经实例化但还没初始化的状态—>半成品。实例化的过程又是通过构造器创建的,如果A还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决

其他衍生问题

问题1:为什么构造器注入属性无法解决循环依赖问题?

       由于spring中的bean的创建过程为先实例化 再初始化(在进行对象实例化的过程中不必赋值)将实例化好的对象暴露出去,供其他对象调用,然而使用构造器注入,必须要使用构造器完成对象的初始化的操作,就会陷入死循环的状态

问题2:一级缓存能不能解决循环依赖问题? 不能

       在三个级别的缓存中存储的对象是有区别的 一级缓存为完全实例化且初始化的对象 二级缓存实例化但未初始化对象 如果只有一级缓存,如果是并发操作下,就有可能取到实例化但未初始化的对象,就会出现问题

问题3:二级缓存能不能解决循环依赖问题?

      理论上二级缓存可以解决循环依赖问题,但是需要注意,为什么需要在三级缓存中存储匿名内部类(ObjectFactory),原因在于 需要创建代理对象  eg:现有A类,需要生成代理对象 A是否需要进行实例化(需要) 在三级缓存中存放的是生成具体对象的一个匿名内部类,该类可能是代理类也可能是普通的对象,而使用三级缓存可以保证无论是否需要是代理对象,都可以保证使用的是同一个对象,而不会出现,一会儿使用普通bean 一会儿使用代理类

7、介绍一下Spring bean 的生命周期、注入方式和作用域    难度系数:⭐

Bean的生命周期

(1)默认情况下,IOC容器中bean的生命周期分为五个阶段:

  • 调用构造器 或者是通过工厂的方式创建Bean对象
  • 给bean对象的属性注入值
  • 调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的.
  • 使用
  • IOC容器关闭时, 销毁Bean对象.

(2)当加入了Bean的后置处理器后,IOC容器中bean的生命周期分为七个阶段:

  • 调用构造器 或者是通过工厂的方式创建Bean对象
  • 给bean对象的属性注入值
  • 执行Bean后置处理器中的 postProcessBeforeInitialization
  • 调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的.x
  • 执行Bean的后置处理器中 postProcessAfterInitialization  
  • 使用
  • IOC容器关闭时, 销毁Bean对象

只需要回答出第一点即可,第二点也回答可适当 加分。

注入方式:

通过 setter 方法注入

通过构造方法注入

Bean的作用域

总共有四种作用域:

  • Singleton  单例的
  • Prototype  原型的
  • Request
  • Session
8、请描述一下Spring 的事务管理    难度系数:⭐

(1)声明式事务管理的定义:用在 Spring 配置文件中声明式的处理事务来代替代码式的处理事务。这样的好处是,事务管理不侵入开发的组件,具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可,这样维护起来极其方便。

基于 TransactionInterceptor  的声明式事务管理:两个次要的属性: transactionManager,用来指定一个事务治理器, 并将具体事务相关的操作请托给它; 其他一个是 Properties 类型的transactionAttributes 属性,该属性的每一个键值对中,键指定的是方法名,方法名可以行使通配符, 而值就是表现呼应方法的所运用的事务属性。

(2)基于 @Transactional 的声明式事务管理:Spring 2.x 还引入了基于 Annotation 的体式格式,具体次要触及@Transactional 标注。@Transactional 可以浸染于接口、接口方法、类和类方法上。算作用于类上时,该类的一切public 方法将都具有该类型的事务属性。

(3)编程式事物管理的定义:在代码中显式挪用 beginTransaction()、commit()、rollback()等事务治理相关的方法, 这就是编程式事务管理。Spring 对事物的编程式管理有基于底层 API 的编程式管理和基于 TransactionTemplate 的编程式事务管理两种方式。

9、MyBatis中 #{}和${}的区别是什么    难度系数:⭐

#{}是预编译处理,${}是字符串替换;

Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;

Mybatis在处理${}时,就是把${}替换成变量的值;

使用#{}可以有效的防止SQL注入,提高系统安全性。

10、Mybatis 中一级缓存与二级缓存    难度系数:⭐
  1. MyBatis的缓存分为一级缓存和 二级缓存。

一级缓存是SqlSession级别的缓存,默认开启。

二级缓存是NameSpace级别(Mapper)的缓存,多个SqlSession可以共享,使用时需要进行配置开启。

  1. 缓存的查找顺序:二级缓存 => 一级缓存 => 数据库
11、MyBatis如何获取自动生成的(主)键值    难度系数:⭐

在<insert>标签中使用 useGeneratedKeys和keyProperty 两个属性来获取自动生成的主键值。

示例:

<insert id=”insertname” usegeneratedkeys=”true” keyproperty=”id”>
    insert into names (name) values (#{name}) 
</insert>

Java

12、简述Mybatis的动态SQL,列出常用的6个标签及作用    难度系数:⭐

动态SQL是MyBatis的强大特性之一 基于功能强大的OGNL表达式。

动态SQL主要是来解决查询条件不确定的情况,在程序运行期间,根据提交的条件动态的完成查询

常用的标签:

<if> : 进行条件的判断

<where>:在<if>判断后的SQL语句前面添加WHERE关键字,并处理SQL语句开始位置的AND 或者OR的问题

<trim>:可以在SQL语句前后进行添加指定字符 或者去掉指定字符.

<set>:  主要用于修改操作时出现的逗号问题

<choose> <when> <otherwise>:类似于java中的switch语句.在所有的条件中选择其一

<foreach>:迭代操作

13、Mybatis 如何完成MySQL的批量操作    难度系数:⭐

MyBatis完成MySQL的批量操作主要是通过<foreach>标签来拼装相应的SQL语句

例如:

<insert** id="insertBatch" >
    insert into tbl_employee(last_name,email,gender,d_id) values 
   <foreach** collection="emps" item="curr_emp" separator=","**>
      (#{curr_emp.lastName},#{curr_emp.email},#{curr_emp.gender},#{curr_emp.dept.id}) 
   </foreach>
</insert>

Java

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

14、谈谈怎么理解SpringBoot框架    难度系数:⭐⭐

Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。

Spring Boot的优点

  • 独立运行

Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。

  • 简化配置

spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。除此之外,还提供了各种启动器,开发者能快速上手。

  • 自动配置

Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置。

  • 无代码生成和XML配置

Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是Spring4.x的核心功能之一。

  • 应用监控

Spring Boot提供一系列端点可以监控服务及应用,做健康检测。

Spring Boot缺点:

Spring Boot虽然上手很容易,但如果你不了解其核心技术及流程,所以一旦遇到问题就很棘手,而且现在的解决方案也不是很多,需要一个完善的过程。

15、Spring Boot 的核心注解是哪个 它主要由哪几个注解组成的    难度系数:⭐

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

  • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,
    • 如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
  • @ComponentScan:Spring组件扫描。
16、Spring Boot自动配置原理是什么    难度系数:⭐

注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,

首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。

@EnableAutoConfiguration是实现自动配置的注解

@Configuration表示这是一个配置文件

具体参考文档:

Spring Boot自动配置原理、实战

17、SpringBoot配置文件有哪些 怎么实现多环境配置    难度系数:⭐

Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。

application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。

bootstrap配置文件的特性:

  • bootstrap 由父 ApplicationContext 加载,比 applicaton 优先加载
  • bootstrap 里面的属性不能被覆盖

bootstrap 配置文件有以下几个应用场景:

  • 使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
  • 一些固定的不能被覆盖的属性;
  • 一些加密/解密的场景;

提供多套配置文件,如:

applcation.properties
application-dev.properties
application-test.properties
application-prod.properties

运行时指定具体的配置文件,具体请看这篇文章《Spring Boot Profile 不同环境配置》。

18、SpringBoot和SpringCloud是什么关系    难度系数:⭐

Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的开发工具;Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架; Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,必须基于Spring Boot开发。

 可以单独使用Spring Boot开发项目,但是Spring Cloud离不开 Spring Boot。

19、SpringCloud都用过哪些组件 介绍一下作用    难度系数:⭐
  1. Nacos--作为注册中心和配置中心,实现服务注册发现和服务健康监测及配置信息统一管理
  2. Gateway--作为网关,作为分布式系统统一的出入口,进行服务路由,统一鉴权等
  3. OpenFeign--作为远程调用的客户端,实现服务之间的远程调用
  4. Sentinel--实现系统的熔断限流
  5. Sleuth--实现服务的链路追踪
20、Nacos作用以及注册中心的原理    难度系数:⭐⭐

Nacos英文全称Dynamic Naming and Configuration Service,Na为naming/nameServer即注册中心,co为configuration即注册中心,service是指该注册/配置中心都是以服务为核心。

Nacos注册中心分为server与client,server采用Java编写,为client提供注册发现服务与配置服务。而client可以用多语言实现,client与微服务嵌套在一起,nacos提供sdk和openApi,如果没有sdk也可以根据openApi手动写服务注册与发现和配置拉取的逻辑。

服务注册原理

服务注册方法:以Java nacos client v1.0.1 为例子,服务注册的策略的是每5秒向nacos server发送一次心跳,心跳带上了服务名,服务ip,服务端口等信息。同时 nacos server也会向client 主动发起健康检查,支持tcp/http检查。如果15秒内无心跳且健康检查失败则认为实例不健康,如果30秒内健康检查失败则剔除实例。

21、Feign工作原理    难度系数:⭐⭐

主程序入口添加了@EnableFeignClients注解开启对FeignClient扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。当程序启动时,会进行包扫描,扫描所有@FeignClient的注解的类,并且讲这些信息注入Spring IOC容器中,当定义的的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate.当生成代理时,Feign会为每个接口方法创建一个RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装HTTP请求需要的全部信息,如请求参数名,请求方法等信息都是在这个过程中确定的。然后RequestTemplate生成Request,然后把Request交给Client去处理,这里指的时Client可以时JDK原生的URLConnection,Apache的HttpClient,也可以时OKhttp,最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发器服务之间的调用。

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

  • 18
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值