Java常问面试题总结

             24年3月份由于公司经营不善濒临倒闭,半死不活的状态,我忍痛于公司离职;互联网行业一片寒冬传言,众多企业倒闭,裁员。本以为随着疫情、春季和金融楼市的回暖,一切都会变好。然而,站在这个应该是光明的时刻,举世瞩目的景象却显得毫无生气。令人失望的是,我们盼望已久的春天似乎仍未到来。

           历经近一个月求职,终于斩获满意offer,在这里总结一下被问到的高频问题,希望可以给大家求职中提供一些帮助,

jVM

jvm类加载器:

1、Bootstrap ClassLoader :称之为启动类加载器,是最顶层的类加载器,负责加载JDK中的核心类库,如 rt.jar、resources.jar、charsets.jar等

2、Extension ClassLoader:称之为扩展类加载器,负责加载Java的扩展类库,默认加载$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。

3、App ClassLoader:称之为系统类加载器,负责加载应用程序classpath目录下所有jar和class文件。

4.自定义类加载器:须继承自 java.lang.ClassLoader

jvm构成:
方法区:线程共享的内存区域;方法区用于存储JVM加载完成的类型信息、常量、静态变量、即时编译器编译后的代码缓存。
堆: 先进先出 负责存放对象实例,当Java创建一个类的实例对象或者数组时,都会在堆中为新的对象分配内存
虚拟机栈:保存基础数据类型 和对象的引用 (注意只是对象的引用而不是对象本身哦

每个线程都会建立一个栈,每个栈又包含了若干个栈帧,每个栈帧对应着每个方法的每次调用,栈帧包含了三个部分:

局部变量区(方法内基本类型变量、对象实例的引用)

操作数栈区(存放方法执行过程中产生的中间结果)

运行环境区(动态连接、正确的方法返回相关信息、异常捕捉)

本地方法栈

区别在于虚拟机栈执行的是Java方法,本地方法栈执行的是本地(Native)方法服务,存储的也是本地方法的局部变量表,本地方法的操作数栈等信息

本地方法都不是使用Java语言编写的,它们可能由C或其他语言编写,本地方法也不由JVM去运行,所以本地方法的运行不受JVM管理。

程序计数器

在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了各条线程之间的切换后计数器能恢复到正确的执行位置,所以每条线程都会有一个独立的程序计数器

程序计数器仅占很小的一块内存空间。

当线程正在执行一个Java方法,程序计数器记录的是正在执行的JVM字节码指令的地址。如果正在执行的是一个Natvie(本地方法),那么这个计数器的值则为空(Underfined)。

GC 相关的主要是思考这3件事情。

1哪些内存需要回收?2什么时候回收?3如何回收?

  1. 哪些内存需要回收? jvm 包括的运行时数据区(线程共享(方法区,堆),虚拟机栈,本地方法栈,程序计数器 )。运行时才知道会创建哪些对象,内存分配/回收是动态的 (线程共享 )

对于如何判断对象是否可以回收,有两种比较经典的判断策略。引用计数算法: 对象引用一次 计数器加1 不能解决循环引用的问题 可达性分析算法: 从GC Roots 为起点开始向下搜索,搜索所走过的路径称为引用链 引用链为0 可回收

GC root 对象是指:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

  • 方法区中类静态属性引用的对象

  • 方法区中常量引用的对象

java对引用的概念进行了扩充,将引用分为 强引用 (垃圾收集器永远不会回收被引用的对象 除非设置为null) > 软引用()>弱引用 > 虚引用软引用(可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象) 弱引用(弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。)虚引用(它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响 唯一目的就是能在这个对象被收集器回收时收到一个系统通知)

有了判断对象是否存活的标准之后,如何GC的相关算法:

标记-清除(Mark-Sweep)算法 标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片分代收集算法的这种思想新生代:复制算法 按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。老年代: 标记-整理算法 :标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。 进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题

垃圾回收器:

jdk8采用的是: Parallel Scavenge + Parallelo Old

Serial

在JDK1.3之前是Java虚拟机新生代收集器的唯一选择,这是一个单线程工作的收集器。在进行垃圾回收的时候,需要暂停所有的用户线程,直到回收结束

它依然是HotSpot虚拟机运行在客户端模式下,或者4核4GB以下服务端的默认新生代收集器,这种核心数和内存空间较小的场景下,它单线程的优势就体现出来了,没有线程交互的开销,加上内存空间不大,单次回收耗时几十毫秒,这点停顿时间,完全是可以接受的

Serial 负责收集新生代区域,它采用标记-复制算法。

Serial Old

SerialOld 是 Serial 收集器的老年代版本,和 Serial 一样,它也是单线程的收集器。目前主要应用在客户端模式(Client VM)下的HotSpot虚拟机使用。

如果在服务端模式(Server VM)下,它也有两种用途:一个是在JDK5以及之前,和Parallel Scavenge收集器搭配使用,另外一个就是作为CMS收集器在出现并发模式故障(Concurrent Mode Failure) 时作为后备收集器。

SerialOld 负责收集老年代区域,它采用标记-整理算法。

ParNew

多线程的垃圾回收器 其他的功能性、配置、策略等等的和 Serial 基本一致

Parallel Scavenge(扒肉 丝杆vg)( jDK8默认的垃圾回收器

新生代的收集器,支持多线程并行回收,也同样是使用标记-复制来作为回收算法。但 Parallel Scavenge 的关注点不一样,它的目标是实现一个可控制吞吐量的垃圾收集器。

吞吐量的计算公式:运行用户代码时间 / (运行用户代码时间 + 运行垃圾收集时间)

假设运行用户代码时间是 99 分钟,运行垃圾收集时间是 1 分钟,结合计算公式 :吞吐量 = 99 / (99 + 1) = 0.99,也就是 99% 的吞吐量。

收集器提供了一些参数,给用户按自身需求控制吞吐量

XX:MaxGCPauseMillis控制垃圾收集停顿的最大时间,单位是毫秒,可以设置一个大于0的数值。不要想着把这个数值设置得很小来提升垃圾收集的速度,这里缩短的停顿时间是以牺牲新生代空间大小换来的,空间小,回收自然就快,停顿时间自然也短,但是空间小,吞吐量自然也会小。所以得综合考虑。-XX:GCTimeRatio设置垃圾收集时间占比的计算因子,参数范围是0 - 100的整数。它的公式是 1 / (1+GCTimeRatio)举个栗子:当设置成15,那就是 1 / (1+15) = 0.0625,就是允许最大垃圾收集时间占总时间的6.25%,当设置成99的时候,就是 1 / (1+99) = 0.01,也就是允许最大垃圾收集时间占总时间的1%,依次类推。-XX:+UseAdaptiveSizePolicy动态调整开关,这个参数和 Parallel Scavenge 收集器无关,但是搭配起来使用是一个很好的选择。当这个参数被激活,就不需要人工指定新生代的大小、Eden和Survivor区的比例、对象直接进入老年代的大小等等细节参数了,JVM会根据当前运行的情况动态调整,给出最合适的停顿时间和吞吐量。搭配以上两个参数,和把基本的内存数据设置好即可,例如堆的最大占用空间等等。

arallel Old 是 Parallel Scavenge 的老年代版本。
Parallel Old 也支持多线程并行回收的能力,使用标记-整理来作为回收算法。这个收集器是JDK6的时候推出的,和 Parallel Scavenge 搭配,在多CPU核心和大内存的场景下,吞吐性能优秀。

CMS

CMS 负责收集老年代区域,它采用标记-清除算法。 它是一款并发低停顿的收集器,对于响应速度有较高要求,对停顿时间忍受度低的应用,非常适合使用CMS作为垃圾收集器。

Garbage First(G1)

:实现一个停顿时间可控的低延迟垃圾收集器

依然遵循分代回收的设计理论,但它对堆(Java Heap)内存进行了重新布局,不再是简单的按照新生代、老年代分成两个固定大小的区域了,而是把堆区划分成很多个大小相同的区域

多线程:

实现多线程的4种方式
  • 继承 Thread 重写 run 方法;

  • 实现 Runnable 接口;

  • 实现 Callable 接口。

  • 线程池

    线程的状态:
    • NEW 尚未启动

    • RUNNABLE 正在执行中

    • BLOCKED 阻塞的(被同步锁或者IO锁阻塞)

    • WAITING 永久等待状态

    • TIMED_WAITING 等待指定的时间重新被唤醒的状态

    • TERMINATED 执行完成

      线程的 run() 和 start() 有什么区别?

      start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

在 Java 程序中怎么保证多线程的运行安全
  • 方法一:使用安全类,比如 Java. util. concurrent 下的类。

  • 方法二:使用自动锁 synchronized。

  • 方法三:使用手动锁 Lock。

    synchronized 和 Lock 有什么区别
    • synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。

    • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。

    • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

    怎么防止死锁
    • 尽量使用 Java. util. concurrent 并发类代替自己手写锁。

    • 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。

    • 尽量减少同步的代码块。

      线程池的核心参数:

      包含核心线程数(corePoolSize)、最大线程数(maximumPoolSize),空闲线程超时时间(keepAliveTime)、时间单位(unit)、阻塞队列(workQueue)、拒绝策略(handler)、线程工厂(ThreadFactory)这7个参数。

      1. 判断线程池的状态,如果不是RUNNING状态,直接执行拒绝策略

      2. 如果当前线程数 < 核心线程池,则新建一个线程来处理提交的任务

      3. 如果当前线程数 > 核心线程数且任务队列没满,则将任务放入阻塞队列等待执行

      4. 如果 核心线程池 < 当前线程池数 < 最大线程数,且任务队列已满,则创建新的线程执行提交的任务

      5. 如果当前线程数 > 最大线程数,且队列已满,则执行拒绝策略拒绝该任务

        1. 线程池都有哪些状态?

          NEW , /* 新建 */

        ​ RUNNABLE , /* 可运行状态 */

        ​ BLOCKED , /* 阻塞状态 */

        ​ WAITING , /* 无限等待状态 */

        ​ TIMED_WAITING , /* 计时等待 */

        ​ TERMINATED; /* 终止 */

      6. 线程池的拒绝策略有哪些?

      RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。

      ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。

      ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常 这是不推荐的做法。

      ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务 然后把当前任务加入队列中。

      ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。

synchronized 实现原理:

原子性、可见性、有序性

涉及到jvm中的对象结构 :一个对象包含:实例数据 ,对齐填充,对象头等

  • Java 对象头

    在 Java 虚拟机中,每个对象都有一个对象头,用于存储对象的元数据信息,包括对象的哈希码、GC 相关信息、锁状态等。对象头通常包含一个标记字段(Mark Word),用于标识对象的锁状态。

  • Monitor(监视器

    Monitor 是一种同步机制,负责管理对象的锁。每个对象都与一个 Monitor 相关联。当一个线程尝试进入一个被synchronized修饰的代码块或方法时,它会尝试获取对象的 Monitor。如果 Monitor 处于无锁状态,则当前线程会尝试将其锁定;如果 Monitor 已经被其他线程锁定,则当前线程会进入阻塞状态,直到持有锁的线程释放锁。

    在 JDK 1.6 之前,synchronized 关键字的实现确实被认为是重量级锁 ,原理基于操作系统提供的互斥量来实现线程间的同步;这涉及了从用户态到内核态的切换以及线程上下文切换等 ;一旦一个线程获得了锁,其他试图获取相同锁的线程将会被阻塞,这种阻塞操作会导致线程状态的改变和 CPU 资源的消耗。

    大致过程:当线程尝试进入 synchronized 代码块时,它会尝试获取对象的 Monitor(监视器)。如果 Monitor 的锁状态是无锁状态(Unlocked),则当前线程将尝试获取锁。如果获取成功,则进入临界区执行代码。如果锁已经被其他线程持有,则当前线程将进入阻塞状态,等待锁被释放。当持有锁的线程退出临界区并释放锁时,等待的线程将被唤醒,重新尝试获取锁。

    缺点:

    • 需要从用户态到内核态的切换,以及线程上下文的切换,导致性能开销较大。

    • 对于竞争不激烈的情况,阻塞等待锁的线程可能会浪费大量的 CPU 时间。

    • 线程在竞争锁的过程中可能会发生多次上下文切换,影响性能

      在 JDK 1.6 中引入了偏向锁(Biased Locking)和轻量级锁(Lightweight Locking)等优化机制,提高了 synchronized 的性能和并发能力。

      底层实现机制如下:

      偏向锁(Bias Locking):当一个线程第一次访问一个同步块时,它会尝试获取对象的偏向锁。如果对象没有被其他线程锁定,那么当前线程会尝试将对象的偏向锁设置为自己。之后,当该线程再次访问同步块时,不需要再次获取锁,直接进入同步块执行。这样可以提高同步操作的性能,减少不必要的竞争。

      轻量级锁(Lightweight Locking):如果对象已经被其他线程获取了偏向锁,但当前线程又想要获取锁,就会升级为轻量级锁。轻量级锁的获取过程包括尝试将对象头的 Mark Word 设置为指向当前线程的锁记录(Lock Record)和CAS(Compare and Swap)操作。如果锁竞争激烈,会升级为重量级锁。

      重量级锁(Heavyweight Locking):如果对象的锁被多个线程竞争,那么会升级为重量级锁。重量级锁使用操作系统的互斥量(Mutex)来实现同步,涉及到用户态和内核态的切换,性能相对较低,但能够确保线程的互斥访问。

    • Mysql

      索引:

      ​ mysql索引由B+tree构成

      b+tree的特点:

      1.所有叶子节点都是按照顺序连接在一起的,可以很方便地遍历整棵树的所有叶子节点,也方便基于范围的查询操作。

      2.所有数据记录的指针都保存在叶子节点上,因此在进行数据查询时,只需要搜索一次B+树就可以找到所有需要的数据记录。

      3.B+树的内部节点只存储索引的值,而不存储数据记录的指针,因此B+树的内部节点可以存储更多的索引,从而减少树的高度,提高查询效率。

      4.B+树的高度很低,因为每个节点存储的索引值比B树要多,所以B+树相对于B树来说,可以更好地利用磁盘预读特性,从而减少I/O操作的次数。

      索引失效的场景:

      1.不满足最左前缀

      2.范围查询之后的索引字段,会失效,但本身用来范围查询的那个索引字段依然有效

      3.索引字段做运算

      4.避免使用select *

      5.用or分割开的条件, 如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到

    • 以%开头的Like模糊查询

    • mysql事务隔离级别:

    • 读未提交(READ UNCOMMITTED) 脏读 不可重复读 幻读

    • 读已提交 (READ COMMITTED) 幻读 不可重复读

    • 可重复读 (REPEATABLE READ)脏读 不可重复读 幻读

      如何解决幻读的:

      普通select 快照读 可重复读隔离级是由 MVCC(多版本并发控制)实现的

      当前读update :通过幻读行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key锁。

      间隙锁:举例来说,假设一个索引包含以下连续的值:10、15、20、30。如果事务A执行了一个范围查询WHERE id > 10 AND id < 30并请求了锁定,那么InnoDB不仅会对id为15和20的记录加锁,还会对(10, 15)和(15, 20)以及(20, 30)这些间隙加锁,这样一来,在这个范围内就不能再插入新的记录,从而避免了幻读现象的发生。

    • 串行化 (SERIALIZABLE)

    • mysql存储引擎:

      innoDB:

      支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。 (内存使用较高)

      MyISAM :插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比较低,也可以使用 (内存使用较低 )

      mysql的组成和查询过程:

      MySQL数据库的组成部分主要包括以下几个核心模块:

    • 连接管理器(Connection Manager / Connectors)

      • 负责监听并处理客户端的连接请求,验证用户的登录凭证(用户名、密码),建立并管理与客户端的通信连接。连接池组件也是在此环节中实现,用于复用已建立的连接,减少频繁创建和销毁连接的开销。

    • MySQL查询过程大致如下:

    • 连接建立

      • 客户端通过指定主机名、端口、用户名、密码等信息,与MySQL服务器建立TCP/IP连接。连接管理器验证登录信息并分配连接资源。

    • SQL提交

      • 客户端通过已建立的连接发送SQL查询请求。

    • 解析与验证

      • 解析器对SQL语句进行词法分析和语法分析,确保其符合SQL语法规则。同时,权限验证也在这一阶段进行,检查当前用户是否有执行该查询的权限。

    • 查询优化

      • 如果查询未命中缓存(如有查询缓存功能的话),优化器基于表结构、索引信息、统计信息等,生成一个执行计划,决定如何最高效地执行查询。这包括选择合适的索引、确定表连接顺序、选择合适的访问方法(全表扫描、索引扫描等)。

    • 执行查询

      • 执行器按照优化器生成的执行计划,与涉及的存储引擎交互,执行实际的数据检索操作。存储引擎根据执行器的请求,从磁盘(或内存)中读取数据,返回给执行器。

    • 结果返回

      • 执行器收集存储引擎返回的结果集,可能还需要进行排序、分组、聚合等操作。最终,将处理后的结果集通过连接返回给客户端。

    • 连接关闭(可选)

      • 完成查询后,客户端可以选择断开与MySQL服务器的连接

    • Redis

    • SQL接口组件

      • 提供与各种编程语言交互的接口,允许客户端以标准SQL语句的形式提交查询请求。例如,PHP、Java、Python等语言通过各自的MySQL驱动程序与SQL接口进行交互。

    • 解析器(Parser)

      • 接收到客户端提交的SQL语句后,解析器负责对其进行词法分析(识别关键词、标识符、字符串等)和语法分析(检查语句结构是否符合SQL语法规则)。如果发现语法错误,解析器会返回错误信息给客户端。

    • 查询分析器(Optimizer)

      • 对经过解析的合法SQL语句进行查询优化。它根据表的统计信息、索引情况以及查询条件,确定执行查询的最佳策略,包括但不限于选择合适的索引、确定表扫描或索引查找的顺序、是否需要使用临时表、是否进行全表扫描等。

    • 缓存组件(Cache)

      • 虽然您提到的资料中指出MySQL 5.8版本之后可能取消了查询缓存,但在某些版本或特定配置下,MySQL仍可能保留了查询缓存机制。如果启用,查询缓存会尝试缓存查询结果,当相同的查询再次执行时,直接从缓存返回结果,避免重复计算。然而,由于缓存管理的复杂性和在许多实际场景下的效率问题,现代MySQL部署往往建议禁用查询缓存。

    • 插件式存储引擎

      • MySQL的核心优势之一是其支持多种存储引擎,如InnoDB、MyISAM、Memory等。存储引擎负责数据的物理存储、读写操作、事务处理、锁管理等。查询过程中,执行器会与所涉及表的存储引擎交互,调用其提供的接口来执行具体的查询操作。

    • 管理服务和工具组件

      • 包括权限验证、日志记录(如binlog)、监控、备份恢复等功能,这些服务确保数据库系统的安全、稳定运行,并为管理员提供必要的管理和运维工具。

      • Redis

        redis:Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能非关系型(NoSQL)的(key-value)键值对数据库 优点:因为是纯内存操作速度快,单线程避免频繁切换上下文,,io多路复用通过跟踪每个 I/O 流的状态,来管理多个 I/O 流

        Redis 支持五种数据类型:

        string(字符串),hash(哈希),list(列表),set(集合)及 zsetsorted set(有序集合

        持久化:

        RDB :是指用数据集快照的方式半持久化模式记录 Redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

        优点:

      • 只有一个文件 dump.rdb,方便持久化。

      • 容灾性好,一个文件可以保存到安全的磁盘。

      • 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis的高性能。

      • 存储数据结构:String, list ,map 特别适合用于存储对象,Sorted set有序集合,和 set 一样也是 string 类型元素的集合,且不允许重复的成员 ,set集合,是 string 类型的无序集合, ,

        分布式锁 :使用reidis 的 setNx SET if Not eXists t 原理设置锁 成功1 失败0

         
      • 相对于数据集大时,比 AOF 的启动效率更高。

        缺点:数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 Redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨

        AOF,:是指所有的命令行记录以 Redis 命令请求协议的格式完全持久化存储保存为 aof 文件 、

        优点:

      • 数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。

      • 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。

      • AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)

        缺点:

        • AOF 文件比 RDB 文件大,且恢复速度慢。

        • 数据集大的时候,比 RDB 启动效率低。

  • 39
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值