Java面试汇总

类加载机制

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如图所示:
java类加载过程
(1) 装载:查找和导入Class文件;
(2) 链接:把类的二进制数据合并到JRE中;
(a)校验:检查载入Class文件数据是否符合JVM规范;
(b)准备:给类的静态变量分配存储空间;这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中;所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值(final修饰的类变量除外)。
©解析:虚拟机将常量池内的符号引用替换为直接引用的过程;解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。得到它们在内存中的指针以便能直接访问。
(3) 初始化:对类的静态变量,静态代码块执行初始化操作;

类加载器:
(1) Bootstrap ClassLoader : 将存放于<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用
(2) Extension ClassLoader : 将<JAVA_HOME>\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载。开发者可以直接使用扩展类加载器。
(3) Application ClassLoader : 负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。

JVM类加载机制:
•全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
•双亲委派:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。优点:能确保一个类全局唯一,防止别篡改的风险具有较高的安全性。
•缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。

JVM

JVM内存模型

JVM内存模型图:
JVM内存模型图
程序计数器:当前线程所执行的字节
码行号指示器;
方法区:所有线程所共享的,用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
本地方法栈:为执行Native Method(Native Method就是一个java调用非java代码的接口)服务的;
java虚拟机栈:java方法执行的内存模型,是线程私有的,生命周期与线程相同。其中局部变量表存放方法参数和方法内部的局部变量,该表的内存大小在java编译期间就分配好了,如果发生内存溢出则会抛出OutOfMemoryError 异常,当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
堆:是所有线程共享的内存区域,该区域唯一的作用是存放对象实例,划分为:新生代、老年代、From Survivor、To Survivor;

垃圾回收算法

1.标记-清除算法
首先标记出所有需要回收的对象,然后统一清除被标记的对象。缺点是效率不高,回收后会产生大量的不连续的内存碎片;
2.复制算法
对内存分块,当某块的内存用完了,则将还存活的对象复制到另外一块内存上去,然后再把刚使用过的内存空间一次清理掉。优点是避免了产生内存碎片等情况。缺点是浪费了一部分内存。研究表明,新生代的对象的生命周期短,存活率低,因此将新生代的内存划分为Eden和两个Survivor的大小比例为8:1:1,每次都是把Eden和Survivor中存活的对象拷贝到另一块Survivor中,然后清理掉Eden和Survivor空间,内存的可用率能达到了90%。当Survivor空间不够用时需要依赖其它内存(老年代)进行分配担保;
3.标记-整理算法
该算法适合于老年代对象的回收。老年代回收对象为存活率较高,复制算法效率变低。第一个阶段仍是进行标记,第二个阶段是把所有存活的对象都向一端移动,按顺序排放,然后直接清理掉端边界意外的内存。
4.分代收集
是目前大多数虚拟机采用的垃圾回收算法。Java堆分为新生代和老年代,新生代选用复制算法,而老年代使用“标记-清理”或“标记-整理”算法来进行回收。

垃圾收集器
1.Serial 收集器 (新生代)
单线程的收集器,当该收集器运行时必须暂停其他所有的工作线程,直到它收集结束。
算法:复制算法
优点:简单高效,拥有很高的单线程收集效率
应用:Client模式下的默认新生代收集器,桌面应用内存一般不大,几十毫秒的停顿时间可以接受
2.ParNew 收集器 (新生代)
Serial 的多线程版本,原理与Serial 收集器一致,若老年代使用CMS收集器,则新生代只能使用ParNew 收集器(或者Serial收集器)
3.Parallel Scavenge 收集器(新生代)
与ParNew 一样使用复制算法,多线程收集器,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,高吞吐量可以最高效的利用CPU时间
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
控制吞吐量的参数:最大垃圾收集停顿时间 -XX:MaxGCPauseMillis ; 直接设置吞吐量大小:-XX:GCTimeRatio。
MaxGCPauseMillis 的值为一个大于0的毫秒数, 最大停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的。
GCTimeRatio 的值为一个大于0且小于100的整数。例如:-XX:GCTimeRatio=19,允许最大的GC时间为5%(1/(1+19)),默认值为99。
-XX:+UseAdaptiveSizePolicy:开启GC自适应调节策略,自动设置新生代大小、Eden与Survior区的比例、晋升老年代对象年龄等细节参数。
4.Serial Old 收集器 (老年代)
Serial收集器的老年代版本,也是一个单线程的收集器,使用标记-整理算法
5. Parallel Old 收集器 (老年代)
Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。与Parallel Scavenge收集器配合使用,吞吐量优先
6. CMS(concurrent mark sweep) 收集器 (老年代)
以获取最短回收停顿时间为目标,基于“标记-清除”算法。
缺点:
1)对cpu资源敏感,默认启动的回收线程数是(cpu数量+3)/4,当cpu数较少的时候,会分掉大部分的cpu去执行收集器线程,影响用户,降低吞吐量。
2)无法处理浮动垃圾,浮动垃圾即在并发清除阶段因为是并发执行,还会产生垃圾,这一部分垃圾即为浮动垃圾,要等下次收集。
3)因为使用的是“标记-清除”算法,会产生碎片。
7. G1(garbage first)收集器 (整个Java堆:包括新生代和老年代)
基于标记-整理算法,不会产生空间碎片,并且可以精确的控制停顿时间。G1将整个堆划分为多个固定的独立区域,在后台维护一个优先级列表,每次根据允许的时间范围,优先收回垃圾最多的区域。
JVM垃圾收集器相关常用参数:
JVM垃圾收集配置参数

JDK命令行工具

1.jps 虚拟机进程状况工具
参数:-m 输出虚拟机启动时传递给主类main()函数的参数
-l 输出主类全名,如果进程执行的是jar包,输出jar路径
-v 输出虚拟机进程启动时JVM参数

2.jstat 虚拟机统计信息监视工具
例如 :
格式:jstat [option] pid [interval] [count]
->jstat -gc [pid] 500 20
表示监视Java堆状况,每500毫秒查询一次,共查询20次
参数:
在这里插入图片描述
3.jmap java内存映像工具
格式:jmap [ option ] pid
参数:
-dump:[live,]format=b,file= 使用hprof二进制形式,输出jvm的heap内容到文件, live子选项是可选的,假如指定live选项,那么只输出活的对象到文件。
-finalizerinfo 打印正等候回收的对象的信息。
-heap 打印heap的概要信息,GC使用的算法,heap的配置及wise heap的使用情况。
-histo[:live] 打印每个class的实例数目,内存占用,类全名信息。VM的内部类名字开头会加上前缀”*”。 如果live子参数加上后,只统计活的对象数量。
-permstat 打印classloader和jvm heap永久代的信息。
-F 在虚拟机进程对-dump没有响应时,强制生成dump快照。 在这个模式下,live子参数无效。
4.jstack java堆栈跟踪工具
格式:jstack [ option ] pid
参数: -F 正常输出请求不响应时,强制输出线程堆栈
-l 显示关于锁的附加信息
-m 如果调用本地方法,可以显示C/C++的堆栈

并发

创建线程

创建线程的三种方式:
1.集成Thread,重写run()方法,创建实现类示例并调用start()方法;
2.实现Runnable接口,重写run()方法,实现类作为Thread(Runnable target)的入参;
3.实现Callable接口,重写call()方法,实现类作为FutureTask(Callable callable)的入参;
后面两种方式可以多个线程共同处理一份资源,Callable可以和FutureTask一起使用异步获取线程的返回结果。
示例代码如下:

package com.zhongxing.springboot.service.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author wangzhongxing
 * @version $Id: ThreadSample.java, v 0.1 2019年05月29日 4:39 PM wangzhongxing Exp $
 */
public class ThreadSample {

    public static int countNum = 0;

    public static void main(String[] args) {
        runnableSample();
        callableSample();
    }

    public static void callableSample() {
        System.out.println("callable main thread begin...");

        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("callable begin...");
                Thread.sleep(2000);
                System.out.println("callable end.");

                return "success";
            }
        };

        // FutureTask可以获取线程的执行情况,也可取消线程的执行
        FutureTask<String> futureTask = new FutureTask<>(callable);
        // 也可使用线程池 ThreadPoolTaskExecutor.execute(Runnable task, long startTimeout) 方法启动线程
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            Thread.sleep(500);
            System.out.println("callable main thread sleep 500ms");
            // 该方法是阻塞的,会等待线程执行完才会返回结果
            String result = futureTask.get();

            System.out.println("futureTask result=" + result);
        } catch (InterruptedException e) {
            // 记录异常日志
        } catch (ExecutionException e) {
            // 记录异常日志
        }

    }

    public static void runnableSample() {
        System.out.println("runnable main thread begin...");
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("runnable begin...");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    // 记录异常日志
                }
                System.out.println("runnable end.");
            }
        };

        // 也可使用线程池 ThreadPoolTaskExecutor.execute(Runnable task, long startTimeout) 方法启动线程
        Thread thread = new Thread(runnable);
        thread.start();

        System.out.println("runnable main thread end.");
    }
}
线程池

ThreadPoolTaskExecutor完全是使用ThreadPoolExecutor进行实现,是对ThreadPoolExecutor的增强。
ThreadPoolTaskExecutor配置如下:

<!-- 异步线程池 -->
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <!-- 核心线程数,默认为1 -->
    <property name="corePoolSize" value="3"/>
    <!-- 最大线程数,默认为Integer.Max_value -->
    <property name="maxPoolSize" value="10"/>
    <!-- 队列最大长度,默认为Integer.Max_value -->
    <property name="queueCapacity" value="25"/>
    <!-- 线程池维护线程所允许的空闲时间,默认值60s -->
    <property name="keepAliveSeconds" value="300"/>
    <!-- 设置为true(默认false)时,corePoolSize核心线程会超时关闭 -->
    <property name="allowCoreThreadTimeOut" value="true"/>
    <!-- 线程池对拒绝任务(无线程可用)的处理策略,默认策略 AbortPolicy  -->
    <property name="rejectedExecutionHandler">
        <!-- AbortPolicy:直接抛出java.util.concurrent.RejectedExecutionException异常 -->
        <!-- CallerRunsPolicy:主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,可以有效降低向线程池内添加任务的速度 -->
        <!-- DiscardOldestPolicy:抛弃旧的任务;会导致被丢弃的任务无法再次被执行 -->
        <!-- DiscardPolicy:抛弃当前任务;会导致被丢弃的任务无法再次被执行 -->
        <bean class="java.util.concurrent.ThreadPoolExecutor.DiscardPolicy"/>
    </property>
</bean>

读写锁(ReadWriteLock):ReadWriteLock将锁拆分为读锁和写锁,读锁可以并发获取。
可重入锁(ReentrantLock,ReentrantReadWriteLock):锁具备可重入性,同一线程可以多次获得锁,不同线程获得锁则需要等待。
乐观锁/悲观锁:乐观锁取数据的时候不会上锁,更新数据的时候或判断期间是否有人已经更新了数据,可以使用版本号之类的机制来保证;悲观锁取数据的时候就马上上锁,知道处理结束,数据库行锁、表锁、synchronized关键字使用的都是悲观锁。
公平锁/非公平锁:公平锁保证等待时间最长的线程将优先获得锁,而非公平锁并不会保证多个线程获得锁的顺序,但是非公平锁的并发性能表现更好。
自旋锁:非阻塞锁,A线程要获取的锁被其它线程占用,线程A不会马上被挂起,而是执行空循环,不停的试图获取锁。执行空循环属于用户态,被挂起属于内核态,这样可以避免用户态和内核态之间互相切换引起的开销和损耗。

java 锁

分布式锁

zookeeper分布式锁:
参考文档:https://blog.csdn.net/crazymakercircle/article/details/85956246

Redis分布式锁:
基于setnx(set if not exists)命令实现。
setnx格式:SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
缺点:Redis肌群如果某个master服务器宕机,slave服务器替换master服务器期间,由于master服务器到slave服务器之间的数据同步是异步的,可能会有短暂时间的数据丢失,会导致重复加锁。

ThreadLocal
Condition

Condition是在Java1.5之后出现的,基本方法是await(),signal(),可用于替代Object的wait(),notify()方法,实现线程间的协作。两者都要先获得锁之后才可调用,不同的是Object的wait(),notify()对应的是synchronized方式的锁,Condition对应的是实现Lock接口的锁。

如何避免死锁
  • 合理安排锁的顺序,避免死锁
  • 尽量使用定时锁(lock.tryLock(timeout))
  • 尽量避免锁嵌套,减少锁持有的时间,减小锁的粒度
如何提升多线程性能
  • 减小锁的范围(锁的代码块尽可能小);
  • 减小锁的粒度;
    例如:将锁分拆到用户的维度,不同用户之间锁的分离的;使用ConcurrentHashMap,ReadWritLock等分离锁的粒度,减少冲突的可能性;
  • 启用适当的线程数,减少线程切换的消耗;
同步工具类

CountDownLatch:可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,就可以利用CountDownLatch来实现。
CyclicBarrier:可以实现让一组线程等待至某个状态之后再全部同时执行。
Semaphore:可以控同时访问的线程个数,Semaphore(int permits)初始化最多可以有几个线程能同时访问,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

Atomic

atomic包中的类是基于CAS(比较与交换,Compare and swap)原理实现的。
CAS:CPU指令,在大多数处理器架构都支持的CAS指令,CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是乐观锁 技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS与lock的区别:

  • lock采用的是悲观锁,需要线程间的上下文切换;
  • CAS由底层硬件直接支持,采用乐观锁,自旋等待,不需要线程上下文切换;在高并发下CPU资源消耗较大;

io

bio:同步阻塞I/O,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。BIO方式适用于连接数目比较小且固定的场景,这种方式对服务端资源要求比较高,并发局限于应用中。

nio:同步非阻塞I/O,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的场景。Channel(通道),Buffer(缓冲区), Selector(选择器)。

aio:异步非阻塞I/O,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。AIO方式适用于连接数目多且连接比较长(重操作)的场景,充分调用OS参与并发操作。

Java Annotation

Annotation其实是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,通常与AOP、反射一起使用,在动态代理中识别有特殊注解标记的类或方法,并执行相应的处理。
注解的继承:
1.用在类上的自定义注解可以被继承下来。作用在接口上自定义注解不能被实现它的类继承下来。
2.类或接口方法上的自定义注解不能被重写或实现了其方法的子类继承。例如:A是接口,C类实现了A接口,但A接口方法上的注解无法被C类实现的方法继承;B是方法带注解的类,C继承了B类,C类如果重写了B的方法,B方法原有的注解是无法继承的,C如果没有重写B的方法,则可以继承B类方法原有的注解。

动态代理

JDK动态代理/CGlib代理:
JDK动态代理只能对实现了接口的类生成代理,而不能针对类。JDK使用Proxy和InvocationHandler实现代理
CGLIB是针对类实现代理,继承指定的类生成一个子类,覆盖其中的方法以实现增强,所以该类或方法不能声明成final,final类或方法是无法继承的。
spring如何实现动态代理:当Bean实现接口时,Spring会用JDK动态代理。当Bean没有实现接口时,Spring会使用CGlib实现动态代理。CGlib使用Enhancer和MethodInterceptor实现代理。可以通过配置:<aop:aspectj-autoproxy proxy-target-class=“true”/>强制限制spring使用CGlib代理。

spring基础:

spring AOP

spring AOP默认使用的是JDK动态代理,也可以通过spring.aop.proxy-target-class这个属性设置默认的代理方式,但如果代理类没有实现接口,则使用CGlib代理,用@Aspect注解将类声明为切面,@Pointcut,@Before,@Around(“execution(* com.zhongxing.service..(…))”),@After(@AfterReturning+@AfterThrowing)
用法:统一打印业务层监控日志、统一处理异常、声明式事务等。

spring aop结合annotation实现方法调用耗时、出参入参日志打印

编写注解类

package com.zhongxing.springboot.service.annotation;

import java.lang.annotation.*;

/**
 * 方法调用日志打印注解,由于接口方法上的注解无法被实现类继承,该注解必须打在接口的实现类的方法上
 * 
 * @author wangzhongxing
 * @version $Id: DigestLog.java, v 0.1 2019年05月17日 5:36 PM wangzhongxing Exp $
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DigestLog {

    /**
     * 是否打印方法入参
     *
     * @return
     */
    boolean isPrintParams() default true;

    /**
     * 是否打印方法返回结果
     *
     * @return
     */
    boolean isPrintResult() default true;

}

创建打印日志拦截器

package com.zhongxing.springboot.service.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.zhongxing.springboot.service.annotation.DigestLog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 方法调用日志打印拦截器
 * 
 * @author wangzhongxing
 * @version $Id: DigestLogMonitorInterceptor.java, v 0.1 2019年05月17日 5:59 PM wangzhongxing Exp $
 */
@Aspect
@Component("digestLogMonitorInterceptor")
public class DigestLogMonitorInterceptor {

    @Around("execution(* com.zhongxing.springboot.service.*.*(..))")
    public Object logPrint(ProceedingJoinPoint proceedingJoinPoint) {
        try {
            System.out.println("DigestLogMonitorInterceptor around begin!");
            long beginTime = System.currentTimeMillis();

            // 执行目标方法调用
            Object obj = proceedingJoinPoint.proceed();

            // 获取执行的目标方法
            MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
            Method method = methodSignature.getMethod();

            // 判断方法上是否有指定注解
            /**
             * 这里要注意,注解必须打在接口的实现类的方法上,接口上的注解无法被继承
             */
            if (method.isAnnotationPresent(DigestLog.class)) {
                DigestLog digestLog = method.getAnnotation(DigestLog.class);
                StringBuilder log = new StringBuilder("Digest log info,");
                log.append("method=" + proceedingJoinPoint.getSignature());
                // 判断是否需要打印方法入参
                if (digestLog.isPrintParams()) {
                    Object[] args = proceedingJoinPoint.getArgs();
                    String[] parameterNames = methodSignature.getParameterNames();
                    log.append(", params=");
                    JSONObject params = new JSONObject();
                    for (int i = 0; i < args.length; i++) {
                        params.put(parameterNames[i], args[i]);
                    }

                    log.append("params=" + params.toJSONString());
                }
                // 判断是否需要打印方法返回结果
                if (digestLog.isPrintResult()) {
                    log.append(", result=" + obj);
                }

                long invokeTime = System.currentTimeMillis() - beginTime;
                log.append(",invokeTime=" + invokeTime + "ms");

                System.out.println(log);
            }

            // 获取类上的注解
            //if (proceedingJoinPoint.getTarget().getClass().isAnnotationPresent(Service.class)) {
            //    Service serviceAnnotation = proceedingJoinPoint.getTarget().getClass().getAnnotation(Service.class);
            //}
            // 获取接口上的注解
            //for (Class<?> cls : proceedingJoinPoint.getTarget().getClass().getInterfaces()) {
            //    if (cls.isAnnotationPresent(Service.class)) {
            //        Service serviceAnnotation = cls.getAnnotation(Service.class);
            //    }
            //}

            return obj;
        } catch (Throwable throwable) {
            System.out.println("System error!");

            return null;
        }
    }

}

编写需要打印日志的接口和实现类

package com.zhongxing.springboot.service;

/**
 * @author wangzhongxing
 * @version $Id: ExternalInvokeService.java, v 0.1 2019年05月17日 5:41 PM wangzhongxing Exp $
 */
public interface ExternalInvokeService {

    /**
     * 调用外部服务
     *
     * @param param
     * @return
     */
    String invokeExternalService(String param);

}



package com.zhongxing.springboot.service.impl;

import com.zhongxing.springboot.service.ExternalInvokeService;
import com.zhongxing.springboot.service.annotation.DigestLog;
import org.springframework.stereotype.Service;

/**
 * @author wangzhongxing
 * @version $Id: ExternalInvokeServiceImpl.java, v 0.1 2019年05月20日 11:16 AM wangzhongxing Exp $
 */
@Service("externalInvokeService")
public class ExternalInvokeServiceImpl implements ExternalInvokeService {

    @DigestLog(isPrintParams = true, isPrintResult = true)
    @Override
    public String invokeExternalService(String param) {
        System.out.println("ExternalInvokeServiceImpl.invokeExternalService,param=" + param);
        // 业务处理逻辑...

        return "success";
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值