【面试】网易游戏社招一面总结

基本情况

面试岗位:Python游戏开发岗
面试方式:视频面试
面试官数量:2人
面试感觉:首先,开场之后,无需自我介绍,从简历开始问,这一点很有技术范儿。面试过程中,两位面试官交叉提问,并且在回答过程中根据技术点随时打断补充提问,很像是一个开发小组在就有个问题讨论。另外在回答问题过程中,可能有些问题回答不上来,面试官还能够给与一点提示,这一点感觉非常好。最后,虽然自己水平很差,但是能够有这样的交流,收获满满,也非常感谢两位年轻帅气的面试官。

问题列表

Java部分

  1. 在公司做了哪些项目?主要用到的哪些技术?自己的贡献是什么?
    答:结合实际情况回答,例如,Spring,SpringBoot,MyBatis,Redis, MySQL,Zabbix,Ubuntu等

  2. 使用的应用服务器是什么?它的启动流程,它是如何根据URL找到对应的处理逻辑?
    答:使用的是jetty。外部启动一个Jetty服务器的流程如下:
    1)java start.jar进行启动,解析命令行参数并读取start.ini中配置的所有参数;
    2)解析start.config确定jetty模块的类路径并确定首先执行的MainClass;
    3)可以选择是否另起一个进程来,如果不另起进程,则通过反射来调用MainClass,start.ini中配置的JVM参数不会生效;
    4)MainClass默认是XmlConfiguration,解析etc/jetty.xml,etc/jetty-deploy.xml等,创建实例并组装Server(是根据在start.ini中定义的顺序创建,而且顺序很重要,这里的IOC是jetty自己实现的),然后调用start()启动Server()。
    5)Server启动其他组件的顺序是:首先启动设置到Server的Handler,通常这个Handler会有很多子handler,这些handler将组成一个Handler链,Server会依次启动这个链上的所有Handler,接着会启动注册在Server上JMX的Mbean,让Mbean也一起工作,最后会启动Connector,打开端口,接受客户端请求。

补充知识:Jetty处理请求流程
Jetty接收到一个请求时,Jetty就把这个请求交给在Server中注册的而代理Handler去执行,如何执行注册的Handler同样由你规定,Jetty要做的就是调用你注册的第一个Handler的handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)方法,接下来要怎么做,完全由你决定。要能接收一个Web请求访问,首先要创建一个ContextHandler
当我们在浏览器中输http://localhost:8080时请求将会代理到Server类的handle方法,Server的handle方法将请求代理给ContextHandler的handle方法,ContextHandler又调用另一个Handler的handle方法。这个调用方式是和Servlet的工作方式类似,在启动之前初始化,创建对象后调用Servlet的service方法。在Servlet的API中我通常也只实现它的一个包装好的类,在Jetty中也是如此。虽然ContextHandler也只是一个handler,但是这个Handler通常由Jetty帮你实现,我们一般只要实现一些与具体要做的业务逻辑有关的Handler就好了,而一些流程性的或某些规范的Handler,我们直接用就好了。下图是请求Servlet的时序图。
Jetty处理流程图Jetty处理请求的过程就是Handler链上handle方法的执行过程。这里需要解释的一点是ScopeHandler的处理规则,ServletContextHandler、SessionHandler和ServletHandler都继承了ScopeHandler,那么这三个类组成一个Handler链,他们的执行规则是ServletContextHandler.handler→ServletContextHandler.doScope→SessionHandler.doScope→ServletHandler.doScope→ServletContextHandler.doHandle→SessionHandler.doHandle→ServletHandler.doHandle,这种机制使得我们可以在duScope阶段做一些额外工作。

补充问题:Jetty和Tomcat的比较

  • 相同点:Tomcat和Jetty都是一种Servlet引擎,他们都支持标准的servlet规范和JavaEE的规范。
  • 不同点:
    • 架构比较
      • Jetty的架构比Tomcat的更为简单
      • Jetty的架构是基于Handler来实现的,主要的扩展功能都可以用Handler来实现,扩展简单。
      • Tomcat的架构是基于容器设计的,进行扩展是需要了解Tomcat的整体设计结构,不易扩展。
    • 性能比较
      • Jetty和Tomcat性能方面差异不大
      • Jetty可以同时处理大量连接而且可以长时间保持连接,适合于web聊天应用等等。
      • Jetty的架构简单,因此作为服务器,Jetty可以按需加载组件,减少不需要的组件,减少了服务器内存开销,从而提高服务器性能。
      • Jetty默认采用NIO结束在处理I/O请求上更占优势,在处理静态资源时,性能较高
      • 少数非常繁忙;Tomcat适合处理少数非常繁忙的链接,也就是说链接生命周期短的话,Tomcat的总体性能更高。Tomcat默认采用BIO处理I/O请求,在处理静态资源时,性能较差。
    • 其它比较
      • Jetty的应用更加快速,修改简单,对新的Servlet规范的支持较好。
      • Tomcat目前应用比较广泛,对JavaEE和Servlet的支持更加全面,很多特性会直接集成进来。
  1. 在Spring框架中web.xml的结构是什么?
    答:1)Spring框架解决字符串编码问题:过滤器 CharacterEncodingFilter(filter-name), 过滤器就是针对于每次浏览器请求进行过滤的
    2)在web.xml配置监听器ContextLoaderListener(listener-class),ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,在web.xml配置这个监听器,启动容器时,就会默认执行它实现的方法。 在ContextLoaderListener中关联了ContextLoader这个类,所以整个加载配置过程由ContextLoader来完成。
    3)部署applicationContext的xml文件:contextConfigLocation(context-param下的param-name)
    4)DispatcherServlet是前置控制器,配置在web.xml文件中的。拦截匹配的请求,Servlet拦截匹配规则要自已定义,把拦截下来的请求,依据某某规则分发到目标Controller来处理。
    5)DispatcherServlet(servlet-name、servlet-class、init-param、param-name(contextConfigLocation)、param-value) ,在DispatcherServlet的初始化过程中,框架会在web应用的 WEB-INF文件夹下寻找名为[servlet-name]-servlet.xml 的配置文件,生成文件中定义的bean。

  2. Java的集合类中List有哪几种实现?Arralist与Vector区别?Arraylist与LinkedList区别?以及它们的扩增方式。
    答:常见的实现有三种:ArrayList,LinkedList,Vector。另外还有AbstractList,AbstractSequentialList等。它们之间的区别如下:
    ArrayList:
    1)ArrayList底层通过数组实现,随着元素的增加而动态扩容。
    2)ArrayList是Java集合框架中使用最多的一个类,是一个数组队列,线程不安全集合。
    3)它继承于AbstractList,实现了List, RandomAccess, Cloneable, Serializable接口。①ArrayList实现List,得到了List集合框架基础功能;②ArrayList实现RandomAccess,获得了快速随机访问存储元素的功能,RandomAccess是一个标记接口,没有任何方法;③ArrayList实现Cloneable,得到了clone()方法,可以实现克隆功能;④ArrayList实现Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是hessian协议。
    4)ArrayList的特点:容量不固定,随着容量的增加而动态扩容(阈值基本不会达到);有序集合(插入的顺序==输出的顺序);插入的元素可以为null;增删改查效率更高(相对于LinkedList来说);线程不安全
    ArrayList扩增源码如下:
    ArrayList扩增源码
    LinkedList
    1)LinkedList底层通过链表来实现,随着元素的增加不断向链表的后端增加节点。
    2)LinkedList是一个双向链表,每一个节点都拥有指向前后节点的引用。相比于ArrayList来说,LinkedList的随机访问效率更低。
    3)它继承AbstractSequentialList,实现了List, Deque, Cloneable, Serializable接口。①LinkedList实现List,得到了List集合框架基础功能;②LinkedList实现Deque,Deque 是一个双向队列,也就是既可以先入先出,又可以先入后出,说简单点就是既可以在头部添加元素,也可以在尾部添加元素;③LinkedList实现Cloneable,得到了clone()方法,可以实现克隆功能;④LinkedList实现Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是hessian协议。
    Vector
    和ArrayList基本相似,利用数组及扩容实现List,但Vector是一种线程安全的List结构,它的读写效率不如ArrayList,其原因是在该实现类内在方法上加上了同步关键字。源码如下:
    Vector获取元素
    其不同之处还在于Vector的增长速度不同:即
    Vector增加元素
    Vector扩增
    Vector在默认情况下是以两倍速度递增,所以capacityIncrement可以用来设置递增速度,因此Vector的初始化多了一种方式,即设置数组增量。
    Arralist与Vector区别与联系
    1) ArrayList出现于jdk1.2,Vector出现于1.0。两者底层的数据存储都使用的Object数组实现,因为是数组实现,所以具有查找快(因为数组的每个元素的首地址是可以得到的,数组是0序的,所以: 被访问元素的首地址=首地址+元素类型字节数*下标 ),增删慢(因为往数组中间增删元素时,会导致后面所有元素地址的改变)的特点
    2)继承的类实现的接口都是一样的,都继承了AbstractList类(继承后可以使用迭代器遍历),实现了RandomAccess(标记接口,标明实现该接口的list支持快速随机访问),cloneable接口(标识接口,合法调用clone方法),serializable(序列化标识接口)
    3)当两者容量不够时,都会进行对Object数组的扩容,arraylist默认增长1.5倍;Vector可以自定义若不自定义,则增长2倍
    4)构造方法略有不同
    ①ArrayList的构造方法:
    ArrayList a1 = new ArrayList(int i); 指定初始化容量的构造方法
    ArrayList a2 = new ArrayList(); 默认构造方法,在添加第一个元素过程中初始化一个长度为10的Object数组
    ArrayList a3 = new ArrayList(Collection); 在构造方法中添加集合,本方法创建的集合的object数组长度等于实际元素个数
    ②Vector的构造方法:
    Vector v1 = new Vector(10,2); 指定初始长度(initialCapacity)与增长因子(capacityIncrement)注意这里的增长因子不是oldCapacity * capacityIncrement而是+,如果不指定或者指定为0,则默认扩容当前容量的两倍。
    Vector v2 = new Vector(10); 通过this关键字调用上面的构造方法,自定义初始数组长度,增长因子默认为0
    Vector v3 = new Vector(); 默认构造方法,在创建对象时便分配长度为10的Object数组
    5)线程的安全性不同,Vector是线程安全的,在Vector的大多数方法都使用synchronized关键字修饰,ArrayList是线程不安全的(可以通过Collections.synchronizedList()实现线程安全)
    6)性能上的差别,由于Vector的方法都有同步锁,在方法执行时需要加锁、解锁,所以在执行过程中效率会低于ArrayList,另外,性能上的差别还体现在底层的Object数组上,ArrayList多了一个transient关键字,这个关键字的作用是防止序列化,然后在ArrayList中重写了readObject和writeObject方法,这样是为了在传输时提高效率。
    ArrayList和LinkedList比较
    1)对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
    2)在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
    3)LinkedList不支持高效的随机元素访问
    4)ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

  3. 什么是线程安全和非线程安全?
    答:线程安全就是在多线程环境下也不会出现数据不一致,而非线程安全就有可能出现数据不一致的情况。线程安全由于要确保数据的一致性,所以对资源的读写进行了控制,换句话说增加了系统开销。所以在单线程环境中效率比非线程安全的效率要低些,但是如果线程间数据相关,需要保证读写顺序,用线程安全模式。线程安全是通过线程同步控制来实现的,也就是synchronized关键字。

  4. Volatile关键字的作用?
    答:回答volatile关键之前,先说明一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景。
    内存模型:计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。也就是说,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中
    在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存。如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量
    为了解决缓存不一致性问题,通常来说有以下2种解决方法:
    1)通过在总线加LOCK #锁的方式 => 由于在锁住总线期间,其他CPU无法访问内存,导致效率低下
    2)通过缓存一致性协议 => 当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
    这2种方式都是硬件层面上提供的方式。
    并发编程的三个重要概念:原子性问题,可见性问题,有序性问题。
    1)原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
    2)可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
    3)有序性:即程序执行的顺序按照代码的先后顺序执行。这里存在指令重排序(Instruction Reorder)的问题。
    什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性
    Java内存模型提供了哪些保证以及在java中提供了哪些方法和机制来让保证在进行多线程编程时程序能够正确执行
    1)原子性问题:在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
    2)可见性问题:对于可见性,Java提供了volatile关键字来保证可见性当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
    3)有序性问题:在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序
    深入剖析volatile关键字
    1)volatile关键字的两层语义:①保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。②禁止进行指令重排序。
    2)volatile保证原子性吗?volatile没办法保证对变量的操作的原子性,可以使用的其他方法有:synchronized,Lock,AtomicInteger
    3)volatile能保证有序性吗?volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。这里有两层意思:①当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;②在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
    4)volatile的原理和实现机制:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:①它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;②它会强制将对缓存的修改操作立即写入主存;③如果是写操作,它会导致其他CPU中对应的缓存行无效。
    使用volatile关键字的场景:通常来说,使用volatile必须具备以下2个条件:①对变量的写操作不依赖于当前值,②该变量没有包含在具有其他变量的不变式中。使用volatile的几个场景:状态标记量、double check。

  5. JDK1.8有哪些变化?
    答:Lambda表达式、函数式接口、*方法引用和构造器调用、Stream API、接口中的默认方法和静态方法、新时间日期API
    在jdk1.8中对hashMap等map集合的数据结构优化。hashMap数据结构的优化:原来的hashMap采用的数据结构是哈希表(数组+链表),hashMap默认大小是16,一个0-15索引的数组,如何往里面存储元素,首先调用元素的hashcode方法,计算出哈希码值,经过哈希算法算成数组的索引值,如果对应的索引处没有元素,直接存放,如果有对象在,那么比较它们的equals方法比较内容,如果内容一样,后一个value会将前一个value的值覆盖,如果不一样,在1.7的时候,后加的放在前面,形成一个链表,形成了碰撞,在某些情况下如果链表
    无限下去,那么效率极低,碰撞是避免不了的;加载因子:0.75,数组扩容,达到总容量的75%,就进行扩容,但是无法避免碰撞的情况发生。在1.8之后,在数组+链表+红黑树来实现hashmap,当碰撞的元素个数大于8时 & 总容量大于64,会有红黑树的引入,除了添加之后,效率都比链表高,1.8之后链表新进元素加到末尾;ConcurrentHashMap (锁分段机制),concurrentLevel,jdk1.8采用CAS算法(无锁算法,不再使用锁分段),数组+链表中也引入了红黑树的使用
    什么是函数式接口?简单来说就是只定义了一个抽象方法的接口(Object类的public方法除外),就是函数式接口,并且还提供了注解:@FunctionalInterface
    Stream操作的三个步骤:创建stream => 中间操作(过滤、map)=> 终止操作
    新的日期API: LocalDate | LocalTime | LocalDateTime
    JVM变化:在JDK1.8之后,堆的永久区取消了,由元空间取代;在JDK 1.7中使用的是堆内存模型

  6. 垃圾回收机制中,对新生代和老生代了解吗?原理是什么?用到了哪些算法?
    答:先了解以下JVM的内容管理,其中包括判断对象存活还是死亡的算法(引用计数算法、可达性分析算法),常见的垃圾收集算法(复制算法、分代收集算法等以及这些算法适用于什么代)以及常见的垃圾收集器的特点(这些收集器适用于什么年代的内存收集)。JVM运行时数据区由程序计数器、堆、虚拟机栈、本地方法栈、方法区部分组成JVM内存结构由程序计数器、堆、栈、本地方法栈、方法区等部分组成,结构图如下所示:
    JVM运行时数据区结构图和JVM内存结构图
    ①程序计数器,也指pc寄存器:几乎不占有内存。用于取下一条执行的指令
    ②堆:所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。堆被划分为新生代和老生代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace和ToSpace组成,(也指s0,s1)结构图如下所示:
    JVM中新生代与老生代结构图
    新生代:新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。
    旧生代:用于存放新生代中经过多次垃圾回收仍然存活的对象。
    ③栈:每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。
    ④本地方法栈:用于支持native方法的执行,存储了每个native方法调用的状态
    ⑤方法区:存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用永久代(PermanetGeneration)来存放方法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。
    JVM的垃圾回收机制:JVM分别对新生代和旧生代采用不同的垃圾回收机制。
    1)新生代的GC:新生代通常存活时间较短,因此基于复制算法来进行回收,所谓复制算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和其中一个Survivor,复制到另一个之间Survivor空间中然后清理掉原来就是在Eden和其中一个Survivor中的对象。新生代采用空闲指针的方式来控制GC触发指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到 survivor,最后到老年代。在执行机制上JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)
    ①串行GC:在整个扫描和复制过程采用单线程的方式来进行,适用於单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定
    ②并行回收GC:在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用**-XX:+UseParallelGC来强制指定**,用**-XX:ParallelGCThreads=4来指定线程数**
    ③并行GC:与老生代的并发GC配合使用
    2)老生代的GC:老生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行 GC(SerialMSC)、并行GC(parallelMSC)和并发GC(CMS),具体算法细节还有待进一步深入研究。
    补充知识:JVM中堆的内存管理
    Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定),默认的,Edem : from : to = 8 :1 : 1 ( 可以通过参数–XX:SurvivorRatio 来设定 ), JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor。 因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
    GC堆: Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、FullGC ( 或称为 Major GC )
    1)Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法
    2)Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法
    区域是空闲着的。
    下面只列举其中的几个常用和容易掌握的配置选项
    -Xms:初始堆大小。如:-Xms256m
    -Xmx:最大堆大小。如:-Xmx512m
    -Xmn:新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%
    -Xss:JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。
    -XX:NewRatio:新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3
    -XX:SurvivorRatio:新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10
    -XX:PermSize:永久代(方法区)的初始大小
    -XX:MaxPermSize:永久代(方法区)的最大值
    -XX:+PrintGCDetails:打印 GC 信息
    -XX:+HeapDumpOnOutOfMemoryError:让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用

  7. 如果有一个环形引用,如何对其进行垃圾回收?
    答:1. 如何判定对象为垃圾对象:引用计数法,可达性分析法;2.如何回收:回收策略(标记-清除算法,复制算法,标记-整理算法,分带收集算法),垃圾回收器(serial,parnew,Cms,G1);3. 何时回收
    判定对象为垃圾的方法
    1)引用计数法:在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用技术器得值就+1,当引用失效的时候,计数器得值就-1。算法缺点:当某个引用被收集时,下个引用并不会清0,因此不被回收造成内存泄露。
    2)可达性分析法:可达性分析法就是从GCroot结点开始,看能否找到对象。GCroot结点开始向下搜索,路径称为引用链,当对象没有任何一条引用链链接的时候,就认为这个对象是垃圾,并进行回收。那么什么是GCroot呢(虚拟机在哪查找GCroot):①虚拟机栈(局部变量表),②方法区的类属性所引用的对象,③方法区中常量所引用的对象,④本地方法栈中引用的对象。目前主流JVM采用的垃圾判定算法就是可达性分析法。
    垃圾回收算法
    1)标记清除算法。存在的问题:效率问题;内存小块过多。
    2)复制算法。将Eden中需要回收的对象放到Survivor,然后清除。也就是两个Survivor中进行复制与清除。这里我们即提高了效率,又减少了内存分配。如果Survivor不够放,那就扔到老年代里,或者其他方法,有内存作担保。复制算法主要针对新生代内存收集方法。
    3)标记整理算法:标记-整理算法主要针对的是老年代内存收集方法。主要步骤:标记-整理-清除
    4)分代收集算法:分代收集算法是根据内存的分代选择不同的算法。对于新生代,一般选择复制算法。对于老年代,一般选择标记-整理-清除算法。
    垃圾回收器
    1)Serial收集器。特点:出现的最早的,发展最悠久的垃圾收集器;单线程垃圾收集器;主要针对新生代内存进行收集;缺点:慢。用处:在客户端上运行还是比较有效;没有线程的开销,所以在客户端还是比较好用的。
    2)ParNew收集器。特点:由单线程变成了多线程垃圾收集器;如果要用CMS进行收集的话,最好采用ParNew收集器。实现原理都是复制算法。缺点:性能较慢。
    3)Parallel Scavenge 收集器。主用算法:复制算法(新生代收集器);吞吐量 = (执行用户代码消耗的时间)/(执行用户代码的时间)+ 垃圾回收时所占用的时间;优点:吞吐量优化(CPU用于运行用户代码的时间与CPU消耗的总时间的比值)
    关于控制吞吐量的参数如下:
    ① -XX:MaxGCPauseMills #垃圾收集器的停顿时间
    ② -XX:GCTimeRatio #吞吐量大小
    当停顿时间过小时,内存对应变小,回收的频率增大。因此第一个参数需要设置的合理才比较好。第二个参数值越大,吞吐量越大,默认是99,(垃圾回收时间最多只能占到1%)
    4)CMS收集器(Concurrent Mark Sweep)。采用算法:标记清除算法。
    工作过程:初始标记(可达性分析法)->并发标记->重新标记(为了修正并发期间,因对象重新运作而修正)->并发清理(直接清除了)
    优点:并发收集,低停顿
    缺点:占用大量的CPU资源,无法处理浮动垃圾,出现ConcurrentMode Failure,空间碎片
    CMS是一个并发的收集器。目标是:减少延迟,增加响应速度。总的来说:客户端可用,服务端最好不用。
    5)G1收集器(面向服务端)。优势:集中了前面所有收集器的优点;G1能充分利用了多核的并行特点,能缩短停顿时间;分代收集(分成各种Region);空间整合(类似于标记清理算法);可预测的停顿()。
    步骤:初始标记->并发标记->最终标记->筛选回收
    在目前发布的Java8中,默认的虚拟机使用的是HotSpot(另一种是JRockit),对应的垃圾回收机制也就是HotSpot的GC机制;而JVM HotSpot使用的就是可达性分析法,即根搜索算法

  8. 序列化和反序列化
    答:1)序列化:把对象转换为字节序列存储于磁盘或者进行网络传输的过程称为对象的序列化。对象序列化过程可以分为两步:第一: 将对象转换为字节数组;第二: 将字节数组存储到磁盘
    2)反序列化:把磁盘或网络节点上的字节序列恢复到内存中的对象的过程称为对象的反序列化。可以是文件中的,也可以是网络传输过来的。
    3)对象的序列化和反序列化主要就是使用ObjectOutputStream 和 ObjectInputStream

Python部分

  1. 介绍Python的项目经验
    答:结合实际情况回答

  2. Python的多线程与多进程
    答:1)什么是线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务一个线程是一个execution context(执行上下文),即一个cpu执行时所需要的一串指令
    2)什么是进程:一个程序的执行实例就是一个进程。每一个进程提供执行程序所需的所有资源(进程本质上是资源的集合);一个进程有一个虚拟的地址空间、可执行的代码、操作系统的接口、安全的上下文(记录启动该进程的用户和权限等等)、唯一的进程ID、环境变量、优先级类、最小和最大的工作空间(内存空间),还要有至少一个线程;每一个进程启动时都会最先产生一个线程,即主线程,然后主线程会再创建其他的子线程
    3)进程与线程的区别:
    ①同一个进程中的线程共享同一内存空间,但是进程之间是独立的
    ②同一个进程中的所有线程的数据是共享的(进程通讯),进程之间的数据是独立的
    ③对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程
    线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。
    同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现
    创建新的线程很容易,但是创建新的进程需要对父进程做一次复制
    一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程
    线程启动速度快进程启动速度慢(但是两者运行速度没有可比性)。
    4)P

  3. 是否了解Python的协程?

  4. Python的装饰器的用法和原理

Linux部分

  1. 如何查看当前系统CPU使用率最高的线程和进程?
    答: 进程查看的命令是ps和top。进程调度的命令有at,crontab,batch,kill。
    (gdb)info threads 显示当前可调试的所有线程,每个线程会有一个GDB为其分配的ID,后面操作线程的时候会用到这个ID。 前面有*的是当前调试的线程。
    (gdb)thread ID 切换当前调试的线程为指定ID的线程。
    (gdb)thread apply ID1 ID2 command 让一个或者多个线程执行GDB命令command。
    (gdb)thread apply all command 让所有被调试线程执行GDB命令command。
    (gdb)set scheduler-locking off|on|step 估计是实际使用过多线程调试的人都可以发现,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。 off 不锁定任何线程,也就是所有线程都执行,这是默认值。 on 只有当前被调试程序会执行。 step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。
    (gdb) bt 察看所有的调用栈
    (gdb) f 3 调用框层次
    (gdb) i locals 显示所有当前调用栈的所有变量
  2. awk实现一条命令将某一个目录里的所有文件分别进行备份,后缀为.bak
  3. 简述进程的启动、终止的方式以及如何进行进程的查看.
    答:在Linux中启动一个进程有手工启动和调度启动两种方式:
    (1)手工启动:用户在输入端发出命令,直接启动一个进程的启动方式可以分为:①前台启动:直接在SHELL中输入命令进行启动。②后台启动:启动一个目前并不紧急的进程,如打印进程.
    (2)调度启动:系统管理员根据系统资源和进程占用资源的情况,事先进行调度安排,指定任务运行的时间和场合,到时候系统会自动完成该任务。经常使用的进程调度命令为:at、batch、crontab。

计算机网络部分

  1. 网络的7层协议
  • OSI分层 (7层):物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
  • TCP/IP分层(4层):网络接口层、 网际层、运输层、 应用层。
  • 五层协议 (5层):物理层、数据链路层、网络层、运输层、 应用层。
    补充知识:每一层的协议如下:
  • 物理层:RJ45、CLOCK、IEEE802.3 (中继器,集线器,网关)
  • 数据链路:PPP、FR、HDLC、VLAN、MAC (网桥,交换机)
  • 网络层:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGRP、 (路由器)
  • 传输层:TCP、UDP、SPX
  • 会话层:NFS、SQL、NETBIOS、RPC
  • 表示层:JPEG、MPEG、ASII
  • 应用层:FTP、DNS、Telnet、SMTP、HTTP、WWW、NFS
  1. TCP的4次分手过程。如果大量出现close_wait状态,可能是什么原因?
    答:1)Client发起断开连接,给Server发送FIN,进入FIN_WAIT1状态,表示Client想主动断开连接;2)Server接受到FIN字段后,会继续发送数据给Client端,并发送ACK给Client端,表明自己知道了,但是还没有准备好断开,请等我的消息;3)当Server确定自己的数据已经发送完成,就发送FIN到Client;4)Client接受到来自Server的FIN,发送ACK给Server端,表示可以断开连接了,再等待2MSL,没有收到Server端的数据后,表示可以正常断开连接。如下图所示:
    TCP的四次挥手补充知识:TCP的三次握手
    补充问题:**为什么TIME_WAIT状态还需要等2
    MSL(Max SegmentLifetime,最大分段生存期)秒之后才能返回到CLOSED状态呢?**
    答:因为虽然双方都同意关闭连接了,而且握手的4个报文也都发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SENT状态到ESTABLISH状态那样),但是我们必须假想网络是不可靠的,你无法保证你最后发送的ACK报文一定会被对方收到,就是说对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文
    补充问题:为什么要4次挥手?
    答:TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,是一个全双工模式:
    1、当主机A确认发送完数据且知道B已经接受完了,想要关闭发送数据口(当然确认信号还是可以发),就会发FIN给主机B。
    2、主机B收到A发送的FIN,表示收到了,就会发送ACK回复。
    3、但这是B可能还在发送数据,没有想要关闭数据口的意思,所以FIN与ACK不是同时发送的,而是等到B数据发送完了,才会发送FIN给主机A。
    4、A收到B发来的FIN,知道B的数据也发送完了,回复ACK, A等待2MSL以后,没有收到B传来的任何消息,知道B已经收到自己的ACK了,A就关闭链接,B也关闭链接了。
    确保数据能够完成传输。
    补充问题:如果已经建立了连接,但是客户端突然出现故障了怎么办?
    答:TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

  2. 浏览器和服务器之间如何建立连接?
    答:
    补充知识:HTTP的长连接和短连接?
    HTTP的长连接和短连接本质上是TCP长连接和短连接。HTTP属于应用层协议.

  • 短连接:浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。
  • 长连接:当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。
  • TCP短连接: client向server发起连接请求,server接到请求,然后双方建立连接。client向server发送消息,server回应client,然后一次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起 close操作。短连接一般只会在 client/server间传递一次读写操作
  • TCP长连接: client向server发起连接,server接受client连接,双方建立连接。Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。
  1. IO中同步与异步,阻塞与非阻塞区别
    答:同步和异步关注的是消息通信机制 (synchronous communication/asynchronous communication)。所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
    阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。非阻塞不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

  2. DNS进行域名解析的过程.
    答:客户端发出DNS请求翻译IP地址或主机名,DNS服务器在收到客户机的请求后:
    (1)检查DNS服务器的缓存,若查到请求的地址或名字,即向客户机发出应答信息;
    (2)若没有查到,则在数据库中查找,若查到请求的地址或名字,即向客户机发出应答信息;
    (3)若没有查到,则将请求发给根域DNS服务器,并依序从根域查找顶级域,由顶级查找二级域,二级域查找三级,直至找到要解析的地址或名字,即向客户机所在网络的DNS服务器发出应答信息,DNS服务器收到应答后先在缓存中存储,然后将解析结果发给客户机;
    (4)若没有找到,则返回错误信息。

  3. 在浏览器中输入www.163.com后执行的全部过程
    答:1、客户端浏览器通过DNS解析到www.163.com的IP地址210.11.27.79,通过这个IP地址找到客户端到服务器的路径。客户端浏览器发起一个HTTP会话到210.11.27.79,然后通过TCP进行封装数据包,输入到网络层。
    2、在客户端的传输层,把HTTP会话请求分成报文段,添加源和目的端口,如服务器使用80端口监听客户端的请求,客户端由系统随机选择一个端口如5000,与服务器进行交换,服务器把相应的请求返回给客户端的5000端口。然后使用IP层的IP地址查找目的端。
    3、客户端的网络层不用关心应用层或者传输层的东西,主要做的是通过查找路由表确定如何到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作,不作过多的描述,无非就是通过查找路由表决定通过那个路径到达服务器。
    4、客户端的链路层,包通过链路层发送到路由器,通过邻居协议查找给定IP地址的MAC地址,然后发送ARP请求查找目的地址,如果得到回应后就可以使用ARP的请求应答交换的IP数据包现在就可以传输了,然后发送IP数据包到达服务器的地址。

其他题目收集

Python部分

  • 迭代器生成器,生成器是如何实现迭代的?
  • list实现
  • import一个包时过程是怎么样的?
  • 装饰器实现
  • 菱形继承
  • 内存垃圾回收:分代回收细节
  • WSGI
  • uWSGI进程模型
  • 比较c语言和Python语言中的异步
  • epoll原理
  • Python里的eval
  • Tornado框架
  • PythonGIL锁
  • Python垃圾回收与内存泄露
  • 虚拟内存与物理内存区别

Java部分

  • 多线程安全问题
  • 并发和并行的区别
  • Java的并发安全机制(答了synchronized、ReentrantLock、CAS)
  • String、StringBuilder、StringBuffer的区别和应用场景
  • String a=“abc” String b=“abc” ,a等于b吗?常量池位于JVM的哪里?String提供了什么方法使用常量池?(intern)
  • string为什么不会变
  • collection与collections区别

数据库部分

  • MySQL B+树
  • MySQL联合索引
  • SQL的左连接、右连接等连接
  • SQL的索引数据结构
  • B+树和二叉查询树的区别,为什么要降低树的高度
  • MySQL优化(回答索引、拆分等,回答不够)

算法部分

  • 堆排序
  • 求二叉树深度
  • Top k问题
  • 二叉树的镜像
  • 如何判断链表有没有环
  • 如何用两个栈表示一个队列
  • 不用中间元素交换两个元素的方法,(答:使用异或),又问:不使用异或有什么缺点。。
  • 亿级元素top k,答:k大小小顶堆,又问:如何多线程改进
  • 递归翻转链表
  • 多个有序数组合并为一个
  • 顺时针打印数组
  • 写二分查找(
    • 写完了问为什么要left+(right-left)/2(答怕溢出)
    • int 的最大值是?
    • (right-left)/2会不会出现浮点数
    • 二分查找有什么要求(数组有序,升序降序都可以吗?)
    • 能不能写个代码让升序降序都满足)
  • leetcode 75题
  • leetcode 55
  • 快速排序的原理,手写快速排序,哪些排序是稳定的?
  • 一个数组中假如有一个数出现频率达到了百分之五十以上,怎么找到这个数?

计算机网络

  • TCP与UDP的区别
    答:从9个方面比较:
    1.连接: TCP面向连接,UDP面向非连接
    2.可靠性: TCP可靠, UDP非可靠
    3.有序性: TCP有序, UDP不保证有序
    4.速度: TCP慢,UDP快
    5.量级: TCP重量级, UDP轻量级
    6.拥塞控制或流量控制: TCP有, UDP没有
    7 TCP面向字节流,无记录边界; UDP面向报文,有记录边界
    8 TCP只能单播; UDP可以广播或组播
    9.应用场景: TCP效率低,准确性高; UDP效率高,准确性低

  • TCP 三次握手
    答:Server处于Listen状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了:1)当Client端socket执行connect连接时,首先发送SVN报文到Server,进入SVN_SENT状态,等待Server发送ACK;2)Server接受到SVN进入SVN_RCVD状态,(很短暂,一般查询不到),发送SVN+ACK给Client端;3)Client端接受到Server的ACK,发送ACK给Server,Server接收到后进入established状态,Client也进入established状态。如下图所示:
    TCP的三次握手
    补充问题:为什么TCP连接要建立三次连接
    答:为了防止失效的连接请求又传送到主机,因而产生错误如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

  • 如果一个客户端不理会服务端发来的ack,一直重发syn怎么办?(我理解为类似syn洪水攻击)

  • 拥塞控制 流量控制
    答:TCP/IP的流量控制:利用滑动窗口实现流量控制,如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。TCP为每一个连接设有一个持续计时器(persistence timer)。只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口控测报文段(携1字节的数据),那么收到这个报文段的一方就重新设置持续计时器。
    TCP拥塞控制:防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提:网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及与降低网络传输性能有关的所有因素。拥塞控制代价:需要获得网络内部流量分布的信息。在实施拥塞控制之前,还需要在结点之间交换信息和各种命令,以便选择控制的策略和实施控制。这样就产生了额外的开销。拥塞控制还需要将一些资源分配给各个用户单独使用,使得网络资源不能更好地实现共享。

  • Socket编程:raw_socket

  • http cookie具体所有相关内容

  • http传输一个二进制文件的所有过程

  • post和get的区别

操作系统

  • 编译原理相关
  • 从用户态到内核态的汇编级过程
  • 中断以及系统调用
  • 全局变量和局部变量都保存在哪儿
  • LRU(O(1)时间复杂度)
  • 进程间通信
  • 如何从用户态到内核态
  • 介绍进程跟线程,进程之间的通信

Linux部分

  • 部署项目怎么写shell脚本
  • 查看进程用什么指令(jps?)
    • jps是java进程的,非java进程用什么看?
  • Linux系统,怎么找到某个端口运行的程序的绝对地址
  • ps指令
  • 软连接和硬链接,linux 目录默认inode多少

框架

  • MVC框架

其他

  • 100个石头,每个人一次可以摸1-5个,甲先摸,问甲有没有必赢的方法?
  • 有没有读Python源码?
  • 游戏模型如何确认人身上的胶囊体是否被激光射中?
  • 网页相似性比较
  • RPC框架
  • Git常用指令
  • 服务感知(客户端如何感知服务端状态)
  • 如果地球自转速度降低一半,会怎么样?

参考资料

  • https://blog.csdn.net/diaopai5230/article/details/101210745
  • https://blog.csdn.net/u010843421/article/details/82026427
  • https://blog.csdn.net/shantf93/article/details/79775702
  • https://blog.csdn.net/lqglqglqg/article/details/48293141
  • https://blog.csdn.net/qq_37113604/article/details/80836025
  • https://www.cnblogs.com/Ferda/p/10833863.html
  • https://www.cnblogs.com/dolphin0520/p/3920373.html
  • https://www.cnblogs.com/godoforange/p/11552865.html
  • https://www.cnblogs.com/whatisfantasy/p/6440585.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章