Java面试_基础

Java代码可以实现一次编写、到处运行

JVM(Java虚拟机)是Java跨平台的关键。

在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。

同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。

一个Java文件里可以有多个类吗

  1. 一个java文件里可以有多个类,但最多只能有一个被public修饰的类;

  2. 如果这个java文件中包含public修饰的类,则这个类的名称必须和java文件名一致。

Java访问权限

private:该成员可以被该类内部成员访问;

default:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问;

protected:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问,还可以被它的子类访问;

public:该成员可以被任意包下,任意类的成员进行访问。

全局变量和局部变量的区别

成员变量:(Java中没有真正的全局变量,指成员变量)

  1. 成员变量是在类的范围里定义的变量;

  2. 成员变量有默认初始值;

  3. 未被static修饰的成员变量也叫实例变量,存储于对象所在的堆内存中,生命周期与对象相同

  4. 被static修饰的成员变量也叫类变量,它存储于方法区中,生命周期与当前类相同。

局部变量:

  1. 局部变量是在方法里定义的变量;

  2. 局部变量没有默认初始值;

  3. 局部变量存储于栈内存中,作用的范围结束,变量空间会自动的释放。

 自动装箱、自动拆箱的应用场景

自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;

自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;

如何对Integer和Double类型判断相等

 可以将Integer、Double先转为转换为相同的基本数据类型(如double),然后使用==进行比较

对面向对象的理解 

面向对象是一种更优秀的程序设计方法,它的基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计。它从现实世界中客观存在的事物出发来构造软件系统,并在系统构造中尽可能运用人类的自然思维方式,强调直接以现实世界中的事物为中心来思考,认识问题,并根据这些事物的本质特点,把它们抽象地表示为系统中的类,作为系统的基本构成单元,这使得软件系统的组件可以直接映像到客观世界,并保持客观世界中事物及其相互关系的本来面貌。

面向对象的三大特征  

面向对象的程序设计方法具有三个基本特征:封装、继承、多态

封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;

继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法; 

多态指的是父类引用指向子类对象,运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。1.编译看左边,运行看右边。2.多态的前提:继承+重写。

抽象也是面向对象的重要部分,抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是考虑部分问题。

封装的目的是什么 

封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以实现以下目的:

  • 隐藏类的实现细节;

  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问;

  • 可进行数据检查,从而有利于保证对象信息的完整性;

  • 便于修改,提高代码的可维护性。 

多态的理解  

子类是一种特殊父类,Java允许把父类的引用指向子类的对象,不需要任何类型转换,这种向上转型是系统自动完成的。编译时类型是父类,运行时类型是子类。当运行时调用引用变量方法时,总表现出子类方法行为特征。所以可能出现相同类型变量调用同一方法时表现出不同特征,这就是多态。 

多态的实现

多态实现需要继承,在调用程序时,可以根据实际传入子类型的某个实例。父类型可以三种形式,普通类,抽象类,接口。对于子类型,根据自身特征,重写父类的某些方法,实现抽象类/接口的方法。

Java单继承,不能多继承的原因

Java不用多继承,是因为多继承容易混淆。多个父类中的相同方法在子类调用或重写的时候会迷惑。虽然只能继承一个父类,但可以间接继承多个父类。

 重写与重载的区别

重载发生在同一个类中,多个方法相同,参数列表不同,则构成重载的关系。重载与方法返回值或访问修饰符无关,重载方法不能根据返回类型进行区分。

重写发生在继承或实现的关系中,方法名、形参列表必须与父类方法中的相同。子类返回值类型要小于小于等于父类中返回值类型,抛出异常小于父类方法,访问修饰符大于等于父类方法。父类访问修饰符不能为private,否则子类不能重写。

==和equals()有什么区别

==运算符:

  • 作用于基本数据类型时,是比较两个数值是否相等;

  • 作用于引用数据类型时,是比较两个对象的内存地址是否相同,即判断它们是否为同一个对象;

equals()方法:

  • 没有重写时,Object默认以 == 来实现,即比较两个对象的内存地址是否相同;

  • 进行重写后,一般会按照对象的内容来进行比较,若两个对象内容相同则认为对象相等,否则认为对象不等。

 String类

  • char charAt(int index):返回指定索引处的字符;

  • String substring(int beginIndex, int endIndex):从此字符串中截取出一部分子字符串;

  • String toUpperCase():将此字符串中所有的字符大写;

  • String toLowerCase():将此字符串中所有的字符小写;

String类由final修饰,所以不能被继承。 不能修改字符串值。

StringBuffer和StringBuilder类 

有共同的父类 AbstractStringBuilder,并且两个类的构造方法和成员方法也基本相同。不同的是,StringBuffer是线程安全的,而StringBuilder是非线程安全的,所以StringBuilder性能略高。一般情况下,要创建一个内容可变的字符串,建议优先考虑StringBuilder类。

创建String a = "abc"

JVM会先检查常量池中是否已经存有"abc",若没有则将"abc"存入常量池,否则就复用常量池中已有的"abc",将其引用赋值给变量a。

创建new String("abc") 

JVM会先使用常量池来管理字符串直接量,即将"abc"存入常量池。然后再创建一个新的String对象,这个对象会被保存在堆内存中。并且,堆中对象的数据会指向常量池中的直接量。

 接口和抽象类区别

接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务;对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。

抽象类体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。

  • 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法。

  • 接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量。

  • 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。

  • 接口里不能包含初始化块;但抽象类则完全可以包含初始化块。

  • 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。

 Java的异常机制

关于异常处理:

在Java中,处理异常的语句由try、catch、finally三部分组成。其中,try块用于包裹业务代码,catch块用于捕获并处理某个类型的异常,finally块则用于回收资源。当业务代码发生异常时,系统会创建一个异常对象,然后由JVM寻找可以处理这个异常的catch块,并将异常对象交给这个catch块处理。若业务代码打开了某项资源,则可以在finally块中关闭这项资源,因为无论是否发生异常,finally块一定会执行。

关于抛出异常:

当程序出现错误时,系统会自动抛出异常。除此以外,Java也允许程序主动抛出异常。当业务代码中,判断某项错误的条件成立时,可以使用throw关键字向外抛出异常。在这种情况下,如果当前方法不知道该如何处理这个异常,可以在方法签名上通过throws关键字声明抛出异常,则该异常将交给JVM处理。

final关键字

final关键字可以修饰类、方法、变量,以下是final修饰这3种目标时表现出的特征:

  • final类:final关键字修饰的类不可以被继承。

  • final方法:final关键字修饰的方法不可以被重写。

  • final变量:final关键字修饰的变量,一旦获得了初始值,就不可以被修改。

static关键字 

static关键字可以修饰成员变量、成员方法、初始化块、内部类,被static修饰的成员是类的成员,它属于类、不属于单个对象。以下是static修饰这4种成员时表现出的特征:

  • 类变量:被static修饰的成员变量叫类变量(静态变量)。类变量属于类,它随类的信息存储在方法区,并不随对象存储在堆中,类变量可以通过类名来访问,也可以通过对象名来访问,但建议通过类名访问它。

  • 类方法:被static修饰的成员方法叫类方法(静态方法)。类方法属于类,可以通过类名访问,也可以通过对象名访问,建议通过类名访问它。

  • 静态块:被static修饰的初始化块叫静态初始化块。静态块属于类,它在类加载的时候被隐式调用一次,之后便不会被调用了。

  • 静态内部类:被static修饰的内部类叫静态内部类。静态内部类可以包含静态成员,也可以包含非静态成员。静态内部类不能访问外部类的实例成员,只能访问外部类的静态成员。外部类的所有方法、初始化块都能访问其内部定义的静态内部类。

Java中有哪些容器(集合类) 

  • Set代表无序的,元素不可重复的集合;

  • List代表有序的,元素可以重复的集合;

  • Queue代表先进先出(FIFO)的队列;

  • Map代表具有映射关系(key-value)的集合。

  •  Java中的容器,线程安全和线程不安全的分别有哪些

        java.util包下的集合类大部分都是线程不安全HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap,这些都是线程不安全的集合类,但是它们的优点是性能好。如果需要使用线程安全的集合类,则可以使用Collections工具类提供的synchronizedXxx()方法,将这些集合类包装成线程安全的集合类

HashMap

JDK8中的HashMap,是基于数组+链表+红黑树来实现的,它的底层维护一个Node数组。当链表的存储的数据个数大于等于8的时候,不再采用链表存储,而采用了红黑树存储结构。这么做主要是在查询的时间复杂度上进行优化,链表为O(N),而红黑树一直是O(logN),可以大大的提高查找性能。

LinkedHashMap的底层原理

LinkedHashMap继承于HashMap,它在HashMap的基础上,通过维护一条双向链表,解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上,LinkedHashMap很多方法直接继承自HashMap,仅为维护双向链表重写了部分方法。  

TreeMap的底层原理 

TreeMap基于红黑树(Red-Black tree)实现。映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。TreeMap的基本操作containsKey、get、put、remove方法,它的时间复杂度是log(N)。

TreeMap包含几个重要的成员变量:root、size、comparator。其中root是红黑树的根节点。Entry节点根据根据Key排序,包含的内容是value。Entry中key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。

 ArrayList和LinkedList有什么区别

  1. ArrayList的实现是基于数组,LinkedList的实现是基于双向链表;

  2. 随机访问ArrayList要优于LinkedList

  3. 插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引;

线程安全的List 

Vector虽然线程安全,但效率低。

SynchronizedList是Collections的内部类,它所有的方法都带有同步锁,也不是性能最优的List。

CopyOnWriteArrayList是Java 1.5在java.util.concurrent包下增加的类,它采用复制底层数组的方式来实现写操作。在所有线程安全的List中,它是性能最优的方案。

ArrayList的数据结构

ArrayList的底层是用数组来实现的,默认第一次插入元素时创建大小为10的数组,超出限制时会增加50%的容量。

按数组下标访问效率高,在末尾插入元素效率高,在中间删除插入元素效率低。

TreeSet和HashSet的区别

HashSet、TreeSet中的元素都是不能重复的,并且它们都是线程不安全的,二者的区别是:

  1. HashSet中的元素可以是null,但TreeSet中的元素不能是null;

  2. HashSet不能保证元素的排列顺序,而TreeSet支持自然排序、定制排序两种排序的方式;

  3. HashSet底层是采用哈希表实现的,而TreeSet底层是采用红黑树实现的。

Java的序列化与反序列化 

序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中,对象的序列化(Serialize),是指将一个Java对象写入IO流中,对象的反序列化(Deserialize),则是指从IO流中恢复该Java对象。 

若要实现序列化,则需要使用对象流ObjectInputStream和ObjectOutputStream。其中,在序列化时需要调用ObjectOutputStream对象的writeObject()方法,以输出对象序列。在反序列化时需要调用ObjectInputStream对象的readObject()方法,将对象序列恢复为对象。 

创建线程有哪几种方式 

创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。

通过继承Thread类来创建并启动线程的步骤如下(不建议使用):

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。

  2. 创建Thread子类的实例,即创建了线程对象。

  3. 调用线程对象的start()方法来启动该线程。

通过实现Runnable接口来创建并启动线程的步骤如下:

  1. 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。

  2. 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。

  3. 调用线程对象的start()方法来启动该线程。

通过实现Callable接口来创建并启动线程的步骤如下:

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。

  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。

  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

线程的生命周期 

在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。

当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务。当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。当发生如下情况时,线程将会进入阻塞状态:

  • 线程调用sleep()方法主动放弃所占用的处理器资源。

  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。

  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。

  • 线程在等待某个通知(notify)。

  • 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。

实现线程同步

  1. 同步方法

    即有synchronized关键字修饰的方法,由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意, synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

  2. 同步代码块

    即有synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。需值得注意的是,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

  3. volatile

    volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

Java多线程之间的通信方式 

  1. wait()、notify()、notifyAll()

    如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作。并且因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。另外,这三个方法都是本地方法,并且被final修饰,无法被重写。

    wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

  2. await()、signal()、signalAll()

    如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信。这三个方法都是Condition接口中的方法,该接口是在Java 1.5中出现的,它用来替代传统的wait+notify实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作。

sleep()和wait()的区别 

  1. sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;

  2. sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;

  3. sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。

notify()、notifyAll()的区别 

  • notify()

    用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

  • notifyAll()

    用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

如何实现子线程先执行,主线程再执行 

启动子线程后,立即调用该线程的join()方法,则主线程必须等待子线程执行完成后再执行。

Thread类提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。

阻塞线程的方式有哪些 

当发生如下情况时,线程将会进入阻塞状态:

  • 线程调用sleep()方法主动放弃所占用的处理器资源;

  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;

  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;

  • 线程在等待某个通知(notify);

  • 程序调用了线程的suspend()方法将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免使用该方法。

 乐观锁和悲观锁的区别 

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中悲观锁是通过synchronized关键字或Lock接口来实现的。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相对于对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。

公平锁与非公平锁是怎么实现的

在Java中实现锁的方式有两种,一种是使用Java自带的关键字synchronized对相应的类或者方法以及代码块进行加锁,另一种是ReentrantLock,前者只能是非公平锁,而后者是默认非公平但可实现公平的一把锁。 

线程池 

系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。

与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法。

线程池的工作流程 

JVM

JVM 主要由四大部分组成:ClassLoader(类加载器),Runtime Data Area(运行时数据区,内存分区),Execution Engine(执行引擎),Native Interface(本地库接口)

Java程序运行流程 

Java 源代码文件经过 Java 编译器编译成字节码文件后,通过类加载器加载到内存中,才能被实例化,然后到 Java 虚拟机中解释执行,最后通过操作系统操作 CPU 执行获取结果。

对象的实例化过程

对象实例化过程,就是执行类构造函数对应在字节码文件中的<init>()方法(实例构造器),<init>()方法由非静态变量、非静态代码块以及对应的构造器组成。

  • <init>()方法可以重载多个,类有几个构造器就有几个<init>()方法;

  • <init>()方法中的代码执行顺序为:父类变量初始化、父类代码块、父类构造器、子类变量初始化、子类代码块、子类构造器。

静态变量、静态代码块、普通变量、普通代码块、构造器的执行顺序如下图:

具有父类的子类的实例化顺序如下: 

Java的垃圾回收机制 

一、哪些内存需要回收

在Java内存运行时区域的各个部分中,堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理,我们平时所说的内存分配与回收也仅仅特指这一部分内存。

二、怎么定义垃圾

引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,例如单纯的引用计数就很难解决对象之间相互循环引用的问题。

可达性分析算法

当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。 

什么是内存泄漏,怎么解决 

内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象已经不再需要,但由于长生命周期对象持有它的引用而导致不能被回收。以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。

  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

避免内存泄漏的几点建议:

  1. 尽早释放无用对象的引用。

  2. 避免在循环中创建对象。

  3. 使用字符串处理时避免使用String,应使用StringBuffer。

  4. 尽量少使用静态变量,因为静态变量存放在永久代,基本不参与垃圾回收。

什么是内存溢出,怎么解决 

内存溢出(out of memory):简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。

引起内存溢出的原因有很多种,常见的有以下几种:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

  3. 代码中存在死循环或循环产生过多重复的对象实体;

  4. 使用的第三方软件中的BUG;

  5. 启动参数内存值设定的过小。

内存溢出的解决方案:

  • 第一步,修改JVM启动参数,直接增加内存。

  • 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

  • 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

  • 第四步,使用内存查看工具动态查看内存使用情况。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以为您提供一些Java基础面试题,希望能帮助您准备面试。 1. Java的访问修饰符有哪些?它们分别代表什么含义? 答:Java的访问修饰符有四种,分别是public、private、protected和默认访问修饰符。它们分别代表以下含义: - public:公共的,可以被任何其他类访问; - private:私有的,只能在本类被访问; - protected:受保护的,可以被同一包的其他类和不同包的子类访问; - 默认访问修饰符:没有关键字修饰,只能在同一包被访问。 2. Java的异常处理机制是什么? 答:Java的异常处理机制是通过try-catch-finally语句块来实现的。在try块编写可能引起异常的代码,在catch块捕获异常并进行处理,在finally块编写必须执行的代码。如果在try块发生异常,程序会跳转到catch块进行异常处理,如果catch块没有捕获到异常,异常会继续向上抛出,直到被处理或者导致程序崩溃。 3. Java的多态是什么? 答:Java的多态是指同一个方法可以在不同的对象表现出不同的行为。多态可以通过继承、接口实现和方法重载等方式实现。 4. Java的线程是什么?如何创建线程? 答:Java的线程是指程序独立运行的子任务,可以并发执行。Java可以通过继承Thread类或者实现Runnable接口来创建线程。具体实现方式可以参考以下代码: - 继承Thread类: ``` class MyThread extends Thread { public void run() { // 线程执行的代码 } } // 创建线程示例 MyThread myThread = new MyThread(); myThread.start(); // 启动线程 ``` - 实现Runnable接口: ``` class MyRunnable implements Runnable { public void run() { // 线程执行的代码 } } // 创建线程示例 MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); // 启动线程 ``` 5. Java的反射是什么? 答:Java的反射是指程序在运行时动态获取类信息、调用对象方法、访问或修改对象属性的能力。Java的反射可以通过Class类和java.lang.reflect包的相关类实现。反射可以使得程序更加灵活,但是也会带来一定的性能损耗。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值