1. JVM的架构,具体阐述一下各个部分的功能?解析:考察面试者对JVM有没有整体理解,一般在简历中写了相关技能,面试管会问参考答案:JVM(Java Virtual Machine,Java虚拟机)是Java程序运行的环境,它负责将Java字节码转换成特定机器上的机器码并执行。JVM的架构主要由以下几个部分组成,每个部分都有其特定的功能:类加载子系统:负责加载类的信息到JVM中。当Java程序需要使用某个类时,类加载子系统负责找到对应的.class文件,并将其加载到JVM的方法区中。这个子系统确保了类的正确性和安全性,同时实现了类的动态加载。方法区:存放已加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区是线程共享的内存区域,它在JVM启动的时候被创建,并且随着类的加载而动态扩展。方法区中包含运行时常量池,用于存放编译期生成的各种字面量和符号引用。Java堆:Java堆是JVM所管理的最大一块内存区域,它是所有线程共享的内存区域。几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此也经常发生垃圾回收操作。Java栈:每个Java虚拟机线程都有一个私有的Java栈,与线程同时创建。Java栈中保存着帧信息,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。程序计数器(PC寄存器):这是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。本地方法栈:与虚拟机栈所发挥的作用非常相似,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。此外,JVM还包含直接内存,这是Java堆外的、直接向系统申请的内存空间。直接内存的访问速度通常优于Java堆,因此在一些读写频繁的场合可能会考虑使用直接内存。垃圾回收器可以对方法区、Java堆和直接内存进行回收。JVM的执行引擎负责执行虚拟机的字节码,虚拟机会使用即时编译技术将方法编译成机器码后再执行。这样,JVM就能提供一个安全、稳定、高效的运行环境,使得Java程序能够跨平台运行。学习指引:JVM组成结构以及各部分的功能详解面经专栏直通车面经专栏下载2. Zset的底层如何实现解析:Redis 经典八股文之一,属于常考题。参考答案:Redis 的 Zset(有序集合)类型的底层实现会根据实际情况选择使用压缩列表(ziplist)或者跳跃表(skiplist)。Redis 会根据实际情况动态地在这两种底层结构之间切换,以在内存使用和性能之间找到一个平衡。这主要取决于两个配置参数:zset-max-ziplist-entries 和 zset-max-ziplist-value。使用压缩列表:当 Zset 存储的元素数量小于 zset-max-ziplist-entries 的值,且所有元素的最大长度小于 zset-max-ziplist-value 的值时,Redis 会选择使用压缩列表作为底层实现。压缩列表占用的内存较少,但是在需要修改数据时,可能需要对整个压缩列表进行重写,性能较低。压缩列表是一种为节省内存而设计的特殊编码结构,它将所有的元素和分数紧凑地存储在一起。这种方式的优点是占用内存少,但是在需要修改数据时,可能需要对整个压缩列表进行重写,性能较低。当 Zset 存储的元素数量较少,且元素的字符串长度较短时,Redis 会选择使用压缩列表作为底层实现。使用跳跃表:当 Zset 存储的元素数量超过 zset-max-ziplist-entries 的值,或者任何元素的长度超过 zset-max-ziplist-value 的值时,Redis 会将底层结构从压缩列表转换为跳跃表。跳跃表的查找和修改数据的性能较高,但是占用的内存也较多。跳跃表是一种可以进行快速查找的有序数据结构,它通过维护多级索引来实现快速查找。这种方式的优点是查找和修改数据的性能较高,但是占用的内存也较多。当 Zset 存储的元素数量较多,或者元素的字符串长度较长时,Redis 会选择使用跳跃表作为底层实现。跳跃表(skiplist)是一种可以进行快速查找的有序数据结构,它通过维护多级索引来实现快速查找。这两个参数都可以在 Redis 的配置文件中进行设置。通过调整这两个参数,你可以根据自己的应用特性,选择更倾向于节省内存,还是更倾向于提高性能。学习指引:Redis中zset的底层实现原理3. Mysql隔离机制有哪些?怎么实现的?可串行化是怎么避免的三个事务问题?解析:MySQL 常见八股文,考察数据库隔离机制及实现原理。参考答案:MySQL的隔离机制主要通过事务的隔离级别来实现。事务的隔离级别定义了事务之间如何相互隔离,以及如何影响其他事务。MySQL提供了四种事务隔离级别:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ和SERIALIZABLE。READ UNCOMMITTED(读未提交)这是最低的隔离级别。在这个级别下,一个事务可以读取另一个未提交事务的修改。这可能导致脏读、不可重复读和幻读。实现:无需额外的机制,直接读取数据即可。READ COMMITTED(读已提交)大多数数据库系统的默认隔离级别(但不是MySQL的默认级别)。它只能读取已经提交的数据。这可以防止脏读,但仍然可能导致不可重复读和幻读。实现:当事务进行时,它读取的数据行会被加上锁,其他事务不能修改这些数据行,直到当前事务提交或回滚。REPEATABLE READ(可重复读)MySQL的默认隔离级别。它确保了在同一个事务中多次读取同样记录的结果是一致的。这可以防止脏读和不可重复读,但仍然可能导致幻读。实现:MySQL使用多版本并发控制(MVCC)来实现这个隔离级别。每个事务都看到一个一致的数据快照,即使其他事务正在修改数据。SERIALIZABLE(可串行化)这是最高的隔离级别。它通过强制事务串行执行,从而避免脏读、不可重复读和幻读。但是,这可能会大大降低并发性能。实现:在SERIALIZABLE级别下,每个读写事务都会获得一个唯一的锁,这确保了事务的串行执行。MySQL通过表锁或行锁来实现这一点,具体取决于存储引擎。可串行化是如何避免三个事务问题的?三个事务问题通常指的是:脏读、不可重复读和幻读。脏读:一个事务读取了另一个未提交事务的修改。在SERIALIZABLE隔离级别下,由于事务是串行执行的,未提交的事务的修改不会被其他事务读取,因此避免了脏读。不可重复读:在同一个事务中,多次读取同一数据返回的结果有所不同。SERIALIZABLE级别通过强制事务串行执行,确保在事务期间数据的一致性,从而避免了不可重复读。幻读:一个事务在执行两次相同的查询,但由于另一个并发事务的插入或删除操作,导致第二次查询返回了不同的结果集。在SERIALIZABLE级别下,由于每个读写事务都会获得一个唯一的锁,这确保了其他事务不能插入或删除数据,从而避免了幻读。需要注意的是,虽然SERIALIZABLE级别可以完全避免这三个问题,但它也可能导致性能下降,因为它限制了并发性。在实际应用中,需要根据具体的应用需求和性能要求来选择合适的隔离级别。学习指引:MySQL事务隔离级别和实现原理4. Spring源码看过吗?Spring的三级缓存知道吗?解析:Spring 原理老八股问,常考。参考答案:Spring框架中确实存在三级缓存机制,它主要用于解决循环依赖问题。具体来说,这三级缓存分别是:singletonObjects:一级缓存,通常被理解为单例池,用于存放已经完全初始化的单例Bean实例。这些实例在缓存中是以键值对的形式存在的,键为Bean的名字,值为Bean实例。earlySingletonObjects:二级缓存,用于存放已经创建但还未完成初始化的单例Bean实例。这些Bean实例通常是因为依赖其他Bean实例而无法完成初始化,处于不完整状态。singletonFactories:三级缓存,用于存放Bean实例的工厂对象。这些工厂对象可以用来创建单例Bean实例,并在需要时提前暴露Bean,以便解决循环依赖问题。需要注意的是,只有单例的Bean会通过三级缓存提前暴露来解决循环依赖的问题。非单例的Bean每次使用都会从容器中创建新对象,因此不会将其放到三级缓存中。此外,Spring之所以设计二三级缓存而不仅仅是二级缓存,主要是为了解决循环依赖对象需要提前被AOP代理的问题,以及在没有循环依赖的情况下,早期的Bean也不会真正暴露,避免不必要的代理过程。总的来说,Spring的三级缓存机制是一个复杂但高效的设计,它允许Spring在创建和初始化Bean时处理各种复杂情况,包括循环依赖和AOP代理等。学习指引:彻底搞懂Spring之三级缓存解决循环依赖问题5. 抛开Spring,讲讲反射和动态代理?那三种代理模式怎么实现的?解析:Spring 原理八股,属于常考题参考答案:当谈到反射和动态代理时,通常是指在运行时动态地创建和操作类的实例,而不是在编译时静态地绑定类和方法。这些概念在Java编程中经常用于实现框架和库,以及在运行时实现各种功能的灵活性和可扩展性。反射(Reflection):反射允许在运行时检查、获取并操作类的属性、方法和构造函数,以及访问和修改类的成员变量。Java反射API提供了一组类和接口,例如Class、Method、Field等,可以用于实现这些功能。反射的主要应用包括:在运行时创建类的实例。动态地调用类的方法。获取和修改类的成员变量。通过反射读取和操作注解信息。动态代理(Dynamic Proxy):动态代理是一种在运行时动态生成代理类的机制,代理类负责调用目标类的方法并在必要时执行额外的逻辑。Java中的动态代理通常通过java.lang.reflect.Proxy类实现,它要求目标类必须实现至少一个接口。动态代理的主要应用包括:实现AOP(面向切面编程)。实现远程方法调用(RMI)。实现虚拟代理、延迟加载等模式。三种代理模式的实现方式:静态代理(Static Proxy):静态代理是通过手动编写代理类来实现的,在编译时就已经确定了代理类和目标类之间的关系。代理类通常实现与目标类相同的接口,并在方法中调用目标类的方法,并可以在方法前后添加额外的逻辑。JDK动态代理:JDK动态代理是基于接口的代理,在运行时通过java.lang.reflect.Proxy类动态生成代理类。这种代理方式要求目标类必须实现至少一个接口,代理类在调用目标类方法时会通过InvocationHandler接口来处理方法的调用。CGLIB动态代理:CGLIB动态代理是基于类的代理,它通过继承目标类并重写其方法来实现代理。与JDK动态代理不同,CGLIB动态代理可以代理没有实现接口的类,它使用Enhancer类生成代理类,并通过MethodInterceptor接口来处理方法的调用。学习指引: 深入理解 Java 反射和动态代理6. 讲讲线程池?为什么用线程池?解析:操作系统八股文,常考提,难度适中。参考答案线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。具体来说,线程池在系统中开辟一块区域,存放一些待命的线程,这些线程会等待任务的到来。一旦有任务需要执行,线程池会从这些待命的线程中选取一个来执行任务,任务执行完毕后,线程会返回线程池等待下一次的任务分配。使用线程池的主要原因包括:资源重用:通过重复利用已创建的线程,线程池显著降低了线程创建和销毁所带来的资源消耗,包括内存和CPU时间。这有助于减少系统的开销,并提高整体性能。提高响应速度:由于线程已经预先存在,当任务到达时,它们可以立即开始执行,无需等待线程的创建过程。这大大提高了系统的响应速度,特别是在高并发场景下。控制并发数:线程池可以有效地控制并发执行的任务数量,防止系统资源耗尽或响应速度下降。通过设置线程池的核心线程数、最大线程数以及队列大小等参数,可以根据系统的承载能力来限制并发级别,提高系统资源的利用率。提高线程的可管理性:线程是稀缺资源,无限制的创建不仅会消耗系统资源,还可能降低系统的稳定性。线程池提供了统一的线程分配、调优和监控机制,使得线程的管理更加便捷和高效。提供灵活的线程调度策略:线程池通常支持多种线程调度策略,如优先级队列、定时调度、阻塞队列等,这些策略可以根据任务的特性来合理分配线程资源,优化程序运行效率。异常处理与监控:线程池能够更好地统一管理和处理线程的异常情况,避免因为单个线程的异常而导致整个应用程序崩溃。此外,线程池还提供了监控机制,可以实时查看线程的状态和性能数据,便于进行性能分析和调优。线程池通过优化线程的使用和管理,提高了系统的性能和稳定性,特别是在处理大量并发任务时表现出色。因此,在需要频繁创建和销毁线程的场景中,使用线程池是一种非常有效的解决方案。学习指引:【详解】为什么使用线程池?线程池的实现原理是什么?7. 集合里面的arraylist和linkedlist的区别是什么?有何优缺点解析:JAVA 基础考察,难度简单参考答案:ArrayList 和 LinkedList 是 Java 集合框架中两种最常用的列表实现。它们的主要区别在于数据的内部存储和访问方式,因此它们在性能特性上有所不同。底层数据结构:ArrayList:基于数组实现。它在内存中维护一个连续的空间来存储元素。LinkedList:基于链表实现。它使用双向链表存储元素,每个元素都包含指向其前一个和后一个元素的引用。访问元素:ArrayList:通过索引访问元素非常快,因为可以直接计算元素在内存中的位置。时间复杂度为 O(1)。LinkedList:通过索引访问元素相对较慢,因为需要从头或尾开始遍历链表直到找到所需元素。时间复杂度为 O(n)。插入和删除元素:ArrayList:在列表的开头或中间插入或删除元素时,可能需要移动大量元素以保持数组的连续性。因此,这些操作相对较慢,特别是在列表很大时。LinkedList:在列表的任何位置插入或删除元素都相对较快,因为只需要更新相邻元素的引用。内存消耗:ArrayList:由于数组需要预留连续的内存空间,所以可能存在一定的内存浪费。但是,由于它的内存布局更紧凑,所以在存储相同数量的元素时,ArrayList 通常比 LinkedList 占用更少的内存。 LinkedList:由于每个元素都需要存储指向其前一个和后一个元素的引用,所以链表实现通常会有更高的内存开销。用途:当你需要频繁地通过索引访问元素,且不需要经常插入或删除元素时,ArrayList 是一个好选择。当你需要在列表的任何位置频繁地插入或删除元素时,LinkedList 更为合适。总结优缺点:ArrayList:优点:通过索引访问元素非常快,内存占用相对较小(相对于 LinkedList)。缺点:在列表开头或中间插入、删除元素时性能较差,因为可能需要移动大量元素。LinkedList:优点:在列表的任何位置插入或删除元素都很快,不需要移动其他元素。缺点:通过索引访问元素较慢,因为需要从头或尾开始遍历链表。内存占用相对较大,因为每个元素都需要存储额外的引用。学习指引:ArrayList和LinkedList的区别以及优缺点8. 介绍一下计网里面的tcp和udp协议解析:考察计算机网络基础知识,属于基础题,需要注意的是要答全要点。参考答案:TCP(传输控制协议)和UDP(用户数据报协议)是计算机网络中用于处理传输数据包的两种重要协议,它们各自具有不同的特点和使用场景。TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。在通信之前,TCP会在发送方和接收方之间建立连接,确保数据传输的可靠性。TCP通过确认机制、超时重传、流量控制以及拥塞控制等手段,确保数据能够无差错、不丢失、不重复、按序到达。TCP协议提供全双工通信,允许通信双方的应用进程在任何时候都可以发送数据。然而,由于TCP需要建立连接并进行一系列的检查与修改,其开销相对较大,实时性也不如UDP。相比之下,UDP协议则是一种无连接、不可靠的传输层协议。UDP在发送数据前不建立连接,也不对数据报进行检查与修改,因此其传输效率较高,实时性较好。但是,由于UDP不保证数据的可靠性,可能会出现分组丢失、重复、乱序等问题,需要应用程序自行处理这些问题。UDP协议适用于对实时性要求较高,但对数据可靠性要求不高的场景,如视频流传输、实时语音通信等。TCP和UDP协议各有优劣,应根据具体的应用场景和需求来选择合适的协议。对于需要保证数据可靠性的场景,如文件传输、电子邮件等,通常选择TCP协议;而对于实时性要求较高,但对数据可靠性要求不高的场景,则可以选择UDP协议。学习指引:深入理解网络通讯和TCP、IP协议9.  介绍一下http和https的区别?为什么https安全?解析:计算机网络基础知识,属于简单八股文,需要注意的是要答全要点。参考答案:HTTP和HTTPS的主要区别体现在安全性和数据传输方式上。首先,HTTP是超文本传输协议,它采用明文方式传输数据,这意味着数据在传输过程中是不加密的,可能会被第三方截获或篡改。相比之下,HTTPS则是具有安全性的SSL加密传输协议,它通过在HTTP协议的基础上添加SSL/TLS加密层,实现了数据的加密传输。因此,HTTPS可以防止中间人攻击和数据窃听,使得数据在传输过程中更加安全。其次,HTTPS需要到CA申请证书,一般情况下需要一定费用,而HTTP则无需此步骤。此外,HTTP和HTTPS使用的端口号也不同,HTTP的端口号是80,而HTTPS的端口号是443。HTTPS之所以安全,主要得益于其采用的加密技术和身份验证机制。SSL/TLS加密层对传输的数据进行加密处理,确保数据的完整性和机密性。同时,HTTPS还包含身份验证环节,通过验证服务器端的证书,确保客户端与正确的服务器进行通信,防止了中间人攻击和服务器劫持等安全问题。学习指引:只看这一篇就能搞懂Http和Https协议区别10. Mysql有很大的数据量怎么办?怎么分表分库?解析:MySQL 大数据处理,数据一定难度的题,也是常考题。参考答案:当MySQL数据库中的数据量变得非常大时,性能可能会受到影响,查询速度变慢,甚至可能导致系统崩溃。为了解决这个问题,我们可以采用分表分库的策略。分表分库是将一个大的数据库或表拆分成多个较小的、更易于管理的部分,以提高性能、可靠性和可维护性。以下是一些关于如何分表分库的建议:1. 垂直拆分垂直分库:按照业务将不同的表分到不同的数据库上,每个数据库都包含各自独立的业务数据。垂直分表:将一个大表中的某些列拆分到另一个表中,通常是按照列的属性进行拆分。例如,将经常访问的列和不经常访问的列分开。2. 水平拆分水平分库(也叫分区):按某个字段的某种规则,将同一个表中的数据拆分到多个数据库(服务器)上,每个库可以放在不同的服务器上。水平分表(也叫分片):将同一个表中的记录拆分到多个结构相同的表,每个表只包含一部分数据。常见的分片键有用户ID、订单ID等。3. 分片策略范围分片*:按某个范围来分片,比如按时间范围或ID范围。哈希分片:对某个字段进行哈希计算,然后按照哈希值的大小或范围进行分片。目录分片:创建路由表或目录,路由表中保存了数据记录的存放位置信息,系统执行查询操作时,根据路由表的地址信息去相应的分片中查询。4. 注意事项事务处理:分表分库后,跨表或跨库的事务处理变得更加复杂,需要考虑分布式事务的解决方案。数据迁移与备份:随着数据的增长,可能需要定期迁移数据或进行备份。确保有有效的数据迁移和备份策略。中间件:使用数据库中间件(如MyCAT、Sharding-JDBC等)可以简化分表分库的操作,隐藏底层的复杂性。应用层调整:分表分库后,应用层需要进行相应的调整,以支持新的数据结构和查询方式。测试与监控:在实施分表分库之前和之后,都需要进行充分的测试和监控,以确保系统的稳定性和性能。总之,分表分库是一个复杂的过程,需要根据具体的业务场景和需求来制定合适的策略。在实施之前,建议进行充分的调研和测试,以确保系统的稳定性和性能。学习指引 MySQL数据库中,数据量越来越大,有什么具体的优化方案么?11. Redis的基本数据类型?Redis的持久化呢?有何优缺点?解析:Redis 基础知识,简单的八股文,需要掌握好。参考答案:Redis是一个开源的使用ANSI C语言编写的、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。以下是关于Redis的基本数据类型和持久化的详细解答:一、Redis的基本数据类型Redis支持五种基本数据类型:String(字符串):这是Redis最基本的数据类型,可以理解为与Memcached中的类型一致,即一个key对应一个value。String类型是二进制安全的,意味着它可以包含任何数据,如jpg图片或序列化的对象。在Redis中,字符串的value最大可以达到512M。Hash(哈希):Redis的Hash是一个键值对集合,实际上是一个String类型的field和value的映射表。Hash特别适合用于存储对象。List(列表):列表是用来存储多个有序的字符串,一个列表最多可以存储2^32-1个元素。Set(集合):Redis的Set是String类型的无序集合,它是通过HashTable实现的。Zset(有序集合):Redis的Zset和Set一样都是String类型元素的集合,且不允许重复的成员。但与Set不同的是,Zset的每个元素都会关联一个double类型的分数,Redis正是通过分数来为集合中的元素从小到大进行从小到大的排序。此外,Redis还提供了其他三种特殊的数据结构类型,包括Geospatial(地理位置)、Hyperloglog(基数统计)和Bitmap(位图)。二、Redis的持久化Redis提供了两种主要的持久化方法:RDB(Redis DataBase)和AOF(Append Only File)。RDB:原理:通过创建子进程,将数据快照写入一个临时文件,然后替换之前的文件达到持久化。主进程在持久化过程中不进行IO操作,保证了持久化过程的高性能。优点:节省磁盘空间,恢复速度快。缺点:如果突然宕机,可能会丢失最后一次备份后的数据。此外,由于是利用拷贝技术,如果数据量较大,持久化过程可能会消耗较多性能。AOF:原理:通过记录每次写操作命令,并在启动时重新执行这些命令来恢复数据。优点:提供了更稳健的数据恢复机制,丢失数据少,生成的日志可读,便于处理误操作。缺点:相比RDB,AOF占用了更多的内存空间,恢复备份的速度较慢。如果每次读写都同步的话,会有一定的性能压力。在选择持久化方式时,需要根据实际的应用场景和需求进行权衡。例如,如果更关心数据的完整性和可靠性,可以选择AOF;如果更看重性能和磁盘空间的利用率,可以选择RDB。当然,也可以结合使用两种持久化方式,以达到最佳的效果。学习指引: redis的五种数据类型、redis持久化机制12. B+树了解吗?底层呢?为什么这么用?解析:数据库底层原理,数据结构基本原理,常考提,要掌握。参考答案:B+树是一种多路搜索树,其定义基本与B-树相同,但在某些方面进行了优化。B+树的主要特点包括:所有的叶子结点中包含了全部关键字的信息,以及指向包含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。所有的非终端结点可以看成索引部分,结点中仅含有其子树(根结点)中最大(或最小)关键字。非叶子节点的关键字数目等于它的分支数量。在B+树中,数据存放的更加紧密,具有更好的空间局部性,因此访问叶子节点上关联的数据具有更好的缓存命中率。同时,由于叶子结点都是相链的,对整棵树的遍历只需要一次线性遍历叶子结点即可,且由于数据顺序排列并且相连,所以便于区间查找和搜索。B+树的底层实现主要是基于多路平衡查找树。在查询过程中,从根节点出发,查找到叶子节点方可获得所查键值,然后根据查询判断是否需要回表查询数据。这种结构使得B+树在访问数据时具有更高的效率。B+树在数据库和许多其他数据结构中得到广泛应用的原因主要有以下几点:I/O次数更少:由于B+树的层数比其他树(如二叉搜索树)小,因此读取节点并进行I/O操作的次数也会减少。同时,B+树中间节点不存储数据,只存储索引,使得相同大小的磁盘页可以容纳更多的节点元素,进一步减少了I/O次数。查询更加稳定:B+树每次查询都必须访问到叶子节点,而B-树可能在中间节点或叶子节点找到匹配元素,因此B+树的查询性能更为稳定。更利于查询范围:由于B+树的叶子节点首尾相连,因此在进行范围查询时,只需要在链表上进行遍历,效率非常高。学习指引:面试官:MySQL索引为什么要用B+树实现?更多面经直通车原贴连接
点赞 132
评论 23
全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务