第十六章 Java
final类不能继承、重写,final方法不能重写,final属性不能变
16.1 JVM
组成
JVM内存大致分为五个区域:方法区、虚拟机栈、本地方法栈、堆、程序计数器
**程序计数器:**记录的是正在执行的虚拟机字节码指令的地址,通过改变程序计数器,java程序才能按顺序、循环、跳转等流程执行各个方法。该区域是所有区域中唯一没有定义内存溢出错误的区域。
**虚拟机栈:**java为每个方法保存状态信息的区域,这里存放的是每个方法中的局部变量、方法出口、动态链接等,著名的栈溢出错误就是在这里发生。
**本地方法栈:**java可以执行非java函数,这些函数的状态信息就保存在这个区域,因此这个区域也有可能发生栈溢出。
**堆:**一块线程共享的存放对象实例和数组的内存区域,线程安全问题的根本原因,也是整个内存区域中最大的一块。
**方法区:**存储已被加载的类信息、常量、静态变量等,著名的常量池就位于这里。
类加载机制
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
**加载:**将类的class文件读入到内存,创建一个类对象的过程,加载的方法有三种:
new的方式加载、调用类反射的方法加载、调用类加载器的加载方法加载,其中使用类加载器加载的对象不会执行其中的静态语句块。
java采用双亲委派机制来使用加载器,双亲委派就是先让父类加载器加载类,父类不行才动用子类加载器加载,这种方式可以节省子类加载器的载入时间。
类加载完成后,java会对类进行验证,检验类的内部结构是否正确,诸如数组越界这类错误就是在这里发生的,这里也是整个加载过程最费时的部分。
验证完成后,最后就是真正的初始化,java会先初始化静态部分,再初始化实例部分,在这基础上java又会优先初始化父类对象,最后才是子类对象。
整个类的加载到此就结束了。
类加载器
类的加载器也有三种:启动类加载器、扩展类加载器和系统类加载器。
启动类加载器加载java的核心类,扩展类加载jre的jar包,系统类加载器才会加载我们指定的jar包,除此之外我们也可以自定义加载器。
16.2 GC
JVM内存按回收机制可分为年轻代和老年代,年轻代分为eden区和多个幸存区,老年代则不分区。
无论是YGC还是Full GC,都会使java线程暂停,但是YGC暂停的事件极短,因此基本是针对减少Full GC的方向优化。
可以通过参数指定分配的最大堆大小、初始堆大小、年轻代大小、比值、指定并发收集器、并行收集器等等。
16.2.1 YGC
新的对象会存入eden区,当eden区满了放不下的时候,会对年轻代的内存进行垃圾回收,eden中有用的对象移到幸存区,清空eden区,称为YGC。
16.2.2 Full GC
某个对象经过多次YGC后依然存活,会移植到老年代。当老年代满了放不下的时候,就会触发FullGC,对整个内存进行一次垃圾回收。
无论是YGC还是Full GC,都会使java线程暂停,但是YGC暂停的事件极短,因此基本是针对减少Full GC的方向优化。
可以通过参数指定分配的最大堆大小、初始堆大小、年轻代大小、比值、指定并发收集器、并行收集器等等。
16.2.3 垃圾回收机制(算法原理)
引用计数法:对象每被引用一次就+1,为0时回收,速度很快但是无法识别循环引用
标记清除法:遍历所有对象,标记没被引用的,然后统一清除。缺点是效率低、清理后内存不连续。
复制清除法:将内存分为两块,其中一块写满后,遍历对象标记有用的对象复制到另一块,然后把这一块清理,这样复制的内容很少而且内存始终连续,缺点是始终需要有一块内存空出来用于复制。
标记整理法:遍历出有用的对象,将这些对象全都向一端移动,然后清理其它空间,一样能腾出连续的内存,但是移动对象的成本比复制大得多。
GC采用分代收集法:年轻代采用复制清除法,每当eden满时,就遍历出eden和幸存者1区的有用对象复制到幸存者2区,然后清空重新写起。因此无论何时一定有一个幸存者区是空的。
老年代由于有用的对象很多所以复制成本高,采用标记整理法减少复制。
16.3 其他
16.3.1 java底层 hashmap扩容怎么实现 hashtable和currenthashmap的原理
##### 第一:java底层 hashmap扩容怎么实现
答:可是当哈希表接近装满时,因为数组的扩容问题,性能较低(转移到更大的哈希表中).
Java默认的散列单元大小全部都是2的幂,初始值为16(2的4次幂)。假如16条链表中的75%链接有数据的时候,则认为加载因子达到默认的0.75。HahSet开始重新散列,也就是将原来的散列结构全部抛弃,重新开辟一个散列单元大小为32(2的5次幂)的散列结果,并重新计算各个数据的存储位置。以此类推下去.....
负载(加载)因子:0.75.-->hash表提供的空间是16 也就是说当到达12的时候就扩容
##### 第二:hashtable和currenthashmap的原理
答:HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下,HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
###### hashtable实现:
底层数组+链表实现,无论key还是value都**不能为null**,线程**安全**,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
初始size为**11**,扩容:newsize = olesize*2+1
计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
###### Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。concurrentHashMap的原理:
底层采用分段的数组+链表实现,线程**安全**
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
###### concurrentMap和hashtable比较:
ConcurrentHashMap是使用了锁分段技术来保证线程安全的。
**锁分段技术**:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
16.3.2 nio和bio的区别 为啥nio好
同步阻塞IO(JAVA BIO/Blocking IO ): 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销.
Java NIO(Non-Blocking IO ) : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO的优点在于首先基于缓存读写文件,能够批量操作,然后用channel双向读写数据,减少每次打开断开流的资源消耗。引入selecore的概念,用一个线程管理多个通道,大大减少线程开销。
Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。另外NIO的非阻塞,需要一直轮询,也是一个比较耗资源的。所以出现AIO
16.3.3 网络如何通信
这个问题回答起来比较复杂.设计到硬件和软件的相关知识.
所有的一切,都依赖于一套网络协议,协议是通信双方约定好的通信法则.计算机也要遵循协议,来实现计算机的通信.计算机的协议从低到高分成多层,在底层,两台计算机只能通过0或1的二进制信号通话.信息在向高层协议翻译的过程,信息越来越容易被人理解.
从底层到高层依次是物理层,数据链路层,网络层,传输层,会话层,表示层,应用层.在每一层上都有主要的协议.我们通过这些协议就可以实现通信.比如:
我们常见的IP协议在网络层,TCP,UDP在传输层,http,ftp,smtp等在应用层.应用层是离我们最近的层,实现与其它计算机进行通讯的一个应用,它是对应应用程序的通信服务的.
16.3.4 threadlocal原理
ThreadLocal就是一种以**空间换时间**的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了
4.1实际通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
4.2.为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量;
4.3.在进行get之前,必须先set,否则会报空指针异常;因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。
4.4 如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
16.3.5 arrayList和LinkedList的区别
Arraylist:底层是基于动态数组,根据下表随机访问数组元素的效率高,向数组尾部添加元素的效率高;但是,删除数组中的数据以及向数组中间添加数据效率低,因为需要移动数组。
Linkedlist基于链表的动态数组,数据添加删除效率高,只需要改变指针指向即可,但是访问数据的平均效率低,需要对链表进行遍历。
总结:对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
16.3.6 单利模式是什么,线程安全吗
单例是java中一种典型的设计模式,定义在功能实现时一个类只能有一个对象,建立一个全局的访问点提供出去供大家使用.也就是说通过单例我们可以实现数据的全局访问,还可以再全局实现功能的调用.单例分成懒汉式和饿汉式,对于懒汉式会有线程安全问题,需要进行同步处理,对于饿汉式不会有线程安全问题,不需要同步.
16.3.7 Vector是什么,线程安全吗?一直是安全的吗
Vector 可实现自动增长的对象数组。 java.util.vector提供了向量类(Vector)以实现类似动态数组的功能。 创建了一个向量类的对象后,可以往其中随意插入不同类的对象,即不需顾及类型也不需预先选定向量的容量,并可以方便地进行查找。对于预先不知或者不愿预先定义数组大小,并且需要频繁地进行查找,插入,删除工作的情况,可以考虑使用向量类。
Vector是Collection中List的一种,vector的单个操作时原子性的,也就是线程安全的。但是如果两个原子操作复合而来,这个组合的方法是非线程安全的,需要使用锁来保证线程安全。
16.3.8 Synchronized和lock说一下区别
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java接口;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.synchronized需要使用Object的wait,notify等方法实现唤醒等待,而lock通过面向对象的Condition实现.
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
7.Lock被称为显式锁,出现在jdk1.5,,synchronized称为隐式锁,出现在jdk1.0,lock的效率更高.
16.3.9 ConcurrentHashMap了解吗
首先Map是接口,一般而言concurrentHashMap是线程安全的,具体实现
在1.7采取的segment分段锁,有点类似于16个线程安全的hashtable组合成了一个concurrenthashmap,不同分段操作不需要上锁,同一个分段才需要上锁,读不上锁,写上锁。锁的粒度更加精细,而在1.8中而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,而原有的Segment的数据结构虽保留了,但是已经简化了属性,只是为了兼容旧版本.
16.3.10 说一下你们堆外内存和堆内存是如何分配的?
堆内内存(on-heap memory)完全遵守JVM虚拟机的内存管理机制,堆内内存 = 新生代+老年代+持久代,我们采用垃圾回收器(GC)统一进行内存管理,平时GC会去频繁的回收新生代的对象,也就是minor GC.然后GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描,在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。我们在jvm参数中只要使用-Xms,-Xmx等参数就可以设置堆的大小和最大值,
和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。
我们经常用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存.DirectByteBuffer类是在Java Heap外分配内存,对堆外内存的申请主要是通过成员变量unsafe来操作
16.3.11 常见的gc策略了解吗?有哪些gc策略?你们的gc策略是什么?说一下你们的gc策略的实现?
GC是分代收集算法,频繁收集Young区,较少收集Old区,基本不动Perm区 ,JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。 因此GC按照回 收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC), 普通 GC(minor GC):只针对新生代区域的GC。 全局GC(major GC or Full GC):针对年老代的GC,偶尔伴随对 新生代的GC以及对永久代的GC。
GC常用算法 1.引用计数法(了解) 2.复制算法(Copying) 3.标记清除(Mark-Sweep) 4.标记压缩(Mark-Compact) 5.标记清除压缩(Mark-Sweep-Compact)
算法没有最好的,只能找最合适的,我们使用的是分代收集算法(相对联合的应用)
年轻代(Young Gen)
年轻代特点是区域相对老年代较小,对像存活率低。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代 的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenure Gen) 老年代的特点是区域较大,对像存活率高。
这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的
混合实现。
Mark阶段的开销与存活对像的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可 以通过多核/线程利用,对并发、并行的形式提标记效率。
Sweep阶段的开销与所管理区域的大小形正相关,但Sweep“就地处决”的特点,回收的过程没有对像的移动。使其 相对其它有对像移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题。
Compact阶段的开销与存活对像的数据成开比,如上一条所描述,对于大量对像的移动是很大开销的,做为老年代 的第一选择并不合适。
基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为 例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact 算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用 Serial Old执行Full GC以达到对老年代内存的整理。
16.3.12 多线程了解吗?线程池呢?有几种线程池?线程池的构造方法的参数有哪几个?
进程process是操作系统中运行的一个任务,占有一定的内存资源;线程thread是进程中包含的一个或多个执行单元,归属于进程,多线程就是在一个进程中同时存在一个以上的线程.当一个程序需要同时完成多个任务时或者多个线程效率更高的情况下,比如下载可以使用多线程.
对于线程池:java给我们提供了Executor类,Executor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务的线程相当于消费者,并用Runnable来表示任务,Executor的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能监视等机制。
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newWorkStealingPool jdk8增加了newWorkStealingPool(int parall),增加并行处理任务的线程池,不能保证处理的顺序。
主要参数:
```
corePoolSize:线程池的大小。线程池创建之后不会立即去创建线程,而是等待线程的到来。当当前执行的线程数大于改值是,线程会加入到缓冲队列;
maximumPoolSize:线程池中创建的最大线程数;
keepAliveTime:空闲的线程多久时间后被销毁。默认情况下,改值在线程数大于corePoolSize时,对超出corePoolSize值得这些线程起作用。
unit:TimeUnit枚举类型的值,代表keepAliveTime时间单位,可以取下列值:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue:阻塞队列,用来存储等待执行的任务,决定了线程池的排队策略,有以下取值:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
threadFactory:线程工厂,是用来创建线程的。默认new Executors.DefaultThreadFactory();
handler:线程拒绝策略。当创建的线程超出maximumPoolSize,且缓冲队列已满时,新任务会拒绝,有以下取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
```
16.3.13 说一下hashmap,是如何实现的?
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,计算并返回的hashCode是用于找到Map数组的bucket位置来储存Node 对象。这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Node 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U2cNibi8-1577109373330)(./img/ajava/%E5%9B%BE1.png)]
以下是HashMap初始化 ,简单模拟数据结构**Node[] table=new Node[16]** 散列桶初始化,tableclass Node {hash;//hash值key;//键 value;//值 node next;//用于指向链表的下一层(产生冲突,用拉链法)}
以下是具体的put过程(JDK1.8版)
1、对Key求Hash值,然后再计算下标
2、如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)
3、如果碰撞了,以链表的方式链接到后面
4、如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表
5、如果节点已经存在就替换旧值
6、如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)
以下是具体get过程(考虑特殊情况如果两个键的hashcode相同,你如何获取值对象?)当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UiWwoyGN-1577109373330)(./img/ajava/%E5%9B%BE2.png)]
16.3.14 lambda架构是什么?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DpWm6oYI-1577109373330)(./img/ajava/20190103132811368.png)]
lambda架构从这点出发, 有两套解决办法, 正如图上的两条分支, 一条叫Speed Layer 顾名思义 快速的处理实时数据以供查询, 而另一条分支, 又分作两层(Batch Layer & Serving Layer) 处理那些对时效性要求不高的数据。
Speed Layer处理实时数据 代价是对计算资源要求很高, 而且逻辑复杂度也会很高, 通常采用的技术比如 Redis,Storm,Kafka,Spark Streaming等。而另外两层使用的典型技术比如MR或Spark,Hive。这条路线处理延迟比较大, 结果逻辑相对简单,往往把它的处理叫做“离线处理”, 与Speed Layer的“实时处理”相对应。这种设计被称作:Complexity Isolation(复杂度分离)。
两者其实是相辅相成的, Batch Layer会持续地吸收增量数据加以处理(比如渐变维度,增加索引,划分分区,预计算聚合值等操作), 当新增数据被Batch Layer处理完成后, 它们的分析就不再由Speed Layer处理了(交由Serving Layer处理),所以保证了Speed Layer处理的历史数据量永远不会太大,毕竟对于Speed Layer来说 “快” 是关键。
来源:CSDN
作者:potpof
链接:https://blog.csdn.net/qq_41253208/article/details/103674167