• 面试要点5


     ---Spring 的Bean的生命周期---

    在传统的Java应用中,bean的生命周期很简单。使用Java 关键字 new 进行bean 实例化,然后该 bean 就可以使用了。一旦该bean 不再被使用,则由 java 自动进行垃圾回收。

      相比之下,Spring 容器中的 bean 的生命周期就显得相对复杂多了。正确理解Spring bean 的生命周期非常重要,因为你或许要利用 Spring 提供的扩展点来自定义bean 的创建过程。图1.5展示了  bean 装载到 spring 应用上下文中的一个典型的生命周期过程。(手机拍摄的。。)

    注意图中所说:bean 在 Spring 容器中从创建到销毁经历了若干阶段,每一阶段都可以针对 Spring 如何管理 bean 进行个性化定制

    正如你所见的,在 bean 准备就绪之前, bean 工厂执行了若干启动步骤。我们对图1.5 进行详细描述:(这里是需要背下来的

    1. Spring 对 bean 进行实例化;

    2. Spring 将值和 bean 的引用注入到bean 对应的属性中;

    3. 如果 bean 实现了 BeanNameAware 接口,Spring 将 bean 的 ID 传递给 setBeanName()方法;

    4. 如果 bean 实现了  BeanFactoryAware 接口,Spring 将调用 setBeanFactory ()方法,将 BeanFactory 容器实例传入;

    5. 如果 bean 实现了 ApplicationContextAware 接口,Spring 将调用 set-ApplicationContext () 方法,将 bean 所在的应用上下文的引用传入进来;

    6. 如果 bean 实现了 BeanPostProcessor 接口,Spring 将调用他们的 post-Process-Before-Initialization () 方法;

    7. 如果 bean 实现了 InitializingBean 接口,Spring 将调用它们的 after-Properties-Set () 方法。类似的,如果 bean 使用 init -method 声明了初始化方法,该方法也会被调用;

    8. 如果 bean 实现了 BeanPostProcessor 接口, Spring 将调用它们的 post-Process-After-Initialization () 方法;

    9. 此时,bean 已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;

    10.  如果 bean 实现了 Disposable-Bean 接口,Spring 将调用它的 destory () 接口方法。同样,如果 bean 使用 destory-method 声明了销毁方法,该方法也会被调用。

     ---数组转List常见方式的对比---

    一.最常用
    通过 Arrays.asList(strArray) 方式,将数组转换List后,不能对List增删,只能查改,否则抛异常(因为Arrays.asList(strArray)返回值是java.util.Arrays类中一个私有静态内部类java.util.Arrays.ArrayList,并非java.util.ArrayList类。java.util.Arrays.ArrayList类有 set(),get(),contains()等方法,但没有添加add()或remove()方法,所以调用add()方法会报错。)。

    关键代码:

    List list = Arrays.asList(strArray);

    使用场景:Arrays.asList(strArray)方式仅能用在将数组转换为List后,不需要增删其中的值,仅作为数据源读取使用。

    二.数组转为Arrays.List后,再转java.util.ArrayList,支持增删改查
    通过ArrayList的构造器,将Arrays.asList(strArray)的返回值由java.util.Arrays.ArrayList转为java.util.ArrayList。

    关键代码:

    ArrayList<String> list = new ArrayList<String>(Arrays.asList(strArray)) ;
     

    使用场景:需要在将数组转换为List后,对List进行增删改查操作,在List的数据量不大的情况下,可以使用。

    三.通过集合工具类Collections.addAll()方法(推荐)

    通过Collections.addAll(arrayList, strArray)方式转换,根据数组的长度创建一个长度相同的List,然后通过Collections.addAll()方法,将数组中的元素转为二进制,然后添加到List中。

    关键代码:

    ArrayList< String> arrayList = new ArrayList<String>(strArray.length);
    Collections.addAll(arrayList, strArray);

    使用场景:需要在将数组转换为List后,对List进行增删改查操作,在List的数据量巨大的情况下,优先使用,可以提高操作速度。

     ---转换String三种方式比较:toString()、String.valueOf()、(String)---

     简单介绍:

    1、toString,需要保证调用这个方法的类、方法、变量不为null,否则会报空指针。

    2、String.valueOf。这个方法在使用的时候是有些特殊的。一般情况下,如果是确定类型的null传入,返回的是字符串“null”,而如果直接传入null,则会发生错误。

    3、(String) 字符串类型强转。需要保证的是类型可以转成String类型。

     总结:
    这三者的使用,个人觉得应该使用String.valueOf()的方式。这样的使用安全可靠,不会带来异常,但需要注意null;

    JVM之内存结构详解

    对于开发人员来说,如果不了解Java的JVM,那真的是很难写得一手好代码,很难查得一手好bug。同时,JVM也是面试环节的中重灾区。今天开始,《JVM详解》系列开启,带大家深入了解JVM相关知识。

    我们不能为了面试而面试,但是学习会这些核心知识你必定会成为面试与工作中“最亮的一颗星”。本系列首发于微信公众号“程序新视界”。下面,开启我们的第一篇文章《JVM之内存结构详解》。

    学习也是要讲究方式方法的,本系列学习过程中会引导大家通过《费曼学习法》来学习,同时尽量采用图文方式来进行讲解。正所谓一图胜千言。

    思考一下

    学习一项知识总该知道为什么学习吧。有人会说,这些写代码好像又用不上,貌似所有的事情JVM都替我们做好了。那就,思考一下为什么要学习JVM虚拟机结构。

    那你是否遇到这样的困惑:堆内存该设置多大?OutOfMemoryError异常到底是怎么引起的?如何进行JVM调优?JVM的垃圾回收是如何?甚至创建一个String对象,JVM都做了些什么?

    这些疑问随着学习的深入都会慢慢得到解答,而要解决这些问题的第一步,就是先了解JVM的构成。

    JVM内存结构

    java虚拟机在执行程序的过程中会将内存划分为不同的数据区域,看一下下图。

    image

    如果理解了上图,JVM的内存结构基本上掌握了一半。通过上图我们可以看到什么?外行看热闹,内行看门道。从图中可以得到如下信息。

    第一,JVM分为五个区域:虚拟机栈、本地方法栈、方法区、堆、程序计数器。PS:大家不要排斥英语,此处用英文记忆反而更容易理解。

    第二,JVM五个区中虚拟机栈、本地方法栈、程序计数器为线程私有,方法区和堆为线程共享区。图中已经用颜色区分,绿色表示“通行”,橘黄色表示停一停(需等待)。

    第三,JVM不同区域的占用内存大小不同,一般情况下堆最大,程序计数器较小。那么最大的区域会放什么?当然就是Java中最多的“对象”了。

    学习延伸:如果你记住了这张图,是不是就可以说出关于JVM的内存结构了呢?可以尝试一下,切记不用死记硬背,发挥你的想象。

    堆(Heap)

    上面已经得出结论,堆内存最大,堆是被线程共享,堆的目的就是存放对象。几乎所有的对象实例都在此分配。当然,随着优化技术的更新,某些数据也会被放在栈上等。

    枪打出头鸟,树大招风。因为堆占用内存空间最大,堆也是Java垃圾回收的主要区域(重点对象),因此也称作“GC堆”(Garbage Collected Heap)。

    关于GC的操作,我们后面章节会详细讲,但正因为GC的存在,而现代收集器基本都采用分代收集算法,堆又被细化了。

    image

    同样,对上图呈现内容汇总分析。

    第一,堆的GC操作采用分代收集算法。

    第二,堆区分了新生代和老年代;

    第三,新生代又分为:Eden空间、From Survivor(S0)空间、To Survivor(S1)空间。

    Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。也就是说堆的内存是一块块拼凑起来的。要增加堆空间时,往上“拼凑”(可扩展性)即可,但当堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

    方法区(Method Area)

    方法区与堆有很多共性:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。

    正因为如此相像,Java虚拟机规范把方法区描述为堆的一个逻辑部分,但目前实际上是与Java堆分开的(Non-Heap)。

    方法区个性化的是,它存储的是已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是回收确实是有必要的。

    image

    程序计数器(Program Counter Register)

    关于程序计数器我们已经得知:占用内存较小,现成私有。它是唯一没有OutOfMemoryError异常的区域。

    程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变计数器的值来选取下一条字节码指令。其中,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。

    Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。

    image

    因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

    如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。

    虚拟机栈(JVM Stacks)

    虚拟机栈线程私有,生命周期与线程相同。

    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

    image

    局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。

    其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

    如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

    操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

    动态链接:Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。

    方法返回:无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。

    本地方法栈(Native Method Stacks)

    本地方法栈(Native Method Stacks)与虚拟机栈作用相似,也会抛出StackOverflowError和OutOfMemoryError异常。

    区别在于虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈是为虚拟机使用到的Native方法服务。

    小结

    经过上面的讲解,想必大家已经了解到JVM内存结构的基本情况。下面对照脑图,归纳总结一下,看你能说出来多少。

    image

    JVM内存结构补充

    在上篇《JVM之内存结构详解》中有些内容我们没有讲,本篇结合垃圾回收机制来一起学习。还记得JVM中堆的结构图吗?

    image

    图中展示了堆中三个区域:Eden、From Survivor、To Survivor。从图中可以也可以看到它们的大小比例,准确来说是:8:1:1。为什么要这样设计呢,本篇文章后续会给出解答,还是根据垃圾回收的具体情况来设计的。

    还记得在设置JVM时,常用的类似-Xms和-Xmx等参数吗?对的它们就是用来说设置堆中各区域的大小的。

    image


    (图片来源于网络)

    控制参数详解:

    • -Xms设置堆的最小空间大小。
    • -Xmx设置堆的最大空间大小。
    • -Xmn堆中新生代初始及最大大小(NewSize和MaxNewSize为其细化)。
    • -XX:NewSize设置新生代最小空间大小。
    • -XX:MaxNewSize设置新生代最大空间大小。
    • -XX:PermSize设置永久代最小空间大小。
    • -XX:MaxPermSize设置永久代最大空间大小。
    • -Xss设置每个线程的堆栈大小。

    对照上面两个图,再来看这些参数是不是没有之前那么枯燥了,它们在图中都有了对应的位置。

    有没有发现没有直接设置老年代空间大小的参数?我们通过简单的计算获得。

    1. 老年代空间大小=堆空间大小-年轻代大空间大小

    对上面参数立即了,但记忆有困难?那么,以下几个助记词可能更好的帮你记忆和理解参数的含义。

    Xmx(memory maximum), Xms(memory startup), Xmn(memory nursery/new), Xss(stack size)。

    对于参数的格式可以这样理解:

    • -: 标准VM选项,VM规范的选项。
    • -X: 非标准VM选项,不保证所有VM支持。
    • -XX: 高级选项,高级特性,但属于不稳定的选项。

    GC概述

    垃圾收集(Garbage Collection)通常被称为“GC”,由虚拟机“自动化”完成垃圾回收工作。

    思考一个问题,既然GC会自动回收,开发人员为什么要学习GC和内存分配呢?为了能够配置上面的参数配置?参数配置又是为了什么?

    “当需要排查各种内存溢出,内存泄露问题时,当垃圾成为系统达到更高并发量的瓶颈时,我们就需要对GC的自动回收实施必要的监控和调节。”

    JVM中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生随线程而灭。栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理。它们的内存分配和回收都具有确定性。

    因此,GC垃圾回收主要集中在堆和方法区,在程序运行期间,这部分内存的分配和使用都是动态的。

    下面通过概念和具体的算法来了解GC垃圾回收的过程。

    如何判断对象存活

    判断对象常规有两种方法:引用计数算法和可达性分析算法(Reachability Analysis)。

    引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时计数器加1,引用释放时计数减1,当计数器为0时可以回收。

    引用计数算法实现简单,判断高效,在微软COM和Python语言等被广泛使用,但在主流的Java虚拟机中没有使用该方法,主要是因为无法解决对象相互循环引用的问题。

    可达性分析算法:基本思想是通过一系列称为“GC Root”的对象(如系统类加载器、栈中的对象、处于激活状态的线程等)作为起点,基于对象引用关系,开始向下搜索,所走过的路径称为引用链,当一个对象到GC Root没有任何引用链相连,证明对象是不可用的。

    image

    上图中中绿色部分为存活对象,灰色部分为可回收对象。虽然灰色部分内部依旧有关联,但它们到GC Root是不可达的。

    面试问题

    面试官,说说Java GC都用了哪些算法?分别应用在什么地方?

    答:复制算法、标记清除、标记整理……

    你还在单纯的死记硬背么?继续往下看,你会豁然开朗,再也不用死记硬背了。

    标记清除算法

    标记清除(Mark-Sweep)算法,包含“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

    标记清除算法是最基础的收集算法,后续的收集算法都是基于该思路并对其缺点进行改进而得到的。

    image

    主要缺点:一个是效率问题,标记和清除过程的效率都不高;另外是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

    复制算法

    复制(Copying)算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块内存用完了,就将还存活着的对象复制到另外一块上,然后清理掉前一块。

    image

    每次对半区内存回收时、内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

    缺点:将内存缩小为一半,性价比低,持续复制长生存期的对象则导致效率低下。

    JVM堆中新生代便采用复制算法。回到最初推分配结构图。

    image

    在GC回收过程中,当Eden区满时,还存活的对象会被复制到其中一个Survivor区;当回收时,会将Eden和使用的Survivor区还存活的对象,复制到另外一个Survivor区,然后对Eden和用过的Survivor区进行清理。

    如果另外一个Survivor区没有足够的内存存储时,则会进入老年代。

    这里针对哪些对象会进入老年代有这样的机制:对象每经历一次复制,年龄加1,达到晋升年龄阈值后,转移到老年代。

    在这整个过程中,由于Eden中的对象属于像浮萍一样“瞬生瞬灭”的对象,所以并不需要1:1的比例来分配内存,而是采用了8:1:1的比例来分配。

    而针对那些像“水熊虫”一样,历经多次清理依旧存活的对象,则会进入老年代,而老年的清理算法则采用下面要讲到的“标记整理算法”。

    标记整理算法

    标记整理(Mark-Compact)算法:标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

    image

    这种算法不既不用浪费50%的内存,也解决了复制算法在对象存活率较高时的效率低下问题。

    分代收集算法

    分代收集算法,基本思路:将Java的堆内存逻辑上分成两块,新生代和老年代,针对不同存活周期、不同大小的对象采取不同的垃圾回收策略。

    而在新生代中大多数对象都是瞬间对象,只有少量对象存活,复制较少对象即可完成清理,因此采用复制算法。而针对老年代中的对象,存活率较高,又没有额外的担保内存,因此采用标记整理算法。

    其实,回头看,分代收集算法就是对新生代和老年代算法从策略维度的规划而已。

    Servlet 生命周期

    过程:加载 --> 实例化 --> 服务 --> 销毁

    init():在Servlet生命周期中,init()方法只执行一次,无论有多少客户端访问,都不会重复执行。它是在服务器装入Servlet时执行的,负载初始化Servlet对象。

    service():当Servlet容器接收到一个请求时,Servlet容器会针对这个请求创建ServletRequest ServletResponse对象。然后调用service()方法。并把这两个参数传递给service()方法。service()方法通过ServletRequest对象获得请求的信息。并处理该请求。再通过ServletResponse对象生成这个请求的响应结果。

    destroy():在Servlet生命周期中,destroy()方法只会被执行一次。当Servlet对象结束生命周期时,负责释放资源。

    Servlet 工作原理

    web服务器接受到一个http请求后,web服务器会将请求移交给servlet容器

    servlet容器首先对所请求的URL进行解析并根据web.xml 配置文件找到相应的处理servlet

    同时将request、response对象传递给它,servlet通过request对象可知道客户端的请求者、请求信息以及其他的信息等

    servlet在处理完请求后会把所有需要返回的信息放入response对象中并返回到客户端

    servlet一旦处理完请求,servlet容器就会刷新response对象,并把控制权重新返回给web服务器。

     
  • 相关阅读:
    安卓第一夜 第一个应用
    为什么要学习Linux
    Android的历史与花边
    来玩Play框架07 静态文件
    来玩Play框架06 用户验证
    来玩Play框架05 数据库
    来玩Play框架04 表单
    来玩Play框架03 模板
    来玩Play框架02 响应
    来玩Play框架01 简介
  • 原文地址:https://www.cnblogs.com/java-of-hnj/p/6180338.html
Copyright © 2020-2023  润新知