• java后端研发经典面试题总结四


    Java虚拟机
    Java内存结构,分区,每个区放置什么

    程序计数器:(线程私有)当前线程所执行的字节码的行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码的指令,以程序中分支、循环和跳转等流程的控制都离不开这个计数器的指示。

    虚拟机栈:(线程私有),每个方法在执行时都会创建一个栈桢,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。一个方法从调用到执行完成的过程,对应的栈桢在虚拟机栈的进出过程。当线程结束时,虚拟机栈中的数据会被自动的释放。

    局部变量表:基本数据类型、对象的引用、返回地址,局部变量表锁需要的内存空间是在程序编译时就已经会被确定好的。

    本地方法栈:(线程私有)虚拟机栈是为执行java方法所服务的,而本地方法栈是为了虚拟机使用到的本地方法锁服务的。

    堆区:(线程共享)java堆是被所有的线程所共享的一片区域,所有的对象的实例和数组都会在堆区尽心分配。java堆细分:新生代和老年代;也可能会划分出多个线程锁共享额分配缓冲区TLAB;

    Java堆可以在物理上不连续的内存空间中,只要逻辑上连续就可以。

    方法区:(线程共享)存储已经被虚拟机加载过的类的信息、常量、静态变量和及时编译器编译后的代码。在方法区中一个区域叫做:运行时常量池,用于存放编译后生成的字面量和符号的引用。

    堆的分代
    (1)年轻代:

    所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。
    大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当一个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当另一个Survivor区也满了的时候,从前一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。

    (2)年老代:

    在年轻代中经历了N(可配置)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

    (3)持久代:

    用于存放静态数据,如 Java Class, Method 等。持久代对垃圾回收没有显著影响。

    OOM异常的处理思路

    对象的创建方法,对象的内存的分配,对象的访问定位
    对象的创建:

    (1)第一步,当遇到一个new的指令,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,需要先执行相应的类加载过程;

    (2)第二步,根据类加载完成后确定的内存大小,为对象分配内存;

    (3)第三步,需要对分配到的内存空间都初始化为零值;

    (4)第四步,虚拟机要对对象设置一些基本信息,如对象是那个类的实例、对象的哈希码、对象的GC分代年龄信息、如何才能找到类的元数据信息等,到这里虚拟机创建对象的工作已经完成;

    (5)第五步,从程序的角度,我们还需要对对象进行初始化操作。

    对象的内存分配:

    (1)对象头:

    存储hashcode 、gc分代年龄以及一些必要的自身的运行时数据

    (2)实例数据:

    存储真实的数据信息

    (3)对齐填充:

    仅仅起到占位符的作用

    对象的访问定位:

    通过句柄池的访问,在句柄池中保存着到对象实例数据的指针以及到对象类型的数据的指针

    通过直接的指针服访问,通过引用直接指向java堆的对象的实例数据

    GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?
    标记清除法:

    就是先标记哪些对象实例不用,然后直接清除。缺点就是产生大量的内存碎片,下次若要存储一个大的对象,无法找到连续内存而又必须提前GC

    标记整理:

    也就是先标记,然后对存活的对象进行移动,全部移动到一端,然后再对其它的内存进行清理。

    复制算法:

    把内存分成相等的AB两块,每次只使用其中的一块。比如当A内存使用完后,就把A中还存活着的对象复制到另外一块内存中去(B),然后再把已经使用过的内存清理掉。优点:这样就不用考虑内存碎片的问题了。缺点:内存减半,代价略高。

    GC收集器有哪些?CMS收集器与G1收集器的特点。
    对于新生代的收集器:

    Serial单线程收集器 parnew多线程收集器 parallelSccavenge收集器

    对于老年代的收集器:

    CMS并发收集低停顿收集器 serial Old单线程收集器 parallel Old多线程收集器

    CMS收集器:

    优点:并发收集、低停顿

    缺点:

    (1)对cpu资源非常的敏感,在并发的阶段虽然不会导致用户的线程停顿,但是会由于占用一部分的线程导致应用程序变慢,总的吞吐量会降低;

    (2)无法去处理浮动垃圾;

    (3)基于“标记-清除”算法的收集器,所以会出现碎片。

    G1收集器:

    优点:

    (1)能充分利用cpu、多核的优势,使用多个cpu缩短停顿的时间;

    (2)分代收集,不要其他收集器的配合便可以独立管理整个的GC堆;

    (3)空间整合:整体基于“标记-清理”算法的实现,局部是基于“复制”算法的实现;

    (4)可以预测的停顿

    Minor GC、Full GC分别在什么时候发生?
    Minor GC:新生代GC,当jvm无法为一个新的对象分配空间时会触发

    Full GC:整个堆空间的GC

    类加载的五个过程:加载、连接、初始化。
    类的加载:将类的class文件读入内存,并创建一个叫做java.lang.Class对象,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

    这些类的class文件的来源:

    (1)从本地文件系统中加载class文件

    (2)从jar包中加载class文件,比如jdbc编程时

    (3)通过网络加载class文件

    (4)把一个java源文件动态编译,并执行加载

    连接:

    (1)验证:验证阶段用于检验被加载的类是否具有正确的内部结构,并和其他的类协调一致

    (2)准备:为类的类变量分配内存,并去设置默认的值

    (3)解析:将类的二进制数据中的符号引用替换成直接引用。

    初始化:

    主要是对类变量进行初始化。

    (1)如果该类还没有被加载和连接,则先进行加载连接

    (2)如果该类的直接父类还没有被初始化,则先初始化其直接父类

    (3)类中如果有初始化的语句则先去执行这些初始化语句。

    反射
    概念:在运行的状态中,对于任何一个类或者对象,可以知道其任意的方法和属性,这种动态地调用其属性和方法的手段叫做反射。利用的反编译的手段

    一、通过三种方式来获取Employee类型,获取类:

    (1)Class c1 = Class.forName(“Employee”);

    (2)Class c2 =Employee.class;

    (3)Employee e = new Employee(); Class c3 = e.getClass();

    二、得到class的实例:

    Object o = c1.newInstance();

    三、获取所有的属性

    Field[] fs = c.getDeclaredFields();

    四、获取所有的方法

    GetDeclareMethods();

    动态代理
    J2se
    多线程(线程锁)
    线程的状态:

    新建状态、就绪状态、运行状态、阻塞状态、死亡状态(线程状态转换图)

    多线程的创建和启动:

    (1)继承Thread类,重写类的run方法,调用对象的start方法启动

    (2)实现Runnable接口,并重写该接口的run方法,该方法同样是线程的执行体,创建runnable实现类的实例,并以此实例作为Thread类的target来创建thread对象,该thread对象才是真的线程对象。

    (3)使用Callable和Future接口创建线程。具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。

    方法3:

    // 创建MyCallable对象

    Callable myCallable = new MyCallable();

    //使用FutureTask来包装MyCallable对象

    FutureTask ft = new FutureTask(myCallable);

    //FutureTask对象作为Thread对象的target

    Thread thread = new Thread(ft);

    //线程进入到就绪状态

    thread.start();

    线程同步的方法:sychronized、lock、reentrantLock等
    synchronized修饰同步监视器:修饰可能被并发访问的共享资源充当同步监视器;

    synchronized修饰方法,同步方法的同步监视器是this,也就是调用该方法的对象;

    synchronizedd可以用来修饰方法,可以修饰代码块,但是不能修饰构造器和成员变量;

    使用lock锁对象,每次只能有一个线程对lock对象进行加锁和释放锁,线程开始访问该锁对象时必须先获得锁lock

    基本用法:

    Private final ReentrantLock lock = new ReentrantLock();

    Lock.lock();

    Try(){

    }catch(Exception e){

    }finally{}

    Lock.unlock();

    锁的等级:内置锁、对象锁、类锁、方法锁。
    内置锁:每一个java对象都可以用做一个实现同步的锁,这个锁成为内置锁。当一个线程进入同步代码块或者方法的时候会自动获得该锁,在退出同步代码块或者方法时会释放该锁。

    获得内置锁的方法:进入这个锁的保护的同步代码块或者方法

    注意:java内置锁是一个互斥锁,最多只有一个线程能够获得该锁。

    对象锁:对象锁是用于对象实例方法,或者一个对象实例上的。

    类锁:类锁用于类的静态方法或者一个类的class对象上,一个类的对象实例有多个,但是每个类只有一个class对象,即不同对象实例的对象锁是互不干扰的,每一个类都有一个类锁。类锁只是概念上的,并不是真实存在的。

    方法锁:synchronized修饰方法,同步方法的同步监视器是this,也就是调用该方法的对象;

    ThreadLocal的设计理念与作用。
    作用:

    ThreadLocal类只能去创建一个被线程访问的变量,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。

    创建ThreadLocal的方式:

    private ThreadLocal myThreadLocal = new ThreadLocal();

    我们可以看到,通过这段代码实例化了一个ThreadLocal对象。

    我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。

    虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值。

    如何为ThreadLocal对象赋值和取值:

    一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值:

    myThreadLocal.set("A thread local value”);

    可以通过下面方法读取保存在ThreadLocal变量中的值:

    String threadLocalValue = (String) myThreadLocal.get();

    get()方法返回一个Object对象,set()对象需要传入一个Object类型的参数。

    初始化该ThreadLocal变量:

    通过创建一个ThreadLocal的子类重写initialValue()方法,来为一个ThreadLocal对象指定一个初始值。

    ThreadPool用法与优势。
    优势:

    第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

    第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

    第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。

    用法:

    线程池的创建:

    new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);

    参数:corePoolSize:线程池的基本大小

    maximumPoolSize:线程池的最大大小

    runnableTaskQueue:任务队列

    keepAliveTime:线程活动保持时间

    执行方式:

    threadsPool.execute(handler);

    threadsPool.submit(handler);

    线程池的关闭:

    Shutdown和shutdownNow方法实现

    线程池的工作流程分析:

    先将任务提交的顺序为核心线程池、队列、线程池、当这三个关节都不能执行用户所提交的线程时,则抛出“无法执行的任务”。

    字节流和字符流
    (1)java中字节流处理的最基本的单位是单个字节。通常用来处理二进制数据,最基本的两个字节流类是InputStream和OutputStream,这两个类都为抽象类。

    字节流在默认的情况下是不支持缓存的。每次调用一次read方法都会请求操作系统来读取一个字节,往往会伴随一次磁盘的IO,如果要使用内存提高读取的效率,应该使用BufferedInputStream。

    (2)字符流处理的最基本的单元是unicode(码元),通常用来来处理文本数据。

    输入字符流(文件到内存):把要读取的字节序列按照指定的编码方式解码为相应的字符序列,从而可以存在内存中。

    输出字符流(内存到文件):把要写入文件的字符序列转为指定的编码方式下的字节序列,然后写入文件中。

    区别如下:

    1、字节流操作的基本单元为字节;字符流操作的基本单元为Unicode码元。

    unicode的编码范围:0x0000~0XFFFF,在这个范围的每个数字都有一个字符与之对应

    2、字节流默认不使用缓冲区;字符流使用缓冲区。

    3、字节流通常用于处理二进制数据,实际上它可以处理任意类型的数据,但它不支持直接写入或读取Unicode码元;字符流通常处理文本数据,它支持写入及读取Unicode码元。

    序列化(常见的序列化操作)
    含义:

    java序列化:将java对象转换为字节序列的过程;

    java反序列化:将字节序列恢复为java对象的过程

    序列化的目的:

    实现数据的持久化,将数据永久地保存在磁盘上,通常放在文件中;

    利用序列化实现远程的通讯,在网络上传送对象的字节序列.

    实现序列化的三种方法:

    (1)某实体类仅仅实现了serializable接口(常用)

    序列化步骤:

    步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:

    ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream(“D:\objectfile.obj”));

    步骤二:通过对象输出流的writeObject()方法写对象:

    //Hello对象的字节流将输入到文件

    out.writeObject(“Hello”);

    反序列化步骤:

    步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:

    ObjectInputStream in = new ObjectInputStream(new fileInputStream(“D:\objectfile.obj”));

    步骤二:通过对象输出流的readObject()方法读取对象:

    //将从文件中读取到字节序列转化为对象

    String obj1 = (String)in.readObject();

    (2)若实体类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。

    ObjectOutputStream调用该对象的writeObject(ObjectOutputStream out)的方法进行序列化。

    ObjectInputStream会调用该对象的readObject(ObjectInputStream in)的方法进行反序列化。

    (3)若Student类实现了Externalnalizable接口,且Student类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。

    ObjectOutputStream调用Student对象的writeExternal(ObjectOutput out))的方法进行序列化。

    ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化。

    String,StringBuffer,StringBuilder的区别,应用场景
    1)在执行的速度上:StringBuilder>StringBuffer>String

    2)String是字符串常量 StringBuffer和StringBuilder是字符串变量

    例子1:

    String s = “abcd”;

    s=s+1;

    Syos(s);

    底层执行:首先创建一个对象s,赋予abcd.然后又创建新的对象s,之前的对象并没有发生变化,利用string操作字符串时,是在不断创建新的对象,而原来的对象由于没有了引用,会被GC,这样执行的效率会很低。

    例子2:

    String str2 = “This is only a”;

    String str3 = “ simple”;

    String str4 = “ test”;

    String str1 = str2 +str3 + str4;

    同理:str2 str3 str3没有被引用,但是创建了新的对象str1,执行速度上会很慢。

    StringBuilder:线程非安全的

    StringBuffer:线程安全的

    例子3:

    StringBuffer builder = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);

    应用场景:

    A:使用要操作少量的数据时,使用String

    B:单线程操作字符串缓冲区下操作大量数据使用StringBulider

    C:多线程操作字符串缓冲区下操作大量的数据使用StringBuffer

  • 相关阅读:
    【Lilishop商城】No2-1.确定项目结构和数据结构(用户、商品、订单、促销等模块)
    flutter系列之:创建一个内嵌的navigation
    VLAN的工作原理、划分方式、配置示例
    在Android和iOS上设置手机ip详细教程
    空气扬尘远程监控物联网解决方案
    基于多模态知识图谱的多模态推理-MR-MKG
    OR-Tools求解仓库选址和钢材取料问题
    OOTD | 美式复古穿搭耳机,复古轻便的头戴式耳机推荐
    十大排序算法之——冒泡排序算法(Java实现)及思路讲解
    thinkPHP基于php的衡水游泳馆管理系统--php-计算机毕业设计
  • 原文地址:https://blog.csdn.net/Netty711/article/details/126530131