Java性能问题一直困扰着广大程序员,由于平台复杂性,要定位问题,找出其根源确实很难。随着10多年Java平台的改进以及新出现的多核多处理器,Java软件的性能和扩展性已经今非昔比了。现代JVM持续演进,内建了更为成熟的优化技术、运行时技术和垃圾收集器。与此同时,底层的硬件平台和操作系统也在演化。
目录:
一、Java性能优化系列之一--设计优化
二、Java性能优化系列之二--程序优化
三、Java性能优化系列之三--并发程序设计详解
四、Java性能优化系列之四--Java内存管理与垃圾回收机制详解
五、Java性能优化系列之五--JavaIO
现代大规模关键性系统中的Java性能调优,是一项富有挑战的任务。你需要关注各种问题,包括算法结构、内存分配模式以及磁盘和文件I/O的使用方式。性能调优最困难的通常是找到问题所在,即便是经验丰富的人也会被他们的直觉所误导。性能杀手总是隐藏在最意想不到的地方。
这一次,我将在本文中着重介绍Java性能优化的一系列举措,希望能够对现在的你有所帮助。觉得有帮助的朋友也可以转发、收藏以下。
一、Java性能优化系列之一--设计优化
1、善于利用 Java 中的设计模式:享元模式、代理模式、装饰器模式等。详见我的上一篇文章设计模式之我说
文章链接:https://www.toutiao.com/i6540181471888409102/
2、Java 中的缓冲区:
(1)缓冲最常用的场景就是提高 IO 速度:比如 BufferedWriter 可以用来装饰 FileWriter ,为 FileWriter 加上缓冲。 BufferedOutputStream 可以用来装饰 FileOutputStream 。使用这两个装饰器时候可以指定缓冲区大小,默认的 size 为 8K 。
(2)JavaNIO 中的各种 Buffer 类族,有更加强大的缓冲区控制功能。
(3)除了性能上的优化,缓冲区也可以作为上层组件和下层组件的一种通信工具,将上层组件好下层组件进行解耦。比如生产者消费者模式中的缓冲区。
2、缓存:
(1)比如 Hibernate 采用的两级缓存:一级缓存和二级缓存。二级缓存指的是 sessionFactory层面上的缓存, Hibernate 采用的是 EHCache 。一级缓存指的是 session 层面上的缓存。
3、对象复用技术 -- 池的使用
(1)数据库连接池:较常使用的数据库连接池组件是 C3P0 和 Proxool 。其中 C3P0 是伴随 Hibernate 一起发布的, Hibernate 默认采用的数据库连接池。
(2)线程池:自定义线程池以及 jdk1.5 提供的线程池组件。
4、并行代替串行。
5、时间换空间:不引入中间变量实现两个数字的交换。代价是增加 CPU 运算。
6、空间换时间:使用下标数组排序。
二、Java性能优化系列之二--程序优化
常用的程序设计优化技巧:
1、字符串优化处理
(1)String 类的特点:不变性、针对常量池的优化( String.intern() 方法的意义)
(2)subString 方法的内存泄漏 :
(3)字符串分割和查找不要使用 split 函数,效率低,而是使用 StringTokenizer 或者 indexOf结合 subString() 函数完成分割。
(4)用 charAt ()方法代替 startWith ()方法。
(5)对于静态字符串或者变量字符串的连接操作, Java 在编译的时候会进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的字符串,而不是生成大量的 String 实例。只生成一个对象。
(6)在无需考虑线程安全情况下尽量使用 StringBuilder 。
(7)StringBuffer 和 StringBuilder 初始化的时候都可以设置一个初始值,默认是 16B 。如果字符串的长度大于 16B 的时候,则需要进行扩容。扩容策略是将原有的容量大小翻倍,以新的容量申请内存空间,建立 char 数组,然后将数组中的内容复制到这个新的数组中,使用 Arrays.copyOf() 函数。因此,如果能预先评估 StringBuilder 的大小,则可以节省这些复制操作,从而提高系统的性能。
2、List 接口
( 1 ) ArrayList 和 Vector 的区别:它们几乎使用了相同的算法,它们的唯一区别是对多线程的支持。 ArrayList 是不安全的,而 Vector 是线程安全的。
( 2 ) LinkedList 和 ArrayList 的区别:
|---1 、 linkedList 采用链表实现,适合于数据删除和插入非常频繁的情况,不适合随机访问。
|---2 、 ArrayList 采用数组实现,适用于随机查找和顺序读的情况,不适合删除和插 入数据非常频繁的场景。
(3)基于数组的 List 都会有一个容量参数。当 ArrayList 所存储的元素容量超过其已有大小的时候就会进行扩容,数组的扩容会导致整个数组进行一次内存复制。因此合理的数组大小会减小数组扩容的次数从而提高系统性能。
(4)遍历列表的时候尽量使用迭代器,速度块。
2、Map 接口:
(1)HashMap 的实现原理:简单的说, HashMap 就是将 key 做 hash 算法,然后将 hash 值映射到内存地址,直接取得 key 所对应的数据。在 HashMap 中,底层数据结构使用的是数组,所谓的内存地址指的是数组的下标索引。
(2)容量参数与扩容:默认情况下, hashmap 的初始容量为 16 ,负载因子为 0.75 ,也就是说当 hashmap 的实际容量达到了初始容量 * 负载因子( hashmap 内部维护的一个 threshold 值)的时候, hashmap 就会进行扩容。在扩容时,会遍历整个 hashmap ,因此应该设置合理的初始大小和负载因子,可以减小 hashmap 扩容的次数。
(3)LinkedHashMap-- 有序的 HashMap : HashMap 的最大缺点是其无序性,被存入到 Hashmap 中的元素,在遍历 HashMap 的时候,其输出不一定按照输入的顺序,而是 HashMap 会根据 hash 算法设定一个查找高效的顺序。如果希望保存输入顺序,则需要使用 LinkedHashMap 。LinkedHashmap 在内部又增加了一个链表,用于保存元素的顺序。
(4)LinkedList 可以提供两种类型的顺序:一个是元素插入时候的顺序,一个是最近访问的顺序。注意: LinkedHashMap 在迭代过程中,如果设置为按照最后访问时间进行排序,即:每当使用 get() 方法访问某个元素时,该元素便会移动到链表的尾端。但是这个时候会出现异常,因此, LinkedHashMap 工作在这种模式的时候,不能在迭代器中使用 get() 操作。
(5)关于 ConcurrentModificationException :该异常一般会在集合迭代过程中被修改时抛出。因此,不要在迭代器模式中修改集合的结构。这个特性适合于所有的集合类,包括 HashMap 、 Vector 、 ArrayList 等。
(6)TreeMap-- 如果要对元素进行排序,则使用 TreeMap 对 key 实现自定义排序,有两种方式:在 TreeMap 的构造函数中注入一个 Comparator 或者使用一个实现了 Comparable 的 key 。
(7)如果需要将排序功能加入 HashMap ,最好是使用 Treemap 而不是在应用程序自定义排序。
(8)HashMap 基于 Hash 表实现, TreeMap 基于红黑树实现。
3 、 Map 和 Set 的关系:
( 1 )所有 Set 的实现都只是对应的 Map 的一种封装,其内部维护一个 Map 对象。即: Set只是相应的 Map 的 Value 是一种特殊的表现形式的一种特例。
( 2 ) Set 主要有三种实现类: HashSet 、 LinkedHashSet 、 TreeSet 。其中 HashSet 是基于 Hash 的快速元素插入,元素之间无序。 LinkedHashSet 同时维护着元素插入顺序,遍历集合的时候,总是按照先进先出的顺序排序。 TreeSet 是基于红黑树的实现,有着高效的基于元素 Key 的排序算法。
4 、优化集合访问代码:
( 1 )、分离循环中被重复调用的代码:例如, for 循环中使用集合的 size() 函数,则不应该把这个函数的调用放到循环中,而是放到循环外边、
( 2 )、省略相同的操作:
5 、 RandomAccess 接口:通过 RandomAccess 可知道 List 是否支持随机快速访问。同时,如果应用程序需要通过索引下标对 List 做随机访问,尽量 buyaoshiyongLinkedList , ArrayList 或者 Vector 可以。
6 、 JavaNIO 的特性:
1 、为所有的原始类型提供 Buffer 支持。
2 、使用 Java.nio.charset.Charset 作为字符编码解码解决方案。
3 、增加通道抽象代替原有的 IO 流抽象。
4 、支持锁和内存映射文件的文件访问接口。
5 、提供基于 Selector 的异步网络 IO 。
7 、 Java 中 NIO 的使用。 Channel 是一个双向通道,即可读也可写。应用程序不能直接操作 Channel ,必须借助于 Buffer 。例如读数据的时候,必须把数据从通道读入到缓冲区,然后在缓冲区中进行读取。以文件读取为例,首先通过文件输入流获得文件通道,然后把文件通道的内容读入到缓冲区中,然后就可以对缓冲区操作。
8 、 Buffer 的基本原理:
1 、 Buffer 的创建: Buffer 的静态 allocate(int size) 方法或者 Buffer.wrap(byte[]src) 。
2 、 Buffer 的工作原理:三个变量: position ,代表当前缓冲区的位置,写缓冲区的时候,将从 position 的下一个位置写数据。 Capacity ,代表缓冲区的总容量上限。 Limit ,缓冲区的实际上限,也就是说,读数据的时候,数据即是从 position 到 limit 之间的数据
3 、 flip 操作: limit=position,position=0, 一般是在读写切换的时候使用。写完数据之后,需要限定下有效数据范围,才能读数据;
4 、 clear 操作: position-0 , limit=capacity. 。为重新写入缓冲区做准备。
5 、 rewind 操作: position=0 ,为读取缓冲区中有效数据做准备,一半 limit 已经被合理设置。
9 、读写缓冲区:
1 、 public byte get() :顺序读取缓冲区的一个字节, position 会加一
2 、 public Buffer get(byte[]dst): 将缓冲区中的数据读入到数组 dst 中,并适当的移动 position
3 、 public byte get(int index) :得到第 index 个字节,但不移动 posoiion
4 、 public ByteBuffer put(byte b) :将字节 b 放入到缓冲区中,并移动 position
5 、 public ByteBuffer put(int index,byte b) :将字节 b 放到缓冲区的 index 位位置
6 、 pubglic final ByteBuffer(byte[]src) :将字节数组 src 放到缓冲区中。
10 、标志缓冲区:类似于一个书签的功能,在数据的处理过程中,可随时记录当前位置。然后在任意时刻,回到这个位置。 Mark 用于记录当前位置, reset 用于恢复到 mark 所在的位置、
11 、复制缓冲区:使用 Buffer 的 duplicate 方法可以复制一个缓冲区,副本缓冲区和原缓冲区共享一份空间但是有有着独立的 position 、 capacity 和 limit 值。
20 、缓冲区分片:缓冲区分片使用 slice 方法实现。它将在现有的缓冲区中,创建的子缓冲区。子缓冲区和父缓冲区共享数据。这个方法有助于将系统模块化。缓冲区切片可以将一个大缓冲区进行分割处理,得到的子缓冲区都具有缓冲的缓冲区模型结构;因此。这个操作有助于系统的模块化。
12 、只读缓冲区:只读缓冲区可以保证核心数据的安全,如果不希望数据被随意篡改,返回一个只读缓冲区是很有帮助的。
13 、文件映射到内存: NIO 提供了一种将文件映射到内存的方法进行 IO 操作,这种方法比基于流 IO 快很多。这个操作主要由 FileChanne.map() 操作。使用文件内存的方式,将文本通过 FileChannel 映射到内存中。然后从内存中读取数据。同时,通过修改 Buffer, 将对内存中数据的修改写到对应的硬盘文件中。
14 、处理结构化数据:散射和聚集。散射就是将数据读入到一组 bytebuffer 中,而聚集正好相反。通过 ScatteringByteChannel 和 GatheringByteChannel 可以简化对结构数据的操作。
15 、直接内存访问: DirectBuffer 直接分配在物理内存中,并不占用对空间,因此也不受对空间限制。 DirectBuffer 的读写操作比普通 Buffer 块,因为 DirectBuffer 直接操纵的就是内核缓冲区。
16 、引用类型:强、软、若、虚四种引用类型。
WeakHashMap :是弱引用的一中典型应用,它使用弱引用作为内部数据的存储方案。可以作为简单的缓存表解决方案。
如果在系统中,需要一张很大的 Map 表, Map 中的表项作为缓存之用。这也意味着即使没能从该 Map 中取得相应地数据,系统也可以通过选项方案获取这些数据,虽然这样会消耗更多的时间,但是不影响系统的正常运行。这个时候,使用 WeakHashMap 是最合适的。因为 WeakHashMap 会在系统内存范围内,保存所有表项,而一旦内存不够,在 GC 时,没有被引用的又会很快被清除掉,避免系统内存溢出。
17 、有助于改善系统性能的技巧:
1 、慎用异常: for 循环中使用 try-catch 会大大降低系统性能
2 、使用局部变量:局部变量的访问速度远远高于类的静态变量的访问速度,因为类的 变量是存在在堆空间中的。
3 、位运算代替乘除法:右移代表除以二、左移代表乘以二。
4 、有的时候考虑是否可以使用数组代替位运算。
5 、一维数组代替二维数组。
6 、提取表达式:尽可能让程序少做重复的计算,尤其要关注在循环体的代码,从循环提中提取重复的代码可以有效的提升系统性能。
三、Java性能优化系列之三--并发程序设计详解
1、并发程序设计模式:
( 1 )、 Future-Callable 模式: FutureTask 类实现了 Runnable 接口,可以作为单独的线程运行,其 Run 方法中通过 Sync 内部类调用 Callable 接口,并维护 Callable 接口的返回值。当调用FutureTask.get() 的时候将返回 Callable 接口的返回对象。 Callable 接口是用户自定义的实现,通过实现 Callable 接口的 call() 方法,指定 FutureTask 的实际工作内容和返回对象。 Future 取得的结果类型和 Callable 返回的类型必须一致,这是由定义 FutureTask 的时候指定泛型保证的。 Callable 要采用 ExecutorSevice 的 submit 方法提交,返回的 future 对象可以取消任务。
( 2 )、 Master-Worker 格式:其核心思想是系统由两类进程协作工作: Master 进程和 Worker 进程。 Master 进程负责接收和分配任务, Worker 负责处理子任务。当各个子任务处理完成后,将结果返回给 Master 进程。由 Master 进程进行归纳会汇总,从而得到系统的最终结果。
( 3 )、保护暂停模式:其核心思想是仅当服务进程准备好时,才提供服务。设想一种场景,服务器会在很短时间内承受大量的客户端请求,客户端请求的数量可能超过服务器本身的即时处理能力。为了不丢弃任意一个请求,最好的方式就是将这个客户端进行排列,由服务器逐个处理。
( 4 )、不变模式:为了尽可能的去除这些由于线程安全而引发的同步操作,提高并行程序性能 ,可以使用一种不可变的对象,依靠对象的不变性,可以确保在没有同步操作的多线程环境中依然保持内部状态的一致性和正确性。
( 5 )、 Java 实现不变模式的条件:
1) 、去除 setter 方法以及所有修改自身属性的方法。
2 )、将所有属性设置为私有,并用 final 标记,确保其不可修改。
3 )、确保没有子类可以重载修改它的行为。
4 )、有一个可以创建完整对象的构造函数。
Java 中,不变模式的使用有: java.lang.String 类。以及所有的元数据类包装类。
(6)、生产者 - 消费者模式:生产者进程负责提交用户请求,消费者进程负责具体处理生产者进程提交的任务。生产者和消费者之间通过共享内存缓冲区进行通信。通过 Java 提供和饿 BlockingQueue 可以实现生产者消费者模式。
2、JDK 多任务执行框架:
( 1 )、简单线程池实现:线程池的基本功能就是进行线程的复用。当系统接受一个提交的任务,需要一个线程时,并不着急立即去创建进程,而是先去线程池查找是否有空余的进程,若有则直接使用线程池中的线程工作。如果没有,则再去创建新的进程。待任务完成后,不是简单的销毁进程,而是将线程放入线程池的空闲队列,等待下次使用。使用线程池之后,线程的创建和关闭通常由线程池维护,线程通常不会因为会执行晚一次任务而被关闭,线程池中的线程会被多个任务重复使用。
( 2 )、 Executor 框架: Executor 框架提供了创建一个固定线程数量的线程池、返回一个只有一个线程的线程池、创建一个可根据实际情况进行线程数量调整的线程池、可调度的单线程池以及可变线程数量的可调度的线程池。
( 3 )、自定义线程池 : 使用 ThreadPoolExecutor 接口: ThreadPoolExecutor 的构造函数参数如下:
corePoolSize :指的是保留的线程池大小
maximumPoolSize : 指的是线程池的最大大小
keepAliveTime :指的是空闲线程结束的超时时间
Unit : 是一个枚举,表示 keepAliveTime 的单位
workQueue : 表示存放任务的队列。
ThreadFactory :创建线程的时候,使用到的线程工厂
handler : 当线程达到最大限制,并且工作队列里面也已近存放满了任务的时候,决定如何处理提交到线程池的任务策略
上述的几种线程池的内部实现均使用了 ThreadPoolExecutor 接口。我们可以自定义提交但是未被执行的任务队列被执行的顺序,常见的有直接提交的队列、有界的任务队列、无界的任务队列、优先任务队列,这样可以在系统繁忙的时候忽略任务的提交先后次序,总是让优先级高的任务先执行。使用优先队列时,必须让 target 实现 Comparable 接口。
(4)、优化线程池大小: NThreads=Ncpi*Ucpu*(1+W/C) , Java 中使用: Runtime.getRuntime().availableProcesses() 获取可用的 CPU 数量。
3、JDK 并发数据结构:
( 1 )并发 List : Vector 或者 CopyOnWriteArrayList 是两个线程安全的 List 实现。
CopyOnWriteArrayList 很好的利用了对象的不变性,在没有对对象进行写操作之前,由于对象未发生改变,因此不需要加锁。而在试图改变对象的时候,总是先获得对象的一个副本,然后对副本进行修改,最后将副本写回。 CopyOnWriteArrayList 适合读多写少的高并发场合。而 Vector适合高并发写的场合。
( 2 )并发 Set : synchronizedSet 适合高并发写的情景、 CopyOnWriteSet 适合读多写少的高并发场合。
( 3 )并发 Map : ConcurrentHashMap 是专门为线程并发而设计的 HashMap ,它的 get 操作是无锁的,其 put 操作的锁粒度小于 SynchronizedHashMap ,因此其整体性能优于 SynchronizedHashMap 。
( 4 )并发 Queue :在并发队列上, JDK 提供了两种实现,一个是以 ConcurrentLinkedQueue 为代表的高性能队列,一个是以 BlockingQueue 接口为代表的阻塞队列。如果需要一个能够在高并发时,仍能保持良好性能的队列,可以使用 ConcurrentLinkedQueue 对象。而 BlockingQueue的主要适用场景就是生产者消费者模式中的实现数据共享。 BlockingQueue 接口主要有两种实现: ArrayBlockingQueue 是一种基于数组的阻塞队列实现,也就是说其内部维护着一个定长数组,用于缓存队列中的数据对象。 LinkedBlockingQueue 则使用一个链表构成的数据缓冲队列。
4 、并发控制方法:
( 1 )、 Java 中的内存模型与 Volatile :在 Java 中,每一个线程有一块工作内存区,其中存放着被所有线程共享的主内存中的变量的值的拷贝。当线程执行时,它在自己的内存中操作变量。为了存取一个共享的变量,一个线程通常要先获取锁定并且清除它的内存缓冲区,这保证该共享变量从所有线程的共享内存区正确地装入到线程的工作内存区;当线程解锁时保证该工作内存区中变量的值写回到共享内存中。
( 2 )、 Volatile 关键字:声明为 Volatile 的变量可以做以下保证:
1 )、其他线程对变量的修改,可以随即反应在当前进程中。
2 )、确保当前线程对 Volatile 变量的修改,能随即写回到共享主内存中,并被其他线程所见
3 )、使用 Volatile 声明的变量,编译器会保证其有序性。
4 )、 double 和 long 类型的非原子处理:如果一个 double 类型或者 long 类型的变量没有被声明为 volatile 类型,则变量在进行 read 和 write 操作的时候,主内存会把它当成两个 32 位的read 或者 write 操作。因此,在 32 为操作系统中,必须对 double 或者 long 进行同步
原因在于:使用 Volatile 标志变量,将迫使所有线程均读写主内存中的对应变量,从而使得 Volatile 变量在多线程间可见。
(3)、同步关键字 -Synchronized ,其本质是一把锁: Synchronized 关键字可以作用在方法或者代码块中。当作用的是成员方法时,默认的锁是该对象 this ,这个时候一般在共享资源上进行Synchronized 操作。该关键字一般和 wait ()和 notify ()方法一起使用,调用这两个方法的时候一般指的是资源本身。由于所有的对象都能当成资源,因此这两个方法是从 Object 继承而来的,而不是 Thread 或者 Runnable 才具有的方法。
(4)、 ReentrantLock 锁:比 Synchronized 的功能更强大,可中断、可定时。所有使用内部锁实现的功能,都可以使用重入锁实现。重入锁必须放入 finally 块中进行释放,而内部锁可以自动释放。 重入锁有着更强大的功能,比如提供了锁等待时间 (boolean tryLock(long time.TimeUnit unit)) 、支持锁中断 (lockInterruptibly()) 和快速锁轮询 (boolean tryLock()) 以及一套 Condition 机制,这个机制类似于内部锁的 wait() 和 notify() 方法。
(5)、 ReadWriteLock :读写分列锁。如果 系统中读操作次数远远大于写操作,而读写锁就可以发挥巨大的作用。
(6)Condition 对象: await() 方法和 signal() 方法。 Condition 对象需要和重入锁( ReentrantLock )配合工作以完成多线程协作的控制。
(7)Semaphore 信号量:信号量为多线程写作提供了更为强大的控制方法。广义上讲,信号量是对锁的扩展。无论是内部锁( Synchronized )还是重入锁( ReentrantLock ),一次都只允许一个进程访问一个资源。而信号量却可以指定多个线程同时访问某一个资源。
(8)ThreadLocal 线程局部变量: ThreadLocal 是一种多线程间并发访问变量的解决方案。与synchronized 等加锁方式不同, ThreadLocal 完全不提供锁,而使用以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全,因此并不是一种数据共享的解决方案。
5、同步工具类:
( 1 ) CountDownLatch (闭锁):确保一个服务不会开始,直到它依赖的其他服务都准备就绪。 CountDownLatch 作用犹如倒计时计数器,调用 CountDownLatch 对象的 countDown 方法就将计数器减 1 ,当计数到达 0 时,则所有等待者或单个等待者开始执行。比如有 10 个运动员的田径比赛 , ,有两个裁判 A 和 B , A 在起点吹哨起跑, B 在终点记录记录并公布每个运动员的成绩。刚开始的时候,运动员们都趴在跑道上( A.await() )等到裁判吹哨。 A 吹哨耗费了 5 秒,此时调用 A.countDown() 方法将等待时间减为 4 秒。当减为 0 的时候,所有的运动员开始起跑。这个时候, B 裁判开始工作。启动一个初始值为 10 的定时器,每当有一个运动员跑到重点的时候,就将计数器减一,代表已经有一个运动员跑到终点。当计时器为 0 的时候,代表所有的运动员都跑到了终点。此时可以根据公布成绩了。
( 2 ) CylicBarrier (关卡):
1 )、类似于闭锁,它们能够阻塞一组线程直到某些事件发生
2 )、与同步锁的不同之处是一个可以重用,一个不可以重用
3 )、所有线程必须同时到达关卡点,才能继续处理。
类似组团旅游,导游就是一个关卡。表示大家彼此等待,大家集合好后才开始出发,分散活动后又在指定地点集合碰面,这就好比整个公司的人员利用周末时间集体郊游一样,先各自从家出发到公司集合后,再同时出发到公园游玩,在指定地点集合后再同时开始就餐。
( 3 ) Exchanger :使用在两个伙伴线程之间进行数据交换,这个交换对于两个线程来说都是安全的。
讲解 Exchanger 的比喻:好比两个毒贩要进行交易,一手交钱、一手交货,不管谁先来到接头地点后,就处于等待状态了,当另外一方也到达了接头地点(所谓到达接头地点,也就是到到达了准备接头的状态)时,两者的数据就立即交换了,然后就又可以各忙各的了。
exchange 方法就相当于两手高高举着待交换物,等待人家前来交换,一旦人家到来(即人家也执行到 exchange 方法),则两者立马完成数据的交换。
5、关于死锁:
(1)死锁的四个条件:
1) 、互斥条件:一个资源只能被一个线程使用:
2 )、请求与保持条件:一个线程因请求资源而阻塞时,对已获得则资源保持不放。
3 )、不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺。
4 )、循环等待条件:若干个线程已经形成了一种头尾相接的循环等待资源关系。
(2)常见的死锁:静态顺序死锁、动态顺序死锁、协作对象间的死锁、线程饥饿死锁。
(3)如何尽量避免死锁:
1 )、制定锁的顺序,来避免死锁
2 )、尝试使用定时锁( lock.tryLock(timeout) )
3 )、在持有锁的方法中进行其他方法的调用,尽量使用开放调用(当调用方法不需要持有锁时,叫做开放调用)
4 )、减少锁的持有时间、减小锁代码块的粒度。
5 )、不要将功能互斥的 Task 放入到同一个 Executor 中执行。
6 、 代码层面对锁的优化机制:
1 、避免死锁
2 、减少锁持有时间,代码块级别的锁,而不是方法级别的锁
3 、减小锁粒度, ConcurrentHashMap 分段加锁
4 、读写锁代替独占锁
5 、锁分离,例如 LinkedBlockingQueue 的尾插头出的特点,用两把锁 (putLock takeLock) 分离两种操作。
6 、重入锁和内部锁
重入锁( ReentrantLock )和内部锁( Synchronized ):所有使用内部锁实现的功能,都可以使用重入锁实现。重入锁必须放入 finally 块中进行释放,而内部锁可以自动释放。
重入锁有着更强大的功能,比如提供了锁等待时间 (boolean tryLock(long time.TimeUnit unit))、支持锁中断 (lockInterruptibly()) 和快速锁轮询 (boolean tryLock()) 以及一套 Condition 机制,这个机制类似于内部锁的 wait() 和 notify() 方法。想要获取多线程面试题的可以加群:650385180,面试题及答案在群的共享区。
7 、锁粗化:虚拟机在遇到一连串连续的对同一个锁不断进行请求和释放从操作的时候,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数。
7 、 Java 虚拟机层面对锁的优化机制:
1、自旋锁:由于线程切换(线程的挂起和恢复)消耗的时间较大,则使线程在没有获得锁时,不被挂起,而转而执行一个空循环。在若干空循环后,线程如果获得了锁,而继续执行,若线程依然不能获得锁,而才被挂起。
2、锁消除: JVM 通过对上下文的扫描,去除不可能存在共享资源竞争的锁,这样可以节省毫无意义的请求锁时间。比如单线程中或者非共享资源的常使用的 StringBuffer 和 Vector 。
3、锁偏向:若某一个锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,无需进行相关的同步操作,从而节省了操作时间。
8 、 Java 无锁实现并发的机制:
( 1 )非阻塞的同步 / 无锁: ThreadLocal ,让每个进程拥有各自独立的变量副本,因此在并行计算时候,无须相互等待而造成阻塞。 CVS 算法的无锁并发控制方法。
( 2 )原子操作: java.util.concurrent.atomic 包。
四、Java性能优化系列之四--Java内存管理与垃圾回收机制详解
1 、 JVM 运行时数据区域。
( 1 )、程序计数器:每一个 Java 线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令。此内存区域是唯一一个在 JVM Spec 中没有规定任何 OutOfMemoryError 情况的区域。
( 2 )、 Java 虚拟机栈:该块内存描述的是 Java 方法调用的内存模型,每个方法在被执行的时候,都会同时创建一个帧( Frame )用于存储本地变量表、操作栈、动态链接、方法出入口等信息。
( 3 )、本地方法栈。本地方法调用的内存模型。
( 4 )、 Java 堆。 Java 中的对象以及类的静态变量的存放地方。
( 5 )、方法区:方法区中存放了每个 Class 的结构信息,包括常量池、字段描述、方法描述等等
( 6 )、运行时常量池: Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表 (constant_pool table) ,用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是 Java 语言并不要求常量一定只有编译期预置入 Class 的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的 String.intern() 方法)。运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出 OutOfMemoryError 异常。
( 7 )、本机直接内存( Direct Memory )
在 JDK1.4 中新加入了 NIO 类,引入一种基于渠道与缓冲区的 I/O 方式,它可以通过本机 Native 函数库直接分配本机内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 对和本机堆中来回复制数据。
2、Java 类加载机制的特点:
(1)基于父类的委托机制:运行一个程序时,总是由 AppClass Loader (系统类加载器)开始加载指定的类,在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载, Bootstrap Loader (启动类加载器)是最顶级的类加载器了,其父加载器为 null 。如果父类加载器找不到给定的类名,则交由子加载器去加载,如果最低一层的子加载器也无法找到,则抛出异常。
(2)全盘负责机制:所谓全盘负责,就是当一个类加载器负责加载某个 Class 时,该 Class 锁依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
(3)缓存机制:缓存机制将会保证所有加载过的 Class 对象都会被缓存,当程序中需要使用某个 Class 时,类加载器会先从缓冲区中搜寻该 Class ,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转化为 Class 对象,存入缓存区中。这就是为什么修改了 Class 后,必须重新启动 JVM ,程序所做的修改才会生效的原因。同时,往们比较 A.getClass() 与 B.getClass() 是否相等时,直接使用 == 比较,因为缓存机制保证类的字节码在内存中只可能存在一份。
(4)类加载器的三种方法以及其区别:
1)、命令行启动应用时候由 JVM 初始化加载
2)、通过 Class.forName() 方法动态加载
3)、通过 ClassLoader.loadClass() 方法动态加载 // 使用 Class.forName() 来加载类,默认会执行初始化块 , // 使用 Class.forName() 来加载类,并指定 ClassLoader ,初始化时不执行静态块。
4)区别:使用 ClassLoader.loadClass() 来加载类,不会执行初始化块,
3 、类的主动引用
什么情况下需要开始类加载过程的第一个阶段,也即类的初始化阶段。 Java 虚拟机规定了有且只有 5 种情况下必须立即对类进行初始化:
(1)、遇到 new 、 getstatic 、 putstatic 或 invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要触发其初始化。(而且初始化的时候按照先父后子的顺序)。这四条指令最常见的 Java 代码场景是:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰,已在编译时期把结果放入常量池的静态字段除外)、调用一个类的静态方法的的时候。
(2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先对其进行初始化。
(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。但是一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会被初始化。
(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含 main ()方法的那个类),虚拟机会先初始化这个主类。
(5)当使用 jdk1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic 、 REF_putStatic 、 REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有初始化过,则需要先触发其初始化。
4 、类的被动引用
1、对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
2、通过数组定义来引用类,不会触发类的初始化 SuperClass[]sca=new SuperClass[10].
3、常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发常量的类的初始化。
5 、 Java 对象的创建过程以及如何保证对象创建的多线程的安全性:
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有则进行类加载过程。
在类加载通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存的大小在类加载完成后便可完全确定。为对象分配空间的任务等价于把一块确定大小的内存从 Java 堆中划分出来。
保证多线程的安全性。 有两种方案,一种是对分配内存的动作进行同步操作,实际上虚拟机采用 CAS 加上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同的空间中进行。即为每个线程在 Java 堆中预先分配一小块内存,成为本地线程分配缓冲( TLAB )。哪个线程要分配内存,就在哪个 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB时,才需要分配新的 TLAB 。
6、 什么时候判断一个对象可以被回收?
用可达性分析算法。这个算法的基本思路就是通过一系列的成为“ GC roots ”的对象作为起始点,从这些节点开始向下搜索,如果一个对象到 GCroots 没有任何引用链相连,则证明此对象是不可用的。可作为 GCroots 的对象包括虚拟机栈中引用的对象、方法区中常量引用的对象、方法区中静态属性引用的对象或者本地方法栈中 JNI 引用的对象,这些对象的共同点都是生命周期与程序的生命周期一样长,一般不会被 GC 。判断一个对象死亡,至少经历两次标记过程:如果对象在进行可达性算法后,发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记,并在稍后执行其 finalize ()方法。执行是有机会,并不一定执行。稍后 GC 进行第二次标记,如果第一次标记的对象在 finalize ()方法中拯救自己,比如把自己赋值到某个引用上,则第二次标记时它将被移除出“即将回收”的集合,如果这个时候对象还没有逃脱,那基本上就会被 GC 了。
7 、 关于 finalize ()方法的作用的说明:
finalize ()方法的工作原理理论上是这样的:一旦垃圾回收器准备好释放占用的存储空间,将首先调用其 finalize ()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存,所以使用 finalize ()的目的就是在垃圾回收时刻做一些重要的清理工作。我们知道,使用 GC 的唯一原因就是回收程序不再使用的内存,所以对于与垃圾回收有关的任何行为来说,包括 finalize() 方法,它们也必须同内存及其回收有关。个人认为 Java 对象的 finalize ()方法有两个作用( 1 )回收通过创建对象方式以外的方式为对象分配了存储空间。比如,比如在 Java 代码中采用了 JNI 操作,即在内存分配时,采用了类似 C 语言中的 malloc 函数来分配内存,而且没有调用free 函数进行释放。此时就需要在 finalize ()中用本地方法调用 free 函数以释放内存。( 2 )对象终结条件的验证,即用来判定对象是否符合被回收条件。比如,如果要回收一个对象,对象被清理时应该处于某种状态,比如说是一个打开的文件,在回收之前应该关闭这个文件。只要对象中存在没有被适当清理的部分, finalize ()就可以用来最终法相这种情况。因为对象在被清理的时候肯定处于生命周期的最后一个阶段,如果此时还含有一些未释放的资源,则有能力释放这些资源。这个不是 C/C++ 里面的析构函数,它运行代价高昂,不确定性大,无法保证各个对象的调用顺序。需要关闭外部资源之类的事情,基本上它能做的使用 try-finally 可以做的更好。
8 、 一个类被回收的条件。
(1)、该类所有的实例都已经为 GC ,也就是说 JVM 中不存在该 Class 的任何实例。
(2)、加载该类的 ClassLoader 已经被 GC 。
(3)该类对应的 java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问类的方法。
9、 垃圾回收算法 :
(1)、标记 - 清除算法:标记阶段根据根节点标记所有从根节点开始的可达对象。则未被标记的对象就是未被引用的垃圾对象,然后在清除阶段,清楚所有未被标记的对象。其最大缺点是空间碎片。
(2)、复制算法:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清楚正在使用的内存快中的所有对象,然后交换两个内存的角色。完成垃圾回收。这种算法比较适合新生代,因为在新生代,垃圾对象通常会多于存活对象,复制算法效果较好。 Java 的新生代串行 GC 中,就使用了复制算法的思想。新生代分为 eden 空间、 from 空间和 to 空间三个部分。 From 和 to 空间可以视为用于复制的两块大小相同、地位相等、且可以进行角色互换的空间块。 From 和 to 空间也成为 survivor 空间,即幸存者空间,用于存放未被回收的对象。
(3)、标记 - 压缩算法:标记过程与标记清楚算法一样,但后续不是直接对可回收对象进行清理,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。适合老年代的回收。
(4)、分代收集算法。
10 、 垃圾回收器。
( 1 )、 Serial 收集器
单线程收集器,收集时会暂停所有工作线程(我们将这件事情称之为 Stop The World ,下称 STW ),使用复制收集算法,虚拟机运行在 Client 模式时的默认新生代收集器。
(2)、 ParNew 收集器就是 Serial 的多线程版本,除了使用多条收集线程外,其余行为包括算法、 STW 、对象分配规则、回收策略等都与 Serial 收集器一摸一样。对应的这种收集器是虚拟机运行在 Server 模式的默认新生代收集器,在单 CPU 的环境中, ParNew 收集器并不会比 Serial 收集器有更好的效果。
(3)Parallel Scavenge 收集器(下称 PS 收集器)也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与 ParNew 收集器有所不同,它是以吞吐量最大化(即 GC 时间占总运行时间最小)为目标的收集器实现,它允许较长时间的 STW 换取总吞吐量最大化。
(4)4.Serial Old 收集器 Serial Old 是单线程收集器,使用标记-整理算法,是老年代的收集器
(5) Parallel Old 收集器
老年代版本吞吐量优先收集器,使用多线程和标记-整理算法, JVM 1.6 提供,在此之前,新生代使用了 PS 收集器的话,老年代除 Serial Old 外别无选择,因为 PS 无法与 CMS 收集器配合工作。
(6)CMS ( Concurrent Mark Sweep )收集器
CMS 是一种以最短停顿时间为目标的收集器,使用 CMS 并不能达到 GC 效率最高(总体 GC时间最小),但它能尽可能降低 GC 时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来说至关重要。
( 7 )、 G1 收集器。
11 、内存分配与回收策略:
( 1 )、规则一:通常情况下,对象在 eden 中分配。当 eden 无法分配时,触发一次 Minor GC 。
( 2 )、规则二:配置了 PretenureSizeThreshold 的情况下,对象大于设置值将直接在老年代分配。
( 3 )、规则三:在 eden 经过 GC 后存活,并且 survivor 能容纳的对象,将移动到 survivor 空间内,如果对象在 survivor 中继续熬过若干次回收(默认为 15 次)将会被移动到老年代中。回收次数由 MaxTenuringThreshold 设置。
( 4 )、规则四:如果在 survivor 空间中相同年龄所有对象大小的累计值大于 survivor 空间的一半,大于或等于该年龄的对象就可以直接进入老年代,无需达到 MaxTenuringThreshold 中要求的年龄。
( 5 )、规则五:在 Minor GC 触发时,会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,改为直接进行一次 Full GC ,如果小于则查看 HandlePromotionFailure 设置看看是否允许担保失败,如果允许,那仍然进行 Minor GC ,如果不允许,则也要改为进行一次 Full GC 。
11、 关于 Minor GC 与 Full GC
Java 堆,分配对象实例所在空间,是 GC 的主要对象。分为新生代 (Young Generation/New)和老年代 (Tenured Generation/Old) 。新生代又划分成 Eden Space 、 From Survivor/Survivor 0 、
To Survivor/Survivor 1 。
新生代要如此划分是因为新生代使用的 GC 算法是复制收集算法。新生代使用赋值收集算法,但是为了内存利用率,只使用一个 Survivor 空间来作为轮转备份(之所以把该空间分为 FromSpace 和 ToSpace 两部分是为了在 Minor GC 的时候把一些 age 大的对象从新生代空间中复制到老年代空间中)这种算法效率较高,而 GC 主要是发生在对象经常消亡的新生代,因此新生代适合使用这种复制收集算法。由于有一个假设:在一次新生代的 GC(Minor GC) 后大部分的对象占用的内存都会被回收,因此留存的放置 GC 后仍然活的对象的空间就比较小了。这个留存的空间就是 Survivor space : From Survivor 或 To Survivor 。这两个 Survivor 空间是一样大小的。例如,新生代大小是 10M(Xmn10M) ,那么缺省情况下 (-XX:SurvivorRatio=8) , Eden Space 是 8M , From 和 To 都是 1M 。
在 new 一个对象时,先在 Eden Space 上分配,如果 Eden Space 空间不够就要做一次 Minor GC 。 Minor GC 后,要把 Eden 和 From 中仍然活着的对象们复制到 To 空间中去。如果 To 空间不能容纳 Minor GC 后活着的某个对象,那么该对象就被 promote 到老年代空间。从 Eden 空间被复制到 To 空间的对象就有了 age=1 。此 age=1 的对象如果在下一次的 Minor GC 后仍然存活,它还会被复制到另一个 Survivor 空间 ( 如果认为 From 和 To 是固定的,就是又从 To 回到了From 空间 ) ,而它的 age=2 。如此反复,如果 age 大于某个阈值 (-XX:MaxTenuringThreshold=n),那个该对象就也可以 promote 到老年代了。
如果 Survivor 空间中相同 age( 例如, age=5) 对象的总和大于等于 Survivor 空间的一半,那么 age>=5 的对象在下一次 Minor GC 后就可以直接 promote 到老年代,而不用等到 age 增长到阈值。
在做 Minor GC 时,只对新生代做回收,不会回收老年代。即使老年代的对象无人索引也将仍然存活,直到下一次 Full GC 。
在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果经过 Minor GC 后仍有大量对象存活的情况,则需要老年代进行分配担保,把 Survior 无法容纳的对象直接进入老年代。
13 、四种引用类型:
( 1 )、强引用:直接关联,虚拟机永远不会回收。
( 2 )、软引用:描述一些还有用但并非必须的对象,虚拟机会在抛出内存溢出异常之前会对 这些对象进行第二次回收。
( 3 )弱引用:虚拟机一定会回收的对象
( 4 )虚引用:为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
14 、关于 Java 中生成对象的 4 种方式与区别:
( 1 )、使用 new 操作符,这是最普遍的一种(会调用相应的构造函数):
如: String s=new String("abc");
( 2 )使用反射动态生成( 会调用相应的构造函数 ):
利用 Class , ClassLoader , Constructor 中的方法可以动态的生成类实例
如: Object o=Class.forName("java.lang.String").newInstance();
Object o=String.class.getClassLoader.loadClass("java.lang.String").newInstance();
以上的方式需要目标类拥有公有无参构造函数
以下使用 Constructor 进行动态生成
class User{
public User(String user,Integer id){}
}
Constructor c=User.class.getConstructor(new Class[]{String.class,Integer.class});
User user=(User)c.newInstance(new Object[]{"zhang san",123});
( 3 )使用克隆生成对象( 不会调用构造函数 )
例如使用一个实现了 Cloneable 接口的对象,调用其 clone() 方法获得该对象的一份拷贝,使用 Java 序列化方式实现深拷贝。
( 4 )利用反序列化从流中生成对象( 不会调用构造函数 ):
利用 ObjectInptuStream 的 readObject() 方法生成对象
五、Java性能优化系列之五--JavaIO
1 、关于 Java 序列化与反序列化:
(1)作用:
1、实现对象状态的保存到本地,以便下一次启动虚拟机的时候直接读取保存的序列化字节生成对象,而不是初始化对象; 2 、实现对象的网络传输( RMI 分布对象); 3 、实现对象的深拷贝。
一:对象序列化可以实现分布式对象。主要应用例如: RMI 要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
二: java 对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的 " 深复制 " ,即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。
(2)基本方式:
ObjectOutputStream 只能对 Serializable 接口的类的对象进行序列化。默认情况下, ObjectOutputStream 按照默认方式序列化,这种序列化方式仅仅对对象的非 transient 的实例变量进行序列化,而不会序列化对象的 transient 的实例变量,也不会序列化静态变量。
当 ObjectOutputStream 按照默认方式反序列化时,具有如下特点:
1 ) 如果在内存中对象所属的类还没有被加载,那么会先加载并初始化这个类。如果在 classpath 中不存在相应的类文件,那么会抛出 ClassNotFoundException ;
2 ) 在反序列化时不会调用类的任何构造方法。
如果用户希望控制类的序列化方式,可以在可序列化类中提供以下形式的 writeObject() 和 readObject() 方法。
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
当 ObjectOutputStream 对一个 Customer 对象进行序列化时,如果该对象具有 writeObject() 方法,那么就会执行这一方法,否则就按默认方式序列化。在该对象的 writeObjectt() 方法中,可以先调用 ObjectOutputStream 的 defaultWriteObject() 方法,使得对象输出流先执行默认的序列化操作。同理可得出反序列化的情况,不过这次是 defaultReadObject() 方法。
有些对象中包含一些敏感信息,这些信息不宜对外公开。如果按照默认方式对它们序列化,那么它们的序列化数据在网络上传输时,可能会被不法份子窃取。对于这类信息,可以对它们进行加密后再序列化,在反序列化时则需要解密,再恢复为原来的信息。
默认的序列化方式会序列化整个对象图,这需要递归遍历对象图。如果对象图很复杂,递归遍历操作需要消耗很多的空间和时间,它的内部数据结构为双向列表。
在应用时,如果对某些成员变量都改为 transient 类型,将节省空间和时间,提高序列化的性能。
|-1 、实体对象实现 seriable 接口以及自定义 seriousid 。
|-2 、 ObjectOutputStream out= new ObjectOutputStream(baos);
out.writeObject(new PersonDemo("rollen", 20));
out.close();
|-3 、 ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream input=new ObjectInputStream(bais);
Object obj =input.readObject();
input.close();
(3)Java 自定义序列化反序列化:复写实现了 seriliable 的实体类的 readObject() 和 writeObject() 的方法的原因:
有些对象中包含一些敏感信息,这些信息不宜对外公开。如果按照默认方式对它们序列化,那么它们的序列化数据在网络上传输时,可能会被不法份子窃取。对于这类信息,可以对它们进行加密后再序列化,在反序列化时则需要解密,再恢复为原来的信息。此时便不能使用默认的 readObject 和 writeObject() 方法。
private void writeObject(java.io.ObjectOutputStream out) throws IOException{
out.defaultWriteObject();
out.writeUTF(name);
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
in.defaultReadObject();
name=in.readUTF();
}
一般情况直接实现 Serializable 接口就可以实现序列化的要求,但是有些情况需要对序列化做一些特殊的要求。
(4)Transits 关键字的作用:屏蔽一些不想进行序列化的成员变量,解屏蔽的方法可以用( 3)
(5)Externalize 的作用:
Externalizable 接口继承自 Serializable 接口,如果一个类实现了 Externalizable 接口,那么将完全由这个类控制自身的序列化行为。 Externalizable 接口声明了两个方法:
public void writeExternal(ObjectOutput out) throws IOException
public void readExternal(ObjectInput in) throws IOException , ClassNotFoundException
前者负责序列化操作,后者负责反序列化操作。
在对实现了 Externalizable 接口的类的对象进行反序列化时, 会先调用类的不带参数的构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除 ,或者把该构造方法的访问权限设置为 private 、默认或 protected 级别,会抛出 java.io.InvalidException: no valid constructor 异常。
(6)与 Java 构造函数的关系:
实现了 Externalizable 接口的类的对象进行反序列化时,会先调用类的不带参数的构造方法;而实现了 Serializable 接口的类的对象进行反序列化时,不会调用任何构造方法。仅仅是根据所保存的对象的状态信息,在内存中重新构建对象!
(7)注意事项:
1) 、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义 serialVersionUID 有两种用途:
在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的 serialVersionUID ;
在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的 serialVersionUID 。
2)、 java 有很多基础类已经实现了 serializable 接口,比如 string,vector 等。但是比如 hashtable 就没有实现 serializable 接口。
3)、并不是所有的对象都可以被序列化。由于安全方面的原因一个对象拥有 private,public 等 field, 对于一个要传输的对象 , 比如写到文件 , 或者进行 rmi 传输等等 , 在序列化进行传输的过程中 ,这个对象的 private 等域是不受保护的;资源分配方面的原因 , 比如 socket,thread 类 , 如果可以序列化 , 进行传输或者保存 , 也无法对他们进行重新的资源分配 , 而且 , 也是没有必要这样实现 .
4)、反序列化对象时,并不会调用该对象的任何构造方法,仅仅是根据所保存的对象的状态信息,在内存中重新构建对象!
5)、当一个对象被序列化时,只保存对象的非静态成员变量,不能保存任何的成员方法和静态的成员变量
6)、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因。
(8)序列化与单例模式的冲突解决办法:
另外还有两个自定义序列化方法 writeReplace 和 readResolve ,分别用来在序列化之前替换序列化对象 和 在反序列化之后的对返回对象的处理。一般可以用来避免 singleTon 对象跨 jvm 序列化和反序列化时产生多个对象实例,事实上 singleTon 的对象一旦可序列化,它就不能保证 singleTon 了。 JVM 的 Enum 实现里就是重写了 readResolve 方法,由 JVM 保证 Enum 的值都是 singleTon 的,所以建议多使用 Enum 代替使用 writeReplace 和 readResolve 方法。
Java 代码
private Object readResolve()
{
return INSTANCE;
}
private Object writeReplace(){
return INSTANCE;
}
注: writeReplace 调用在 writeObject 前 ;readResolve 调用在 readObject 之后。
(9)序列化解决深拷贝的代码:
public Object deepClone() throws IOException, OptionalDataException,
ClassNotFoundException {
// 将对象写到流里
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(this); // 从流里读出来
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return (oi.readObject());
}
对象所属的类要实现 Serializable 接口。同时将该方法写入到对象所属的类中。
深拷贝的时候,调用该方法即可。
2、JavaIO 中的装饰模式:
Java 中使用的最广泛的装饰器模式就是 JavaIO 类的设计。比如, OutPutStream 是输出流的基类,其子类有 FileOutputStream 和 FilterOutputStream, 而 FilterOutputStream 的子类有 BufferedOutputStream 和 DataOutputStream 两个子类。其中, FileOutputStream 为系统的核心类,它实现了向文件写数据的功能,使用 DataOutputStream 可以在 FileOutputStream 的基础上增加多种数据类型的写操作支持( DataOutputStream 类中有 writeUTF 、 writeInt 等函数),而 BufferdOutputStream 装饰器可以对 FileOutputStream 增加缓冲功能,优化 I/O 性能。
3、JavaIO 流的使用场景:
(1)IO 流:用于处理设备上的数据,这里的设备指的是:硬盘上的文件、内存、键盘输入、屏幕显示。
(2)字节流和字符流:字节流好理解,因为所有格式的文件都是以字节形式硬盘上存储的,包括图片、 MP3 、 avi 等,因此字节流可以处理所有类型的数据。字符流读取的时候读到一个或多个字节时(中文对应的 字节数是两个,在 UTF-8 码表中是三个字节)时,先去查指定的编码表,将查到的字符返回。字符流之所以出现,就是因为有了文件编码的不同,而有了对字符进行高效操作的字符流对象。因此,只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都使用字节流。
(3)流操作的基本规律:
1 )、明确数据源和数据汇,目的是明确使用输入流还是输出流。
2 )、明确操作的数据是否是纯文本数据。
3 )、是否需要进行字节流和字符流的转换。
4 )、是否需要使用缓存。
(4)实例说明流操作的基本流程:把键盘上读入的数据以指定的编码存入到文件中。
1 )、明白数据源:键盘输入, System.in ,可用 InputStream 和 Reader
2 )、发现 System.in 对应的流是字节读入流,所以要将其进行转换,将字节转换为字符。
3 )、所以要使用 InputStreamReader 转换流
4 )、如果想提高效率,要加入缓存机制,那么就要加入字符流的缓冲区。 BufferedReader,因此前四步构造出的输入流为:
BufferedReader bur = new BufferedReader(new InputStreamReader(System.in));
5 )、明白数据汇:既然是数据汇,则一定是输出流,可以用 OutputStream 或 Writer 。
6 )、往文件中存储的都是文本文件,因此选用 Writer 。
7 )、因为要指定编码表,所以使用 Writer 中的转换流, OutputStreamWriter 。
注意:虽然最终是文件,但是不可以选择 FileWriter ,因为该对象是使用默认编码表。
8 )是否要提高效率,选择 BufferedWriter 。
9 )转换输出流需要接收一个字节输出流进来,所以要是用 OutputStream 体系,而最终输出到一个文件中。那么就要使用 OutputStream 体系中可以操作的文件的字符流对象, FileOutputStream 。
10 )、通过前面的分析,得到的输出流对象如下:
//String charSet = System.getProperty("file.encoding");
String charSet = "utf-8";
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(new
FileOutputStream("a.txt"),charSet);
4 、可以和流相关联的集合对象 Properties 。
Map
|--HashTable
|--Properties
Properties :该集合不需要泛型,因为该集合中的键值都是 String 类型。
5、其他流对象:
( 1 )打印流:
PrintStream :是一个字节打印流 System.out 对应的就是 PrintStream 。它的构造函数可以接收三种数据类型的值:字符串路径、 File 对象、 OutputStream (当为 System.out 的时候即把输入显示到屏幕上)
PrintWriter :是一个字符打印流。构造函数可以接收四种类型的值。字符串路径、 File 对象(对于这两中类型的数据,还可以指定编码表。也即是是字符集)、 OutPutSream 、 Writer (对于三、四类型的数据,可以指定自动刷新,注意:当自动刷新的值为 true 时,只有三个方法可以用: printlf 、 printf 、 format )
(2)管道流: PipedOutputStream 和 PipedInputStream 。一般在多线程中通信的时候用。
(3)RandomAccessFile :该对象不是流体系中的一员,但是该队选中封装了字节流,同时还封装了一个缓冲区(字节数组),通过内部的指针来操作数组中的数据。该对象特点:只能操作文件和对文件读写都可以。多用于多线程下载。、
(4)合并流:可以将多个读取流合并成一个流。其实就是将每一个读取流对象存储到一个集合中,最后一个流对象结尾作为这个流的结尾。
(5)对象的序列化。 ObjectInputStream 和 ObjectInputStream 。
(6)操作基本数据类型的流对象: DataInputStream 和 DataOutputStream 。
(7)操纵内存数组的流对象,这些对象的数据源是内存,数据汇也是内存: ByteArrayInputStream 和 ByteArrayOutputStream , CharArrayReader 和 CharArrayWriter 。这些流并未调用系统资源,使用的是内存中的数组,所以在使用的时候不用 close 。
(8)编码转换:
在 IO 中涉及到编码转换的流是转换流和打印流,但是打印流只有输出。转换流是可以指定编码表的,默认情况下,都是本机默认的编码表, GBK 。可以通过: Syetem.getProperty( “file.encoding”) 得到。字符串到字节数组成为编码的过程,通过 getBytes(charset) 完成,从字节数组到字符串的过程是解码的过程,通过 String 类的构造函数完成 String ( byte[],charset ) .
(9)编码实例与解析:
(10)JavaNIO 的 Charset 类专门用来编码和解码。
想要了解更多分布式知识点的,可以关注我一下,我后续也会整理更多关于分布式架构这一块的知识点分享出来,另外顺便给大家推荐一个交流学习群:650385180,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多,以下的课程体系图也是在群里获取。
网友评论