• Java后端面试题目及答案汇总


    1、Java与C++的区别?

    Java源码会先经过编译器编译成字节码(class文件),然后由JVM中内置的解释器解释成机器码。而C++源码直径过一次编译,直接在编译的过程中链接了,形成机器码

    C++比Java执行效率快,但是Java可以利用JVM跨平台

    Java是纯面向对象的语言,所有代码都必须在勒种定义。而C++中还有面向过程的东西,比如全局变量和全局函数

    C++中有指针,Java中没有,但是有引用

    C++支持多继承,Java类都是单继承。但是继承都有传递性,同时Java中的接口是多继承,接口可以多实现

    Java 中内存的分配和回收由Java虚拟机实现。Java 中有垃圾自动回收机制,会自动清理引用数为0的对象。而在 C++ 编程时,则需要花精力考虑如何避免内存泄漏。

    C++运算符可以重载,但是Java中不可以。同时C++中支持强制自动转型,Java中不行,会出现ClassCastException(类型不匹配)。

    2、Java堆内存和栈内存的区别

    Java把内存分成两种,一种叫做栈内存,一种叫做堆内存。

    在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。

    堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

    引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要原因,实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针!

    3、Java的垃圾回收机制,什么时候会出现Full GC

    由于Java有垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。

    ps:内存泄露是指该内存空间使用完毕之后未回收

    什么情况下回导致内存泄漏?

    1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。

    2.各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。

    3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

    Java分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

    分为三代:

    ①年轻代。

    1.所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

    2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

    3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

    4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

    ②年老代。

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

    2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

    ③持久代

    用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

    不同的收集器采用的算法不一样,如复制算法,标记-整理算法,标记-复制算法,停止-复制算法。

    新生代收集器使用的收集器:Serial(复制算法)、PraNew(停止-复制算法)、Parallel Scavenge(停止-复制算法)

    老年代收集器使用的收集器:Serial Old(标记-整理算法)、Parallel Old(停止-复制算法)、CMS(标记-清理算法)。

    由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。

    GC有两种类型:Scavenge GC和Full GC。

    Scavenge GC

        一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

    Full GC

        对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。

    有如下原因可能导致Full GC:

      1.年老代(Tenured)被写满

      2.持久代(Perm)被写满 

      3.System.gc()被显示调用 

      4.上一次GC之后Heap的各域分配策略动态变化

    【参考】https://www.cnblogs.com/sunniest/p/4575144.html

     4、Java容器都有哪些?

    Java 容器分为 Collection 和 Map 两大类。具体的分类如下:

    Collection

      List(有序,可重复)    

        ArrayList,底层用Object数组实现,特点是查询效率高,增删效率低,线程不安全, 初始化长度是10,默认是16,通过定义更大的数组,将旧数组中的值复制到新数组实现扩容 newC = 1.5oldC +1
        LinkedList,底层使用双向循环链表实现,特点是查询效率低,增删效率高,线程不安全,因为线程不同步
        Vector,底层用长度可以动态增长的对象数组实现,它的相关方法用 Synchronized 进行了线程同步,所以线程安全,效率低
          Stack,栈。特点是:先进后出(FILO, First In Last Out)。继承于Vector
      Set 无序,不可重复
        HashSet,底层用HashMap实现,本质是一个简化版的HashMap,因此查询效率和增删效率都比较高。其add方法就是在map中增加一个键值对,键对象就是这个元素,值对象是PRESENT的对象
          LinkedHashSet,LinkedHashSet继承自HashSet,内部使用的是LinkHashMap。这样做的意义或者好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。
        TreeSet,底层用TreeMap实现,底层是一种二叉查找树(红黑树),需要对元素做内部排序。内部维持了一个简化版的TreeMap,并通过key来存储Set元素。使用时,要对放入的类实现                                 Comparable接口,且不能放入null
    Map
      HashMap,采用散列算法来实现,底层用哈希表来存储数据,因此要求键不能重复。线程不安全,HashMap在查找、删除、修改方面效率都非常高。允许key或value为null

                            哈希表:本质是“数组+链表”,源码中Entery[] table是HashMap的核心数组结构,称为“位桶数组”。其中Entery对象时一个单向链表,存储了四部分(hash值,key,value,next)内容。

                早期的hash值总是1,此时,每一个对象都会存储到索引为1的位置,每存储一个都会发生hash冲突,形成了一个非常长的链表,HashMap也就退化成了一个“链表”。

               如果利用相除取余算法,能使hash值均匀地分布在[0,数组长度-1]区间内,早期的HashTable就是采用这种算法,但由于用了除法,所以效率非常低。

                JDK后来改进了算法,首先约定数组的长度必须为2的整数幂,这样可以使用位运算实现取余效果,hash值 = hashcode & (数组长度-1)。而且为了获得更好的散列效果,JDK对                                         hashcode进行了两次散列处理,目标就是为了使分布的更散列,更均匀。

                扩容问题:hashMap 的位桶数组,初始大小为16.负载因子为0.75,每次扩容2倍。需要注意的是,扩容很耗时。因为扩容的本质是定义更大的数组,并将就数组中的内容逐个复制                                                    到新数组中。在JDK8中,HashMap在存储一个元素时,当对应链表长度大于8时,链表就转化为红黑树,这样就大大提高了查询效率

                                   

        LinkedHashMap,HashMap和双向链表合二为一即是LinkedHashMap。所谓LinkedHashMap,其落脚点在HashMap,因此更准确地说,它是一个将所有Entry节点链入一个双向链表的HashMap。虽然LinkedHashMap增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap 和 保持访问顺序的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的。

      HashTable,与HashMap类似,只是其中的方法添加了synchronized关键字以确保线程同步检查,线程安全,但效率较低。不允许key或value为null
      TreeMap,红黑树的典型实现。TreeMap和HashMap实现了同样的接口Map。在需要Map中Key按照自然排序时才选用TreeMap

      ConcurrentHashMap, 它在JDK1.7和1.8中略有差别

    JDK1.7中:

    由于JDK没有对HashMap做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。因此 JDK 推出了专项专用的 ConcurrentHashMap ,该类位于 java.util.concurrent 包下,专门用于解决并发问题。和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。

    原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

    由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

    ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。

    JDK1.8中:

    1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。

    那就是查询遍历链表效率太低

    其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

    其中,CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

    【CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。】

    1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

    更多请参考:https://blog.csdn.net/weixin_44460333/article/details/86770169

    [ 版权声明 ]: 本文所有权归作者本人,文中参考的部分已经做了标记! 商业用途转载请联系作者授权! 非商业用途转载,请标明本文链接及出处!
  • 相关阅读:
    是否可以继承String类
    访问控制符 public,protected,private,以及默认(default)的区别
    构造器 Constructor 是否可被 override
    重载和重写的区别
    Java 的四个基本特性(抽象、封装、继承, 多态)
    面向对象和面向过程的区别
    Layui时间选择器只选择时和分,不显示秒
    解决JavaScript:Uncaught TypeError: xx(函数名)is not a function at HTMLInputElement.onclick
    Linux(Ubuntu):搭建GitLab托管代码
    【转】【Oracle 集群】Linux下Oracle RAC集群搭建之基本测试与使用(九)
  • 原文地址:https://www.cnblogs.com/gslgb/p/14495179.html
Copyright © 2020-2023  润新知