Java基础面试题整理-程序员宅基地

文章目录

0. Java基础

0.1 Java类加载器

参考文献:

0.1.1 面试官:请说说你理解的类加载器

通过一个类的全限定名来获取描述此类的二进制字节在这里插入代码片流这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载在这里插入代码片器”。

0.1.2 面试官:说说有哪几种类加载器,他们的职责分别是什么,他们之前存在什么样的约定。

在这里插入图片描述

  • BootstrapClassLoader 启动类类加载器
    它用来加载<JAVA_HOME>/jre/lib路径,-Xbootclasspath参数指定的路径以<JAVA_HOME>/jre/classes中的类。BootStrapClassLoader是由c++实现的。

  • ExtClassLoader拓展类类加载器
    用来加载<JAVA_HOME>/jre/lib/ext路径以及java.ext.dirs系统变量指定的类路径下的类。

  • AppClassLoader‘ 应用程序类类加载器
    主要加载应用程序ClassPath下的类(包含jar包中的类)。它是java应用程序默认的类加载器

  • 用户自定义类加载器
    用户根据自定义需求,自由的定制加载的逻辑,继承AppClassLoader,仅仅覆盖findClass()即将继续遵守双亲委派模型。

在虚拟机启动的时候会初始化BootstrapClassLoader,然后在Launcher类中去加载ExtClassLoader、AppClassLoader,并将AppClassLoader的parent设置为ExtClassLoader,并设置线程上下文类加载器。

Launcher是JRE中用于启动程序入口main()的类

public Launcher() {
    
        Launcher.ExtClassLoader var1;
        try {
    
            //加载扩展类类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
    
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
    
            //加载应用程序类加载器,并设置parent为extClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
    
            throw new InternalError("Could not create application class loader", var9);
        }
        //设置默认的线程上下文类加载器为AppClassLoader
        Thread.currentThread().setContextClassLoader(this.loader);
        //此处删除无关代码。。。
}
0.1.3 面试官插嘴:ExtClassLoader为什么没有设置parent?

因为BootstrapClassLoader是由c++实现的,所以并不存在一个Java的类。

0.1.4 面试官:双亲委派的好处是什么呢?

双亲委派模型能保证基础类仅加载一次,不会让jvm中存在重名的类。比如String.class,每次加载都委托给父加载器,最终都是BootstrapClassLoader,都保证java核心类都是BootstrapClassLoader加载的,保证了java的安全与稳定性。

0.1.5 面试官:那自己怎么去实现一个ClassLoader呢?请举个实际的例子。

自己实现ClassLoader时只需要继承ClassLoader类,然后覆盖findClass(String name)方法即可完成一个带有双亲委派模型的类加载器。

我们看下ClassLoader#loadClass的代码

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
    
        synchronized (getClassLoadingLock(name)) {
    
            // 查看是否已经加载过该类,加载过的类会有缓存,是使用native方法实现的
            Class<?> c = findLoadedClass(name);
            if (c == null) {
    
                long t0 = System.nanoTime();
                try {
    
                    //父类不为空则先让父类加载
                    if (parent != null) {
    
                        c = parent.loadClass(name, false);
                    } else {
    
                    //父类是null就是BootstrapClassLoader,使用启动类类加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
    
                    // 父类类加载器不能加载该类
                }

                //如果父类未加载该类
                if (c == null) {
    
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //让当前类加载器加载
                    c = findClass(name);
                }
            }
            return c;
        }
    }

子类只需要实现findClass,关心从哪里加载即可。

0.1.6 面试官:为什么不继承AppClassLoader呢?

因为它和ExtClassLoader都是Launcher的静态类,都是包访问路径权限的。

0.1.7 面试官:有什么应用场景呢?
  • 代码热替换,在不重启服务器的情况下可以修改类的代码并使之生效。

省略一堆代码,请参考文献。

0.1.8 面试官插嘴:为什么需要o.getClass().getMethod(“printVersion”).invoke(o);这样通过反射获取method调用,不能先强转成Test,然后test.printVersion()吗?
Test test = (Test)o;
o.printVersion();

Test.class会隐性的被加载当前类的ClassLoader加载,当前Main方法默认的ClassLoader为AppClassLoader,而不是我们自定义的MyClassLoader。

0.1.9 面试官:会发生什么?

会抛出ClassCastException,因为一个类,就算包路径完全一致,但是加载他们的ClassLoader不一样,那么这两个类也会被认为是两个不同的类。

0.2 HashMap和LinkedHashMap底层原理

Hash也称为散列、哈希。对应的英文就是Hash。基本原理就是把任意长度的输入,通过Hash算法转出固定长度的输出。

Hash的特点:

  • 从Hash值不能反向推导出原始的数据
  • 输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值
  • 哈希算法的执行效率要高效,长的文本也能快速地计算出哈希值
  • 哈希算法的冲突概率要小

图解HashMap原理

在这里插入图片描述

图解LinkedHashMap原理

在这里插入图片描述

LinkedHashMap其实就是可以看成HashMap的基础上,多了一个双向链表来维持顺序

0.3 ArrayList 底层原理

图解ArrayList

1.String面试题

1.1 String 对象的两种创建方式?
String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false

第一种方式是在常量池中拿对象,第二种方式是直接在堆内存空间创建一个新的对象。

在这里插入图片描述

1.2 String 类型的常量池?
  • 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
  • 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。
String s1 = new String("计算机");
String s2 = s1.intern();
String s3 = "计算机";
System.out.println(s2);//计算机
System.out.println(s1 == s2);//false,因为一个是堆内存中的String对象一个是常量池中的String对象,
System.out.println(s3 == s2);//true,因为两个都是常量池中的String对象
1.3 String 字符串拼接
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象     
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

在这里插入图片描述
尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的花,可以使用 StringBuilder 或者 StringBuffer

1.4 String s1 = new String(“abc”);这句话创建了几个对象?
String s1 = new String("abc");// 堆内存的地值值
String s2 = "abc";
System.out.println(s1 == s2);// 输出false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
System.out.println(s1.equals(s2));// 输出true

先有字符串"abc"放入常量池,然后 new 了一份字符串"abc"放入Java堆(字符串常量"abc"在编译期就已经确定放入常量池,而 Java 堆上的"abc"是在运行期初始化阶段才确定),然后 Java 栈的 str1 指向Java堆上的"abc"。

2.多线程面试题

2.1 说下volatile吧,了解多少说多少?

volatile是JVM(Java Memory Model)中用于保证可见性和有序性的轻量级同步机制。
它主要是有两个作用,一是保证被修饰共享变量的可见性,也就是多个线程操作读写时,能被其他线程感知到,在读取变量时会强制将主内存中的变量值读取到自己的工作内存中,写入变量时又会强制自己的新值刷新回主内存;另外一个重要作用在于阻止指令重排序。我们所熟知的双检测单例中,instance必须要用volatile修饰,原因是new SingleTon时,一般说有三个步骤(字节码):

  • 分配一块内存
  • 在内存上初始化SingleTon对象
  • 把这块内存地址返回值赋值给 instance

但是经过编译器的优化,2、3的顺序有可能是颠倒的,也就是说可能你拿到的instance可能还没有被初始化,访问instance的成员变量就可能发生空指针异常,而volatile可以阻止这种情况的发生。

2.2 解释下Java中的线程池及其用法?

使用线程分三步骤:

  • 创建线程
  • 任务执行
  • 销毁线程

备注:创建和销毁线程都会消耗系统资源,影响性能。

线程池:
已经提前创建好线程,用的时候直接执行任务,通过阻塞队列保证线程不会结束退出。

使用:
主要涉及到三大参数:

  • 核心线程数
  • 阻塞队列
  • 线程允许的最大线程数

当然还有三个不太重要的参数:

  • 线程空闲的存活时间
  • 创建线程的工厂
  • 拒绝策略

相对线程而言,有以下几点优势:

  • 降低资源消耗,通过重复利用已经创建的线程来降低创建线程和销毁线程所造成的性能消耗。
  • 提高响应速度,当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性,线程是属于稀缺资料,如果无限制创建,不仅会消耗系统资源,还会降低系统的文档性。使用线程池可以统一分配和控制。
2.3 Java 阻塞队列?

从0到1实现自己的阻塞队列(上)
从0到1实现自己的阻塞队列(下)
多线程中那些看不见的陷阱

3. JVM面试题

在这里插入图片描述

3.1 请解释一下对象的创建过程?(半初始化)

在这里插入图片描述

  • new 申请内存空间(半初始化)
    在这里插入图片描述

  • invokespecial 初始化
    在这里插入图片描述

  • astore_1 对象和变量建立关联关系,也就是赋值
    在这里插入图片描述

3.2 加问DCL与volatile问题?(指令重排序)

在这里插入图片描述
volatile作用:

  • 保证线程可见性
  • 禁止指令重排序

注意:创建对象非原子性操作

  • thread1半初始化状态
    在这里插入图片描述
  • 发生指令重排序
    在这里插入图片描述
  • thread2使用半初始化状态的对象,异常
    在这里插入图片描述
3.3 对象在内存中的存储布局?(对象与数组的存储不同)

在这里插入图片描述

  • 对象头(markword 8个字节)—— 锁信息、HashCode、分代年龄等
  • 类型指针(class pointer 4个字节)
  • 实例数据 (instance data
  • 对齐 (padding
  • 数组长度(length 4字节)- 数组特有
3.4 对象头具体包括什么?(markword、klasspointer、synchronized锁信息)

在这里插入图片描述

  • 偏向锁
  • 自旋锁(无锁、lock-free)轻量级锁
  • 重量级锁
3.5 对象怎么定位?(直接 间接)

在这里插入图片描述

  • 句柄,效率偏低(两次访问),但是对于垃圾回收不用频繁修改t

  • 直接指针,效率高(直接访问),但是垃圾回收需要频繁修改t

3.5.1 句柄

在这里插入图片描述
如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

3.5.2 直接指针

在这里插入图片描述
如果使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址。

3.6 对象怎么分配?(栈上-线程本地-Eden-Old)

在这里插入图片描述

  • 栈上分配(Java逃逸分析)
  • old区分配
  • ThreadLocalAllocateBuffer 线程本地分配
  • Eden区
  • S1、S2区

涉及到Age(4个bit,也就是0-15)

3.7 Object o = new Object()在内存中占用多少字节?

在这里插入图片描述

3.8 JVM内存模型

在这里插入图片描述
JVM的内存空间分为3大部分:

  • 堆内存
    堆内存可以划分为新生代老年代,新生代中还可以再次划分为Eden区、From Survivor区和To Survivor区。

  • 方法区

  • 栈内存
    栈内存可以再细分为java虚拟机栈本地方法栈

3.8.1 堆内存(Heap)
  • 堆是被所有线程共享的区域,实在虚拟机启动时创建的。
  • 几乎所有的new对象都是存在heap中(请注意逃逸分析栈上分配)
  • 堆内存分为两个部分:年轻代和老年代。我们平常所说的垃圾回收,主要回收的就是堆区。更细一点划分新生代又可划分为Eden区和2个Survivor区(From Survivor和To Survivor)。

在这里插入图片描述

新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )

默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

3.8.2 方法区(Method Area)
  • 方法区用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。
  • 在JDK8之前的HotSpot JVM,区域叫做“永久代(permanent generation)”。永久代是一片连续的堆空间,在JVM启动之前通过在命令行设置参数-XX:MaxPermSize来设定永久代最大可分配的内存空间,默认大小是64M(64位JVM默认是85M)。
  • 随着JDK8的到来,JVM不再有 永久代(PermGen)。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory。

方法区或永生代相关设置

-XX:PermSize=64MB 最小尺寸,初始分配
-XX:MaxPermSize=256MB 最大允许分配尺寸,按需分配
XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled 设置垃圾不回收
默认大小
-server选项下默认MaxPermSize为64m
-client选项下默认MaxPermSize为32m

3.8.3 虚拟机栈(JVM Stack) —— 我们平常说的栈
  • java虚拟机栈是线程私有,生命周期与线程相同。创建线程的时候就会创建一个java虚拟机栈。虚拟机执行java程序的时候,每个方法都会创建一个栈帧,栈帧存放在java虚拟机栈中,通过压栈出栈的方式进行方法调用。
  • 栈帧又分为一下几个区域:局部变量表操作数栈动态连接方法出口等。
  • 平时我们所说的变量存在栈中,这句话说的不太严谨,应该说局部变量存放在java虚拟机栈的局部变量表中
  • java的8中基本类型的局部变量的值存放在虚拟机栈的局部变量表中,如果是引用型的变量,则只存储对象的引用地址。
3.8.4 本地方法栈(Native Stack)

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务

3.9 JVM内存参数设置

在这里插入图片描述

  • -Xms设置堆的最小空间大小。
  • -Xmx设置堆的最大空间大小。
  • -Xmn:设置年轻代大小
  • -XX:NewSize设置新生代最小空间大小。
  • -XX:MaxNewSize设置新生代最大空间大小。
  • -XX:PermSize设置永久代最小空间大小。
  • -XX:MaxPermSize设置永久代最大空间大小。
  • -Xss设置每个线程的堆栈大小
  • -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
  • -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
3.10 JVM中,常用的GC Root有哪些?
  • 虚拟机栈中引用的对象
  • 方法区中 静态属性引用的对象
  • 方法区中 常量引用的对象
  • JNI引用的对象

4. Java动态代理是什么?有什么作用?

Java动态代理

要说动态代理,必须先聊聊静态代理。

4.1 静态代理

假设现在项目经理有一个需求:在项目现有所有类的方法前后打印日志。
你如何在不修改已有代码的前提下,完成这个需求?

静态代理的做法:

  • 为现有的每一个类都编写一个对应的代理类,并且让它实现和目标类相同的接口
    在这里插入图片描述
  • 在创建代理对象的时候,通过构造器塞入被代理对象,然后在代理对象的方法内部调用目标对象同名方法,并在调用前后打印日志。也就是说,代理对象 = 增强代码 + 目标对象(原对象)。有了代理对象后,就不用原对象了

在这里插入图片描述

4.1.1 静态代理的缺陷

程序员要手动为每一个目标类编写对应的代理类。如果当前系统已经有成百上千个类,工作量太大了。

现在我们的努力方向是:如何少写或者不写代理类,却能完成代理功能?

4.2 对象创建

在这里插入图片描述
所谓的Class对象,是Class类的实例,而Class类是描述所有类的,比如Person类,Student类

在这里插入图片描述
可以看出,要创建一个实例,最关键的就是得到对应的Class对象

那么这里就会抛出一个问题:
能否不写代理类,而直接得到代理Class对象,然后根据它创建代理实例(反射)

代理类和目标类理应实现同一组接口。

之所以实现相同接口,是为了尽可能保证代理对象的内部结构和目标对象一致,这样我们对代理对象的操作最终都可以转移到目标对象身上,代理对象只需专注于增强代码的编写

在这里插入图片描述
但是别忘了,接口是无法创建对象的,怎么办?

4.3 动态代理

JDK提供了java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy类。

Proxy有个静态方法:getProxyClass(ClassLoader, interfaces),只要你给它传入类加载器和一组接口,它就给你返回代理Class对象。

用通俗的话说,getProxyClass()这个方法,会从你传入的接口Class中,“拷贝”类结构信息到一个新的Class对象中,但新的Class对象带有构造器,是可以创建对象的。

一旦我们明确接口,完全可以通过接口的Class对象,创建一个代理Class,通过代理Class即可创建代理对象。

在这里插入图片描述
静态代理
动态代理
所以,按我理解,Proxy.getProxyClass()这个方法的本质就是:以Class造Class

根据代理Class的构造器创建对象时,需要传入InvocationHandler。

通过构造器传入一个引用,那么必然有个成员变量去接收

代理对象的内部确实有个成员变量invocationHandler,而且代理对象的每个方法内部都会调用handler.invoke()!而且代理对象的每个方法内部都会调用handler.invoke()!

在这里插入图片描述

参照一个改进写法:
在这里插入图片描述

4.3.1 Proxy.newProxyInstance

在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/dpjcn1990/article/details/108070335

智能推荐

被阿里P8面了两个小时,技术、业务有来有回......-程序员宅基地

文章浏览阅读123次。点击关注下方公众号,架构师全套资料 都在这里0、2T架构师学习资料干货分享上一篇:痛心,京东程序员删库跑路获刑!我平时偶尔会参加其他公司的面试,主要是为了检验自己的水平和能力。今天给大家分..._干巴巴得技术怎么回复他

Linux的常用命令_使用哪条命令可以列出目录内容: 第1空-程序员宅基地

文章浏览阅读36次。Linux的常用命令_使用哪条命令可以列出目录内容: 第1空

在 Angularjs 中 ui-sref 和 $state.go 如何传递单个多个参数和将对象作为参数_单个结构sref-程序员宅基地

文章浏览阅读1.5w次。一: 如何传递单个参数首先,要在目标页面定义接受的参数: 传参,ui-sref:$state.go: 接收参数,在目标页面的controller里注入$stateParams,然后 "$stateParams.参数名" 获取二:传递多个参数其实也很简单可以在上面的单个后面直接拼1:目标页面定义需要传的传输个_单个结构sref

android-Scheme与网页跳转原生的三种方式_scheme方式-程序员宅基地

文章浏览阅读1.8w次,点赞5次,收藏15次。参考:Android业务组件化之URL Scheme使用什么是 URL Scheme?android中的scheme是一种页面内跳转协议,是一种非常好的实现机制,通过定义自己的scheme协议,可以非常方便跳转app中的各个页面;通过scheme协议,服务器可以定制化告诉App跳转那个页面,可以通过通知栏消息定制化跳转页面,可以通过H5页面跳转页面等。URL Scheme应用场..._scheme方式

R语言sunburst图(sunburst plot)可视化实战:使用sunburstR包和ggplot2包进行可视化-程序员宅基地

文章浏览阅读22次。R语言sunburst图(sunburst plot)可视化实战:使用sunburstR包和ggplot2包进行可视化

Detection论文总结(3)FA-RPN: Floating Region Proposals for Face Detection_2016 ren rpn proposals-程序员宅基地

文章浏览阅读984次。文章链接:arxiv论文目录FA-RPN: Floating Region Proposals for Face Detection引言相关工作FA-RPN: Floating Region Proposals for Face Detection本文提出了一个新的方法在人脸检测任务中生成候选区域。相比于利用特征图上的一个像素来对anchor分类,我们采用了一种基于池化的方法。然而,池化成百..._2016 ren rpn proposals

随便推点

Eclipse快捷键_eclipse 快速生成数组循环方法-程序员宅基地

文章浏览阅读763次。内容辅助键 alt + / 在想不起来代码的时候,可以用这个来做代码的自动生成 main syso sout 输出语句 创建对象 补全类名 构造方法,给变量起名字 遍历数组 快捷键: ctrl +n 新建工程、 包 、 类、和文件 ctrl + shift + f: 格式化代码 记得关输入法快捷键 ctrl + shift + o:自动导包 或者删除没有用的包 ctrl +/ 单行注释 取消单行注释 ctrl + shift + / ..._eclipse 快速生成数组循环方法

zimbra管理-程序员宅基地

文章浏览阅读577次。转载:http://yang2001.blog.51cto.com/25307/737808vim /etc/hosts---------------------------------127.0.0.1 localhost.localdomain localhost192.168.9.34 mail.myweb.com..._zimbra技巧

java 支付宝 第三方即时到账支付 接口_个人免签支付接口 - csdn博客-程序员宅基地

文章浏览阅读593次。alipay 的几个内核功能文件:AlipayFunction.Javapackage com.test.util.alipay;import java.io.FileWriter;import java.io.IOException;import java.net.MalformedURLException;import java.net.URL;import jav_个人免签支付接口 - csdn博客

前端笔记知识点整合之JavaScript(十二)缓冲公式&检测设备&Data日期-程序员宅基地

文章浏览阅读145次。前端笔记知识点整合之JavaScript(十二)缓冲公式&检测设备&Data日期 一、JavaScript缓冲公式ease原生JS没有自己的缓冲公式,但是你要自己推理的话,必须要懂一些数学和物理公式:让div用100毫秒(帧),从left100px的位置变化到left800px的位置,要求匀速:大致计算如下:..._js ease 算法

B. DS堆栈--括号匹配_处理表达式过程中需要对括号匹配进行检验,括号匹配包括三种:“(”和“)”,“[”和-程序员宅基地

文章浏览阅读491次。括号匹配,绝对AC!考虑栈内是否为空的情况!!_处理表达式过程中需要对括号匹配进行检验,括号匹配包括三种:“(”和“)”,“[”和

50音起源 for Mac(日语五十音学习软件)_50音起源电脑版如何系统-程序员宅基地

文章浏览阅读162次。50音起源 Mac版是一款日语学习软件,在50音起源软件中你还能了解日文假名与汉字的渊源,体验多种记忆学习服方式等。KanaOrigin Mac版安装教程安装包下载完成后打开,双击.pkg按照安装引导器进行安装即可!日语学习软件50音起源功能特色起源 从起源开始,将带你了解日文假名的由来,以及与汉字的渊源,帮助你更好的理解,区分这些假名。速查 速查表可以在你需要的时候,快速找到那个你想了解的假名,日期与数字。学习 融合了多种记忆模式的学习方式,根据遗忘曲线和熟练度进行强化复习。并能够配合学习_50音起源电脑版如何系统