Java常见问题

a 夏天 提交于 2021-02-04 02:37:53

记录Java中的常见概念和原理

参考:

面对对象的三个特点

  • 封装:封装就是隐藏对象的属性和实现细节,仅对外公开接口,形成一个有机的整体
  • 多态:多态同一个行为具有多个不同表现形式或形态的能力。是指一个类实例(对象)的相同方法在不同情形有不同表现形式。
  • 继承:继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法

多态的使用和原理实现

虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。 多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。

  1. 先从操作栈中找到对象的实际类型 class;
  2. 找到 class 中与被调用方法签名相同的方法,如果有访问权限就返回这个方法的直接引用,如果没有访问权限就报错 java.lang.IllegalAccessError ;
  3. 如果第 2 步找不到相符的方法,就去搜索 class 的父类,按照继承关系自下而上依次执行第 2 步的操作;
  4. 如果第 3 步找不到相符的方法,就报错 java.lang.AbstractMethodError ; 如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。

###内存管理

  • JVM内存划分
    • 方法区(线程共享):常量、静态变量、JIT(即时编译器) 编译后的代码也都在方法区;
    • 堆内存(线程共享):存放Java实例对象,垃圾回收的主要场所;
    • 虚拟机栈(栈内存):保存局部变量表、操作数栈、动态链接、方法出口信息
    • 本地方法栈 :为 JVM 提供使用 native 方法的服务
    • 程序计数器: 当前线程执行的字节码的位置指示器,下一条命令是分支、循环、还是顺序等等

垃圾回收算法

  • 标记清除算法:”算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
    • 缺点:效率不高;产生内存碎片
  • 复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
    • 缺点:不适用于 长期存活的对象;将内存缩小为原来的一半
  • 标记整理算法:同标记清除算法,先标记,然后将存活的对象移动向一端,再将边界以外的内存回收
  • 分代收集算法:将内存对象分为新生代和老生代,新生代频繁创建销毁,多采用复制算法;老生代存活时间长,占用空间大,使用标记整理算法。
  • 如何判断对象存活:可达性分析、引用计数法

垃圾回收器

  • 大致分类
    • 新生代收集器:Serial、ParNew、Parallel Scavenge
    • 老年代收集器:CMS、Serial Old、Parallel Old
    • 整堆收集器: G1
  • 相关概念
    • 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
    • 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
    • 吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。
  • Serial
    • 新生代:采用复制法,单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
    • 老生代:同样是单线程收集器,采用标记-整理算法。
  • ParNew
    • 新生代:Series的多线程版本
  • Parallel Scavenge 和 Parallel Old
    • 老生代:Parallel Old,多线程,采用标记-整理算法。
    • 新生代:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)
  • CMS收集器
    • 老生代:基于标记-清除算法实现。CMS收集器的内存回收过程是与用户线程一起并发执行的。
    • 具体步骤:
      • 初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
      • 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
      • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
      • 并发清除:对标记的对象进行清除回收。
  • G1收集器 面向服务器的垃圾回收器
    • 建立可预测的停顿时间模型:它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。
    • 与其他收集器的区别:其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。
    • G1收集器存在的问题:Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。
    • 如何解决上述问题的:采用Remembered Set来避免整堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。
    • 具体的步骤:
      • 初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)
      • 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
      • 最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)
      • 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)

Java对象的内存布局

  • Java对象的内存构成:对象头(header)、实例数据(Instance Data)、对齐填充(Padding)
    • 对象头:包含markWord(4字节)和Class对象指针(4字节)
      • markWord:包含哈希码,GC分代年龄,锁状态表示等等
      • 对象指针:通过指针能确定对象属于哪个类,如果对象是一个数组,对象头还会包含数据长度
    • 实例数据:对象实际数据,实际数据大小。实例成员部分就是成员变量的值,包括父类成员变量和本类成员变量。
    • 对齐填充:对齐可选,按8字节对齐。用于确保对象的总长度为 8 字节的整数倍。
  • 对象的创建过程
    1. 类加载检查:虚拟机在解析.class文件时,若遇到一条 new 指令,首先它会去检查常量池中是否有这个类的符号引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那么必须先执行相应的类加载过程。
    2. 为新生对象分配内存:从堆中划分一段对应大小的内存给对象,具体的分配方式有:指针碰撞和空闲列表两种。
      • 指针碰撞:内存规整(采用复制算法或标记整理算法),空闲内存中放着一个指针作为分界点指示器,所以分配内存中只需要将指针向空闲内存挪动一段距离。
      • 空闲列表:内存不规整(采用标记清除法),VM需要维护一个空闲内存列表,来分配一个足够大的内存空间给对象实例。
    3. 初始化:成员变量复制,设置对象头信息,调用对象的构造函数进行初始化
  • 对象的访问方式 所有对象的存储空间都是在堆里,但是对象的引用在栈中。即堆和栈中都有为这个对象分配空间。 Java的本地变量表中有存放着对象的引用,根据这个引用可以到java堆中找到对象的实际数据,其中就包含对象头里对象类型数据的指针,然后再根据这个指针到方法区里拿到具体的对象类型数据。

Java的四种引用类型

  • 多引用的目的:无论采用的是引用计数法还是可达性分析算法,判断对象是否可以回收都与其引用有关。在JDK1.2之前,一个对象只有引用和被引用两种状态,即只有“被引用”和“未被引用”,但是这样太过狭隘,我们希望有一种弹性的垃圾回收机制,当内存足够时,将一些对对象保留在内存中;内存比较紧张时,将这些对象回收,类似于缓存的功能,提高效率。 强到弱依次是:强引用、软引用、弱引用、虚引用
  • 强引用:通常我们通过new来创建一个新对象时返回的引用就是一个强引用,若一个对象通过一系列强引用可到达,它就是强可达的,那么它就不被回收
  • 软引用:表示一些还有用但是非必需的对象。对于软引用关联着的对象,只有在系统将要发生内存溢出的时候才会回收。因为这个特点,软引用比弱引用更加适合做缓冲对象的reference. 因为它可以尽可能地保留, 减少重建他们所需的时间和消耗。
  • 弱引用:当一个对象仅仅被weak reference指向, 而没有任何其他strong reference指向的时候, 如果GC运行, 那么这个对象就会被回收。同样是非必须对象,但是弱引用只能活到下一次垃圾回收之前,无论内存是否够,都会回收。WeakReference的一个特点是它何时被回收是不可确定的, 因为这是由GC运行的不确定性所确定的. 所以, 一般用weak reference引用的对象是有价值被cache, 而且很容易被重新被构建, 且很消耗内存的对象。(比如,只有一个强引用初始化了,但是后来又没有用到这个对象,这时编译器优化时就会把它置空,这时GC下次运行就会将它回收)
  • 虚引用:我们通过虚引用甚至无法获取到被引用的对象,虚引用存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被回收

JVM 调优

JVM 调优的主要目标是使系统具有 高吞吐 、低停顿 的特点,其优化手段应从两方面着手:Java虚拟机 和 Java应用程序。前者指根据应用程序的设计通过虚拟机参数控制虚拟机逻辑内存分区的大小以使虚拟机的内存与程序对内存的需求相得益彰;后者指优化程序算法,降低GC负担,提高GC回收成功率。

  • Jconsole 与 Visual VM:JConsole 与 Visual VM 都是JDK自带的 Java 性能分析器,可以从命令行或在 GUI shell 中运行,从而可以轻松使用 JConsole来监控 Java 应用程序性能和跟踪 Java 中的代码,其可以从JAVA_HOME/bin/这个目录下找到。
  • Jstack:JDK自带的命令行工具,可以查看某个Java进程内的线程堆栈信息,主要用于线程Dump分析。
  • Jps:jps位于jdk的bin目录下,其作用是显示当前系统的java进程情况及其id。

类加载各阶段

加载、验证、准备、解析、初始化

  • 加载
    • 通过一个类的全限定名来获取定义此类的二进制字节流
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证:连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
  • 准备:正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。
  • 解析:虚拟机将常量池内的符号引用替换为直接引用
  • 初始化:初始化阶段是执行类构造器<clinit>()方法的过程。虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。
  • 具体参考:https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E7%9A%84%E8%BF%87%E7%A8%8B.md

对象的创建

在Java中,创建一个对象常常需要经历如下几个过程:父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的实例构造器(成员变量和实例代码块,父类的构造函数) -> 子类的实例构造器(成员变量和实例代码块,子类的构造函数)。其中,类构造器<clinit>()由静态变量和静态语句块组成,而类的实例构造器<init>()类的实例变量/语句块以及其构造函数组成。

自动装箱和自动拆箱机制

Java为每种基本数据类型都提供了对应的包装器类型。所谓自动装箱机制就是自动将基本数据类型转换为包装器类型,而自动拆箱机制就是自动将包装器类型转换为基本数据类型。在JDK中,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的(xxx代表对应的基本数据类型)。

  • 何时发生:在当前场景下,你需要的是引用类型还是原生类型。若需要引用类型,但传进来的值是原生类型,则自动装箱(例如,使用equals方法时传进来原生类型的值);若需要的是原生类型,但传进来的值是引用类型,则自动拆箱(例如,使用运算符进行运算时,操作数是包装类型)。

内部类

内部类指的是在一个类的内部所定义的类,类名不需要和源文件名相同。在Java中,内部类是一个编译时的概念,一旦编译成功,内部类和外部类就会成为两个完全不同的类,共有四种类型:

  • 成员内部类:成员内部类是外围类的一个成员,是依附于外围类的,所以,只有先创建了外围类对象才能够创建内部类对象。也正是由于这个原因,成员内部类也不能含有 static 的变量和方法;
  • 静态内部类:静态内部类,就是修饰为static的内部类,该内部类对象不依赖于外部类对象,就是说我们可以直接创建内部类对象,但其只可以直接访问外部类的所有静态成员和静态方法;
  • 局部内部类:局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效;
  • 匿名内部类:定义匿名内部类的前提是,内部类必须要继承一个类或者实现接口,格式为 new 父类或者接口(){定义子类的内容(如函数等)}。也就是说,匿名内部类最终提供给我们的是一个 匿名子类的对象。 内部类的作用:
  • 间接实现多重继承
  • 内部类还可以很好的实现隐藏(一般的非内部类,是不允许有 private 与 protected 权限的,但内部类可以)

equals, hashCode, ==

  • ==:如果是基本类型,看值是否相等,否则是判断是不是指向同一个内存地址
  • equals:默认是==,但是我们一般会覆写这个方法,来判断两个对象的属性是不是相等
  • hashhCode:一个对象的 消息摘要函数,一种 压缩映射,其一般与equals()方法同时重写;若不重写hashCode方法,默认使用Object类的hashCode方法,该方法是一个本地方法,由 Object 类定义的 hashCode 方法会针对不同的对象返回不同的整数。
  1. equals与hashCode的区别
    • 一般来讲,equals 这个方法是给用户调用的,而 hashcode 方法一般用户不会去调用 ;
    • 当一个对象类型作为集合对象的元素时,那么这个对象应该拥有自己的equals()和hashCode()设计,而且要遵守前面所说的几个原则。
  2. 在HashMap中使用可变对象作为Key带来的问题 HashMap用Key的哈希值来存储和查找键值对,如果HashMap Key的哈希值在存储键值对后发生改变,那么Map可能再也查找不到这个Entry了。也就是说,在HashMap中可变对象作为Key会造成 数据丢失。因此,
    • 在HashMap中尽量使用不可变对象作为Key,比如,使用String、Integer等不可变类型用作Key是非常明智的或者使用自己定义的不可变类。
    • 如果可变对象在HashMap中被用作键,那就要小心在改变对象状态的时候,不要改变它的哈希值了,例如,可以只根据对象的标识属性生成HashCode。
  3. 重写equals但不重写HashCode会出现的问题 在使用Set时,若向其加入两个相同(equals返回为true)的对象,由于hashCode函数没有进行重写,那么这两个对象的hashCode值必然不同,它们很有可能被分散到不同的桶中,容易造成重复对象的存在。

不可变对象

  • 基本类型变量的值不可变;
  • 引用类型变量不能指向其他对象;
  • 引用类型所指向的对象的状态不可变;
  • 除了构造函数之外,不应该有其它任何函数(至少是任何public函数)修改任何成员变量;
  • 任何使成员变量获得新值的函数都应该将新的值保存在新的对象中,而保持原来的对象不被修改。

Java的序列化/反序列化机制

将实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象,序列化可以弥补不同操作系统之间的差异。其中,需要注意以下几点:

  • 需要序列化的对象必须实现Serializable接口;
  • 只有非静态字段和非transient字段进行序列化,与字段的可见性无关;(transient关键字表明该字段不需要序列化,反序列后是null)
  • 序列化/反序列化的实质上操纵的是一个对象图;

String,StringBuilder 以及 StringBuffer

关于这三个字符串类的异同之处主要关注可变不可变、安全不安全两个方面:

  • StringBuffer(同步的)和String(不可变的)都是线程安全的,StringBuilder是线程不安全的;
  • String是不可变的,StringBuilder和StringBuffer是不可变的;
  • String的连接操作的底层是由StringBuilder实现的;
  • 三者都是final的,不允许被继承;
  • StringBuilder 以及 StringBuffer 都是抽象类AbstractStringBuilder的子类,它们的接口是相同的。 不可变类String的原因
  • String主要的三个成员变量 char value[], int offset, int count均是private,final的,并且没有对应的 getter/setter;
  • String 对象一旦初始化完成,上述三个成员变量就不可修改;并且其所提供的接口任何对这些域的修改都将返回一个新对象

重载,重写,隐藏

  • 重载:类内多态,静态绑定机制(编译时已经知道具体执行哪个方法),方法同名,参数不同
  • 重写:类间多态,动态绑定机制(运行时确定),实例方法,两小两同一大(方法签名相同,子类的方法所抛出的异常、返回值的范围不大于父类的对应方法,子类的方法可见性不小于父类的对应方法)
  • 隐藏:编译期绑定,静态方法和成员变量

BIO, NIO, AIO

按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO 和 异步IO。按照POSIX标准来划分只分为两类:同步IO和异步IO。如何区分呢?首先,一个IO操作其实分成了两个步骤: 发起IO请求 和 处理IO请求。

  • 阻塞IO和非阻塞IO的区别在于发起IO请求是否阻塞:发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO;如果不阻塞,那么就是非阻塞IO。
  • 同步IO和异步IO的区别就在于处理IO请求是否阻塞:如果实际的IO请求处理由应用内线程完成,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO;如果实际的IO请求处理由操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。
  1. BIO BIO是同步阻塞IO(一个线程处理一个链接,双方阻塞等待数据准备好,因此会受到网络延迟等因素的影响),服务端通过一个独立的Acceptor线程监听客户端的连接请求,当它收到客户端的连接请求后,就为每一个客户端创建一个新的线程去处理请求,处理结束后,将处理结果返回给客户端。可以通过线程池技术改良,避免了为每个请求都创建一个新的线程造成的线程资源耗尽问题,但仍然是同步阻塞模型,解决不了根本问题。由于IO请求的处理工作是由应用内线程完成的,因此是同步的。适用于链接数目比较小的架构。
  2. NIO NIO是同步非阻塞IO(数据准备好了再通知我,因此不会受到网络延迟等因素的影响),客户端发起的每个IO连接都注册到多路复用器Selector上,同时Selector会不断轮询注册在其上的Channel,当其中的某些Channel发生读或者写事件时,这些channel就会处于就绪状态,多路复用器Selector就会将这些Channel选择出来逐个处理请求,处理结束后,将处理结果返回到客户端。NIO模型中只需要一个线程负责Selector的轮询就可以接入成千上万的客户端,这相对于BIO来说无疑是巨大的进步。由于IO请求的处理工作是由应用内线程完成的,因此是同步的。适用于链接数目多且短的架构。
    • Channe:类似于BIO中的Stream,但是Channel是非阻塞的、双向的。可以将NIO中的Channel同传统IO中的Stream来类比,但是要注意,传统IO中,Stream是单向的,比如InputStream只能进行读取操作,OutputStream只能进行写操作。而Channel是双向的,既可用来进行读操作,又可用来进行写操作。
    • Buffer:所有数据的读和写都离不开Buffer,读取的数据只能放在Buffer中。同样地,写入数据也是先写入到Buffer中。
    • Selector:Selector的作用就是轮询每个注册的Channel,一旦发现Channel有注册的读/写事件发生,便获取事件然后进行处理。
  3. AIO AIO是异步非阻塞IO (IO请求处理完成后再通知我):服务器端异步的处理客户端的连接,并且客户端的IO请求都是由操作系统先完成了,再通知应用内线程进行处理,因此更适合连接数多且连接比较长的架构,例如相册服务器。

System.gc() 与 Object.finallize()

  • System.gc()方法的作用是发出通知让垃圾回收器启动垃圾回收,但垃圾回收器不一定会真正工作,实际上调用的是Runtime.getRuntime.gc()方法。
  • Object.finallize()方法的作用为,当对象没有与GC Roots相连接的引用链时,若该对象重写了该方法并且还没有被垃圾回收器调用过,垃圾回收器将调用此方法,这也是对象垃圾回收前最后一次自我解救(重新与GC Roots连上)的方式。实际上,每个对象的finallize方法只会被垃圾回收器调用一次。可以用这个方法来进行回收前的资源释放。

函数式接口

  • 函数式接口(functional interface,也叫功能性接口),简单来说,函数式接口是只有一个抽象方法的接口,其用于支持Lamda表达式,是Lamda表达式的类型。比如,Java标准库中的java.lang.Runnable、java.util.concurrent.Callable、java.util.Comparator都是典型的函数式接口。
  • lambda表达式允许你通过表达式来代替函数式接口,lambda表达式就和方法一样,它提供了一个正常的参数列表和一个方法体(body,可以是一个表达式或一个代码块)。Lamda表达式是由函数式接口所支持的,函数式接口是只有一个抽象方法的接口,是Lamda表达式的类型。一个lambda包括三部分:
    • 一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数
    • 一个箭头符号:->
    • 方法体,可以是表达式和代码块,方法体是函数式接口里面方法的实现:如果是代码块,则必须用{}来包裹起来,且需要一个return返回值,但有个例外,若函数式接口里面方法返回值是void,则无需{}。
    • 好处:1、更加紧凑的代码,可读性更强。2、修改方法的能力。3、更好地支持多核处理。
public class TestLambda {
    // 新的使用方式
    public static void runThreadUseLambda() {
        //Runnable是一个函数接口,只包含了有个无参数的,返回void的run方法;
        //所以lambda表达式左边没有参数,右边也没有return,只是单纯的打印一句话
        new Thread(() ->System.out.println("lambda实现的线程")).start(); 
    }

    // 老的使用方式
    public static void runThreadUseInnerClass() {
        //这种方式就不多讲了,以前旧版本比较常见的做法
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("内部类实现的线程");
            }
        }).start();
    }
    public static void main(String[] args) {
        TestLambda.runThreadUseLambda();
        TestLambda.runThreadUseInnerClass();
    }
}
  • StreamAPI + Lamda Stream API更像具有Iterable的集合类,但行为和集合类又有所不同,它是对集合对象功能的增强,专注于对集合对象进行各种非常便捷、高效的聚合操作或大批量数据操作。 在Stream API中,一个流基本上代表一个元素序列,Stream API提供了丰富的操作函数来计算这些元素。以前我们在开发业务应用时,通常很多操作的实现是这样做的:我们使用循环对集合做遍历,针对集合中的元素实现各种操作,定义各种变量来实现目的,这样我们就得到了一大堆丑陋的顺序代码。如果我们使用StreamAPI做同样的事情,使用Lambda表达式和其它函数进行抽象,可以使得代码更易于理解、更为干净。有了这些抽象,还可以做一些优化,比如实现并行等,例如:
public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Stream<Integer> stream = numbers.stream();
        stream.filter((x) -> {
            return x % 2 == 0;   // 将偶数过滤出来
        }).map((x) -> {
            return x * x;     // 将偶数平方
        }).forEach(System.out::println);   // 打印结果
    }
}

双亲委派模型 与 反双亲委派模型

  • 双亲委派模型:基础类的同一加载问题 双亲委派模型工作过程为:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把加载请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器当中,只有当父加载器反馈自己无法完成这个加载请求后,子加载器才会尝试自己去加载。 使用双亲委派模型的好处是,Java类随着它的类加载器一起具备了一种带有优先级的层次关系。比如Object类,它位于rt.jar包中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是使用同一类。相反,如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果一个用户自己写了java.lang.Object类并放到程序的ClassPath中,那么系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将一片混乱。
  • 违反双亲委派模型 双亲委派模型很好地解决了各个类加载器的基础类的统一问题,越是基础的类由越上层的加载器加载。基础类之所以称为基础,就是因为他们总是作为被用户代码调用的API,但如果基础类又要回调用户代码,该怎么办?
    • 基础类回调用户类(SPI接口与SPI实现的加载问题) 前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现,常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JDBC的 SPI 接口定义包含在 javax.sql包中;而这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JDBC SPI 的 Mysql所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类,如SPI中的 javax.sql.DriverManager类需要在com.mysql.jdbc.Driver的实例基础上获得和MySQL的连接对象Connection实例。 线程上下文类加载器正好解决了这个问题。线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上已经违背了双亲委派模型的一般性原则。线程上下文类加载器在很多 SPI 的实现中都会用到。
    • OSGi(Open Service Gateway initiative) OSGi是以Java为技术平台的动态模块化规范,业界Java模块化标准,常用于模块热部署。所谓模块热部署是指将项目部署到Web容器后,我们可以将某些功能模块拿下来或放上去,并且不会对其他模块造成影响。模块化热部署的实现关键在于自定义类加载器机制的实现:每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bunble连同类加载器一起换掉以实现代码的热替换。 在OSGi中,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。
    • Tomcat 中的反双亲委派模型 WebappClassLoader内部重写了loadClass和findClass方法,实现了绕过“双亲委派”直接加载web应用内部的资源,当然可以通过在Context.xml文件中加上如下代码片段开启正统的“双亲委派”加载机制。

为什么新生代内存需要有两个Survivor区

Eden :Survivor(to,from) = 8 :2(1:1)

  • 为什么要有Survivor区 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,从而触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。由于老年代的内存空间一般是新生代的2倍,因此进行一次Full GC消耗的时间比Minor GC长得多,这样,频繁的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度。Survivor的存在意义就在于,减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
  • 为什么要有两个Survivor区 为了保证任何时候总有一个survivor是空的。 因为将eden区的存活对象复制到survivor区时,必须保证survivor区是空的,如果survivor区中已有上次复制的存活对象时,这次再复制的对象肯定和上次的内存地址是不连续的,会产生内存碎片,浪费survivor空间。 如果只有一个survivor区,第一次GC后,survivor区非空,eden区空,为了保证第二次能复制到一个空的区域,新的对象必须在survivor区中出生,而survivor区是很小的,很容易就会再次引发GC。 而如果有两个survivor区,第一次GC后,把eden区和survivor0区一起复制到survivor1区,然后清空survivor0和eden区,此时survivor1非空,survivor0和eden区为空,下一次GC时把survivor0和survivor1交换,这样就能保证向survivor区复制时始终都有一个survivor区是空的,也就能保证新对象能始终在eden区出生了。
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!