NanYin的博客

记录生活点滴


  • Home

  • About

  • Tags

  • Categories

  • Archives

  • Search

深入JVM(二)JVM垃圾回收器

Posted on 2020-05-15 | In JVM
Words count in article: 3k | Reading time ≈ 10

深入JVM(二)JVM垃圾回收器

什么垃圾?为什么要回收?

前提:在java中创建对象的过程就是在内存中分配区域的过程,每次创建,虚拟机会为对象分配一块区域。

定义垃圾:和实际生活一样,没用用处的东西就叫做垃圾,对于Java虚拟机来说,如果这个对象不会被利用,那么这个对象就是垃圾对象。

为什么要回收:其实看完前面已经解释了为什么要回收,因为如果不回收,既不会被使用,而且还会占着内存空间,导致内存溢出,最终OOM异常,这是不允许出现的问题。所以需要对垃圾对象进行回收。在有些语言中需要使用命令、指令手动回收空间,如C、C++,一不小心就忘记回收或多次回收,都会产生问题。而在Java中使用的是垃圾回收器自动回收。

垃圾回收器如何找到垃圾?

有两种方式可以让垃圾回收器来寻找在到在堆中死亡(不能再被任何途径使用的对象)的对象。也就是对对象进行存活分析。

引用计数(Refrence Count)

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1,任何时候引用为 0 的对象就是不可能再被使用的。

这种方式即简单,效率又高,但现在的java虚拟机并没有采用这种方式,原因是这种方式有一个致命的缺陷就是无法找到循环引用的对象。

根可达算法(Root Searching)

根可达算法根据一系列的GC Root 作为起点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。

在Java语言中,可作为GCRoots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/72762049.jpg

GC 算法

在对对象的存活分析后,知道了哪些对象已死亡,哪些对象还存活,会通过下面三种方法对死亡的对象进行清除。

标记清除(Mark-Sweep)

首先会为死亡的对象添加标记,然后在完成标记后,清除标记的对象。此算法为基础的清除算法,因为后面的算法都是基于此改进而来。

这种算法产生两个问题,一是效率问题,标记和清除两步的效率都不高,二是空间碎片化问题,在内存中产生大量的内存碎片。

Untitled

拷贝算法(Copying)

将内存一分为二,每次分配内存只分配在第一块里面,将存活的对象顺序拷贝的另外一块中,然后将第一块中的对象全部抹除掉。这种算法实现简单运行高效,但是带来了一个问题就是内存的利用率不高,每次只能用一半。

Untitled 1

标记压缩(Mark-Compact)

和标记清除相似,同样是做标记,但是后续步骤却有大不同,标记压缩不同于标记清除,直接将死亡对象进行清除,而是在标记死亡的对象后,将存活的对象向一端移动,最后清理到端边界以外的内存。相当于将存活的对象压缩到顺序的内存中后,清理掉其他内容。

Untitled 2

垃圾回收器

现在JVM中一共有10中垃圾回收器,按照模型来分,可分为分代模型和不分代模型(.8之后)

分代模型

虚拟机的垃圾收集可能会采用“分代收集”(GenerationalCollection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。根据不同的区域不同的特性使用不同的垃圾回收算法。

默认新生代与老年代的占比默认是1:2。新产生的对象默认会生成在新生代中,经过gc增加对象的年龄(age),当age超过一定数值时,将对象从新生代转移到老年代中。

新生代

因为在新生代产生的对象,大部分都会被回收掉,也就是说有很少的对象会存活下来,因此,新生代适合使用复制算法能够尽可能少的挪动活的对象,又能够尽可能快的清除死亡的对象。在这个基础上,来谈一谈新生代中的分区。

因为新生代的存活对象非常少,大概占比1/9,所以新生代的分区并不像上面介绍拷贝算法一样是均分为1:1,而是将新生代分为 Eden:survive1:survive2为 8:1:1。具体为何分为三个区域通过下面的图就能够了解:

第一次回收会扫描Eden区,将存活对象从eden区挪到S1中,清空Eden区,第二次会扫描Eden区和S1区,将存活的对象放到S2中,清空Eden和S1区,一次

Untitled 3

老年代

当对象经历了多次GC仍然没有被回收掉时,会被挪到老年代中,因为老年代中的回收次数和回收效率并不想新生代一样,经历一次gc只有少量的对象被回收掉。所以老年代采用标记整理算法,能够减少老年代中出现内存碎片的概率。

TLAB
多线程同时在堆中分配内存时,为了避免多线程冲突,操作同一地址,所以需要对整堆进行加锁,进而影响分配速度。TLAB 能够解决这个问题。
通过对 Eden 区域再进行划分, Thread Local Allocation Buffer(TLAB),这是 JVM 在eden区中为每个线程分配的一个私有缓存区域。多线程分配时,会优先在自己的TLAB中分配,分配不下再在Eden区中分配。

对象在内存中的生命周期

  1. 首先会尝试在栈上分配。在栈上分配的好处:执行速度快,不需要的对象直接弹出,不需要垃圾回收器的介入即可完成内存的释放。
  2. 小对象会现在Eden中的TLAB中进行分配,如果TLAB中分配不下会在Eden区域公共部分进行分配。
  3. 如果对象太大,无法在新生代找到足够长的连续空闲空间, JVM 会直接将对象分配到老年代。
  4. GC过程,GC过程可以分为对新生代、老年代的回收、和全部回收的过程。其中新生代的YGC(young GC)的发生频率最高。

Untitled 4

垃圾回收器的类型

在列举垃圾回收器的类型时,先强调几个Hotspot在垃圾器算法的实现:

  • OopMap:因为Hotspot在GC之前,查找内存中所有存活的对象使用可达性算法寻找死亡的对象,对于一个庞大的系统,每次都进行遍历查找引用是不可行的。因为在进行可达性算法的寻找对象时,需要将所有线程保持一个冻结的状态,Hotspot将这种状态成为”Stop The World“,也就是STW,来配合可达性分析,这样会停顿的时间会非常长。

    所以为了避免STW的时间过长,Hotspot使用一个OopMap的数据结构,让虚拟机知道哪些地址存放着对象引用。当类加载完成的时候,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

  • SafePoint:使用OopMap可以减少STW的时间,但是在运行过程中,因为引用关系随时会发生变化,OopMap的内容也要随之变化,但不是所有指令都会及时的更改内容,这时引入SafePoint的概念。

    程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。

  • SafeRegion:SafePoint的扩充,停滞的线程,内容不会发生变化,所以这段代码区域内任何地方进行GC都是没有问题的。

JVM共有10中垃圾回收器,使用分代模型的共有六种,三种新生代的、三种老年代的,需要配对使用。

Untitled 5

一般的配对类型为:

  • Serial&Serial-Old 在暂停所有工作线程后,使用单个GC线程进行垃圾回收

Untitled 6

  • Parallel Scavenge & Parallel Old 暂停所有工作线程使用多个GC线程进行回收

Untitled 7

  • ParNew & CMS , ParNew和 Parallel Scavenge 其实在功能上是相同的,只是名字不同,CMS在回收老年代时,会经历下图中的三个标记阶段、一个清理阶段:
    • 初始标记
    • 并发标记
    • 重新标记
    • 并发清理

CMS无法清理浮动垃圾,只能等到下一次清理时才能清除。

因为CMS是一款基于“标记—清除”算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量 空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有 很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,当遇到这种问题时,会通过Serial Old,也就是单线程使用标记-整理在进行垃圾回收,此时的回收效率非常低。

由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”

Untitled 8

不分代模型

所谓的不分代模型指的是物理区域不分代,但是在逻辑上风染保留年轻代老年代的概念

G1

G1的出现是为了替代掉在1.5时发布的CMS垃圾回收器。

  • 并行与并发,和CMS相似,G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者 CPU核心)来缩短Stop-The-World停顿的时间。
  • 分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其 他收集器配合就能独立管理整个GC堆。
  • 收集算法:CMS使用标记-清除算法,意味着会产生大量的空间碎片,而G1使用标记-整理,在收集后能提供规整的可用内存
  • 可预测的停顿:G1不像于分代模型中的收集器收集范围是整个年轻代或者老年代,在G1中产生一个概念叫做Region。G1会将堆分为大小相等的Region,在G1中所谓的老年代和年轻代就是不连续的Region区域。因为以上的特性,在G1中进行垃圾回收可以避免对整个区域回收,避免了停顿和增加了回收的效率。

Untitled 9

深入JVM(一)类加载过程和内存分配

Posted on 2020-05-10 | In JVM
Words count in article: 3.8k | Reading time ≈ 14

深入JVM(一)类加载过程和内存分配

本篇需要完成的内容是完成对对象从加载到创建过程的分析,与过程中的内存分配的方式等进行总结

内存区域

在分析对象创建过程前,需要先了解Java虚拟机中对内存区域的划分与管理。

Untitled

红色部分的方法区在1.8之后已经被移除,变为元数据区,红色内的运行时常量池已经成为堆中的一部分使用!!

上图中展示了Java虚拟机在运行时对管理的内存区域的划分。其中,分为线程私有的程序计数器、本地方法栈、虚拟机栈和线程共享的堆与方法区和直接内存。

1. 程序计数器

程序计数器是线程私有的、当前线程所执行的字节码的行号指示器,就像名字一样,它是可以看做计数的,每一条指令执行完后,会挪动程序计数器的指针,并指向下一条指令。(就像在平时debug一样)。

  • 占用大小:程序计数器占用非常小的部分
  • 生命周期:跟随线程的创建而创建,销毁而销毁
  • 作用:简单来说可以执行该线程执行字节码的位置。在多线程切换的情况下也能够知道当前线程执行的位置。

2. Java虚拟机栈

虚拟机栈有一个一个的栈针组成,而每一个栈针内由局部变量表、操作数栈、动态链接、方法出口信息组成。线程的方法调用过程(方法调用链)会将其中的每一个方法依次压入栈中,最后在调用过后,依次出栈,也就是说,方法执行的过程也就是栈针压栈到弹出的过程。

一个线程中方法的调用链可能会很长,很多方法都同时处于执行状态。对于JVM说,在活动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,也就是当前执行的方法。

局部变量表

局部变量表是我们在使用和了解Java虚拟机栈的主要部分,是变量值的存储空间,主要存放了编译时可知的基本数据类型(boolean、byte、char、short、int、float、long、double)和方法引用(reference)。在编译时,局部变量表的空间就确定下来。

操作数栈

操作数栈在方法执行的过程中,起到了对常量或者变量暂存和计算的作用,当方法开始时,操作数栈时空的,随着方法的进行,会将局部变量表中或者实例对象中,复制常量变量到操作数栈,再随着方法的执行,进行数据的运算。最后出栈返回给方法调用者。

和局部变量表相同的是,操作数栈的栈的深度在编译时就已经确认下来。

动态链接

一个方法调用另一个方法时,会通过寻找符号引用的方式查找到直接内存地址。而方法的符号引用存在于运行时常量池中。

在每一个栈针中,会通过栈针中的动态链接,来持有方法的符号引用。能够动态的寻找到方法对应的内存地址完成调用。

方法出口信息

方法返回时可能需要在栈帧中保存一些信息,用来于恢复调用者(调用当前方法的方法)的执行状态。

如果正常返回,会在调用者的栈针中保存调用方法的返回值,而被调用者的栈针可能会保存调用者当前程序计数器的值。

如果异常退出,也就是在调用过程中发现异常,调用者的返回值将由异常处理器返回,而被调用者中就不可能存在调用者的计数器值。

实例

通过下面的代码,来看虚拟机栈的调用返回过程和操作数栈的入栈出栈过程。

源程序:

1
2
3
4
5
6
7
8
9
10
11
12
public class NewObject {

public static void main(String[] args) {
NewObject newObject = new NewObject();
newObject.add();
}
public int add(){
int a = 5;
int b = 6;
return a+b;
}
}

反编译后的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/java8/Jvm/NewObject 半初始化申请内存地址
3: dup // 复制栈顶数值并将复制值压入栈顶
4: invokespecial #3 // Method "<init>":()V 构造初始化
7: astore_1 // 建立关联,将初始化后的存到变量newObject上
8: aload_1 // 从本地变量上加载newObject
9: invokevirtual #4 // Method add:()I 执行add()方法
12: pop // 栈顶数值弹出
13: return
public int add();
Code:
0: iconst_5 // 将5推至操作数栈顶
1: istore_1 // 将栈顶int型数值存入第1个本地变量 也就是a,保存到局部变量表中
2: bipush 6 //将单字节的常量值6推送至操作数栈顶
4: istore_2 // 将栈顶int型数值存入第2个本地变量 也就是b,保存到局部变量表中
5: iload_1 // 将第1个int型本地变量推送至操作数栈顶
6: iload_2 // 将第2个int型本地变量推送至操作数栈顶
7: iadd // 将栈顶两int型数值相加并将结果压入操作数栈顶
8: ireturn //返回

调用过程是:先通过在main方法里面创建一个newObject对象,再调用其中的add方法。其中newObject的过程先略过,后面讲,先看add方法执行的过程。

在add方法内,每次会将int类型的值先推到栈顶,然后执行赋值操作,将赋值的变量保存到局部变量表中,最后计算后将结果return。

3. 本地方法栈

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

和虚拟机栈相同,每次方法执行时,都会创建一个栈针,存放本地方法的局部变量表、操作数栈、动态链接、方法出口信息。

4. 堆

堆是Java虚拟机管理内存中的最大的部分,也是进行GC主要部分。几乎所有的实例变量在堆中分配内存,少数小的对象可能在栈上分配。

在栈上分配的好处,首先快,其次不需要额外的垃圾回收的介入即可完成内存的释放。

在1.7之后,字符串常量池从方法区中挪到了堆中

5. 方法区

方法区和堆一样是线程共享的内容,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

方法区是Java虚拟机定义的规范,在1.7之前,Hotspot虚拟机使用永久代实现方法区。在1.7后,使用了元空间区实现方法区。两者的差异在于元空间使用的是直接内存,整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

运行时常量池

有三个比较容易混淆的概念,什么是类常量池,什么是运行时常量池,什么是字符串常量池,三者的区别是什么。

  • 类常量池存在于字节码文件中,在字节码文件中存在类的常量池,其中存放两个类常量:字面量和符号引用,其中字面量的含义就是文本字符串和final修饰的常量值等。符号引用包含import的包、类和接口的全限定名、字段名称和描述符、方法类型和引用等等。
  • 运行时常量池存在于内存中,在类加载的过程时,会将存在于字节码中的类常量池中的内容加载到运行时常量池中。相较于类常量池,运行时常量池具有动态性,不一定是在编译时期就确定的放入的。
  • 字符串常量池,也叫做StringTable,在java7之后被放到了堆内,存放了字符串常量和字符串对象的引用。在class中的内容加载到运行时常量池后,在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

下图表示运行时常量池中的内容:

http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-14/26038433.jpg

方法区和常量池_Java_屌丝程序员的奋斗之路-CSDN博客

6. 直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。

对象创建过程和对象布局

对象的创建过程

使用new关键字创建对象的过程分为5步,类加载检查、内存分配、初始化零值、设置对象头、执行init方法。参考下图:

Untitled 1

1. 类加载检查

虚拟机遇到new指令时,会先进行类加载检查,如果再运行时常量池中未发现类的符号引用,则先去加载、解析、初始化类这一些列类加载到内存中的过程。

2. 分配内存

在类加载到内存后,对象在堆中分配的大小已经确定,利用指针碰撞或者空闲列表的方式将堆中指定大小的空间分配给新创建的类对象。

3. 初始化零值

在为对象分配完内存后,首先需要对对象的初始化零值,来保证对象不手动赋值也能够正常被访问。此时的对象处于半初始化状态。

4. 设置对象头

在初始化零值后,还需要为对象设置头部信息,包括Mark Word、Kclass Word、数组长度(如果是数组的话),具体会在后面提到。

5. 执行init方法

通过init方法,为对象真正的赋值,当执行init方法后,一个需要的对象才产生出来。

下面通过反编译的代码来看创建一个Object对象的过程:

1
2
3
4
5
6
7
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Object 申请内存空间
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V 构造初始化
7: astore_1 // 建立关联,将初始化后的存到变量Object上
8: return

通过javap反编译可以看出一个对象,通过使用new命令后在堆中申请一块内存空间后,将o推到栈顶,使用invokespecial命令进行初始化,最后使用astore命令将对象o和创建的对象关联到一起。

对象布局

一个对象包含三个部分,分别为对象头、实例数据(对象体)、Padding对其字节。

对象头(Header)

在对象头中包含了三个部分:Mark Word、Class Pointer、数组长度(如果是数组)

  • Mark Word:MarkWord中包含了自身的hashcode,gc的分代信息和锁标志位等,其中当上锁、上不同的锁的时候,头部信息都会出现不同的变化,具体占用长度和信息可以参考下图:

Untitled 2

  • Class Pointer: 在Class Pointer中包含了对方法区中的类元数据的引用,在64位系统下,如果不开启指针压缩时,会比32位多占二分之一的空间,所以在1.6之后,通过使用JVM的参数-XX:+UseCompressedOops可以开启指针压缩将原来的8字节压缩为现在的4字节。
  • 数组长度:如果当前对象时数组时,会额外产生一部分空间存储数组的长度

实例数据(Instance Data)

对象中主要的内容,存放实例数据,包括了对象的所有成员变量,其大小由各个成员变量的大小决定。

对其字节(Padding)

Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的倍数。

观察对象空间实例

1
2
3
4
5
6
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
int[] arr = new int[10];
System.out.println(ClassLayout.parseInstance(arr).toPrintable());
}

当使用上面的方式创建一个对象o和一个数组对象arr时,利用openjdk的jol(Java Object Layout)工具进行内存的分析,产生如下的分析结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 0a 00 00 00 (00001010 00000000 00000000 00000000) (10)
16 40 int [I.<elements> N/A
Instance size: 56 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

因为当前环境为64位,所以生成对象的大小应为8的倍数。可以看到对象o的layout中,前8个字节表示64位的MarkWord,之后4个字节表示KlassWord,最后四个字节并没有内容,是loss掉的,用于补齐字节用的,保证能够被8整除。

而在下面的数组对象arr中,可以看到最后的四个字节并不是对其,而是存储量数组的长度,而最后的字节表示了对象体,也就是初始化的数组内容。

在使用 -XX:-UseCompressedOops 关闭指针压缩时,观察对象o所占大小,发现大小未发生变化,然而原来的padding字节不见了,class pointer 变为了8个字节,一共16个字节,所以不需要padding来对其空间。

1
2
3
4
5
6
7
8
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 1c b6 0d (00000000 00011100 10110110 00001101) (230038528)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

补充:javap和jol工具

  1. 使用JDK中自带的javap对class文件进行反编译,能够获得反编译到的执行过程。
  2. 使用IDEA中的插件:Jclasslib可以更具体的看反编译的代码和常量池中的内容
  3. 使用jol可以查看对象布局,使用方法,将OpenJDK中的jol的jar包加入到项目中,在main方法中使用ClassLayout静态方法解析对象后输出:
1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>

线上内存溢出分析

Posted on 2020-05-02 | In JVM
Words count in article: 1.8k | Reading time ≈ 6

线上内存溢出分析

系统变慢,打开页面变卡,使用top命令查看cpu和内存情况后,发现在使用系统时,CPU 飙高。

针对此问题,展开排查。

排查产生原因

首先,使用 arthas 工具或者使用 jstack 查看 java 运行中的线程状态,查看哪些线程占用cpu过高,在使用时,发现为gc线程。

然后,使用 jstat 或者 arthas 中的 dashboard 命令 进行确认,发现堆中新生代老年代空间已满,每过十几秒就在进行一次full gc。所以,可以分析出CPU飙高的原因为内存溢出。

内存溢出原因分析

因为要进行内存溢出情况,需要拿出堆转储信息,所以,在无人使用系统时,使用 jmap 命令获取堆转储信息:

1
jmap -dump:format=b,file=heapdump.hprof pid

需要注意的是,使用 jmap 时,会使 JVM 处于 STW(stop the world) 状态,所以尽量不要在生产上有人使用时使用该命令,否则会导致非常长的时间停顿。

在拿到堆转储信息后,使用MAT(Memory Analyzer Tool)工具进行分析,装载文件后,得到如下图中的内容:

a1

发现DefaultSVNRepositoryPoll引出的对象占用了3.6个G的空间(整个JVM分配空间为4个G),所以可以确定,是这个类生产 RunnableSecheduleFuture 对象的问题。

对 DefaultSVNRepositoryPool 进行分析,找问题

在前面已经分析出是DefaultSVNRepositoryPool引出的内存溢出,所以直接在IDEA中搜索到这个类,进入到该类,查看是否有相关 RunnableSecheduleFuture 方法。

  • 首先确认哪里引用到了该类

一切还要从SVN上传文件说起,SVN上传文件有几个步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1. 判断路径是否存在;
SVNClientManipulator rp = new SVNClientManipulator(user);
//2. 得到版本库;
SVNRepository rps = rp.getRepository(svnDao.getSvnUrlByRepository(repository) +
repository + "/" + newPath);
//3. 判断是否存在;
SVNNodeKind nodeKind = rps.checkPath("", -1);
if (nodeKind == SVNNodeKind.NONE) {//如果存在则更新路径;
svnConfigDevelopService.addDir(svnDao.getSvnUrlByRepository(repository) +
repository + "/" + newPath, desc, rp, true);
}
importFolder = newPath;
// 4. 上传文件
String msg = svnCommonService.addFileToSVN(svnFilePath, gdFileName,
repository, "/" + importFolder + "/", user, desc);

其中对获取SVNClientManipulator时,其实是获取一个client客户端,整个过程可以用一个简单的sequence图来说明:

a2

  • 然后对DefaultSVNRepositoryPool内部排查

在引用到的 DefaultSVNRepositoryPool 的构造方法内,可以看到

a3

这里有两个Timer,作为全局的变量,可以看到作用是从10秒开始,没10秒执行一次 TimeOutTask() 方法。而这里的 myScheduledTimeoutTask 正是 ScheduledFuture 类型的。

如果不确认该类是否为溢出的对象,可以再进入到scheduleWithFixedDelay中。可以看到返回的正是RunnableScheduledFuture 对象。

a4

此时就可以由刚才的seq图往回推

最终问题有可能出现在在 svnCommonService 中创建了过多的 SVNClientManipulator 。

SVN上传代码排查

是否为上述分析的原因,需要在svnCommonService代码中确认。

  • 对现有的SVN上传代码过程分析

svnFileJsonArray 将json解析为array,循环每个数组中的内容,进行文件上传。

首先SVNClientManipulator的创建发生在循环内,也就是说一次请求上传多个文件,则会创建多个客户端。此处为导致问题的主要原因。

其次,在代码内没有发现同步区域,而且大部分内容是需要查询表、插入表,在高并发的情况下可能会出现 mysql 的 lock wait 异常,最终导致文件无法上传的错误。

最后,现有因为代码是一个请求创建一个线程,n次请求就产生n个线程,这在高请求量的情况下出现问题的概率非常大。

基于以上三点进行代码上的修改

问题代码修改

  • 针对SVNClientManipulator的创建发生在循环内

将json中取出一人或者直接传进一个处理人,创建client,减少client的创建次数

  • 针对一次请求创建一个线程,修改为使用线程池,默认线程池的coreSize为8,核心数*2
  • 针对同步代码块,使用sycronized进行同步代码块。

进阶修改:

上述修改后,可以满足过程,但是实际上大部分代码会使用sycronized进行包裹,同时进行的线程只有一个,所以提前创建多线程是占用内存的时间的。

a5

最终修改为:

  1. 使用单线程处理请求,单线程处理业务代码,避免了加锁解锁的消耗
  2. 仅仅在上传文件时,使用线程池进行文件上传

插曲:为什么DefaultSVNRepositoryPool没有回收掉

这里涉及到客户端连接SVN的状态,默认连接状态时 keepAlive 的,但是不需要手动进行连接的关闭。

一个客户端启动一个全局的Timer,这个Timer没10秒检测一下是否可以关闭连接,可以关闭连接的条件是 这个连接在 60 秒内没有再次访问过SVN。

但是,因为task是需要线程进行执行的,当创建非常多的pool时,timer可能取不到CPU时间片来执行task,所以就在一直等待,导致链上的所有对象,虚拟机都无法进行回收,最终导致内存溢出。

a6


其他

一、JVM调优参数

线上分为三台机器,而应用占其中一台,总内存为16g,针对此环境修改JAVA_OPTS

1
2
3
4
5
6
7
8
9
10
JAVA_OPTS="-server -Xms8192m -Xmx8192m 
-XX:PermSize=1024M -XX:MaxPermSize=1024M -Duser.language=zh -Djava.util.Arrays.useLegacyMergeSort=true
-Djava.awt.headless=true
-Xloggc:/app/okit/java/gc-%t.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=50M -XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-XX:HeapDumpPath=./java_dump.hprof
-XX:+HeapDumpOnOutOfMemoryError"

针对其中参数含义解释:

  • -XX:+UseGCLogFileRotation GCLog文件输出
  • -XX:NumberOfGCLogFiles=5 GCLog文件数量
  • -XX:GCLogFileSize=20M GCLog文件大小
  • -XX:+PrintGCTimeStamps 打印GC耗时
  • -XX:+PrintGCDetails 打印GC回收的细节
  • -XX:HeapDumpPath=./java_pid.hprof :堆内存快照的存储文件路径。文件名一般为java____heapDump.hprof。
  • -XX:+HeapDumpOnOutOfMemoryError 在OOM时,自动输出一个dump文件

二、JDK自带工具使用

  • jps

JPS(Java Virtual Machine Process Status Tool),可以显示进行中的Java线程。

使用方式:jps [options] [hostid]

  • jstat -gc

jstat(Java Virtual Machine statistics monitoring tool),能够查看JVM的使用情况

使用方式:jstat [ generalOption | outputOptions vmid [ interval [ s|ms ] [ count ] ] ]

如: jstat -gc -h3 31736 1000 10

  • jstack

jstack(Java stack trace)是Java的堆栈分析工具。

两个功能:

  1. 针对活着的进程做本地的或远程的线程dump;
  2. 针对core文件做线程dump。

使用方式:jstack [ option ] pid

可将堆栈输出到指定文件中:jstack -l PID >> jstack.out

  • jmap

jmap(Java memory map),它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。

jmap -dump:format=b,file=heapdump.hprof pid

  • JVisualVM

用来监测JVM内存和线程使用情况,可以远程连接

三、其他分析工具

  • MAT

用来分析堆转储信息,能够分析内存溢出问题

  • Arthas

可以实现JDK中工具所有功能,更直观。还能够线上热部署。

Java的深拷贝和浅拷贝

Posted on 2020-04-16 | In Java
Words count in article: 1.3k | Reading time ≈ 5

Java的深拷贝和浅拷贝

对象拷贝

在展开说深拷贝和浅拷贝之前,先来阐述阐述一下什么是对象拷贝。对象拷贝(Object Copy)就是将一个对象的属性拷贝到另一个有着相同类类型的对象中去。

可以简单类比为在电脑上复制文件,这时候,复制普通文件和复制链接就产生了差异,这个差异就是接下来需要分析的深拷贝和浅拷贝的差异。

对象拷贝的实现

在Java中如果想要实现拷贝(忽略对象之间使用=号),只能使用clone方法。clone方法使用protect修饰,声明在Object上,也就是所有Object子对象都可以使用clone方法进行对象拷贝。

1
protected native Object clone() throws CloneNotSupportedException;

在注释中可以看到,如果这个类没有继承自Cloneable接口,那么它会抛出CloneNotSupportedException 异常。

在实现接口,并调用clone时,就能完成对象的拷贝。

深拷贝和浅拷贝

在对象拷贝章节类比电脑上复制文件一样,针对普通文件和链接文件有不同的处理方式,这种处理方式在Java对象拷贝的上的体现就是深拷贝。

在 Java 中,除了基本数据类型(元类型)之外,还存在 类的实例对象 这个引用数据类型。如果再拷贝对象的过程中,只对基本类型的变量进行了值得复制,却对引用类型只做了引用的复制(也就是内存地址引用),没有真正复制引用到的对象。此时的对象拷贝就叫做浅拷贝。

与之相反,不光对基本数据类型执行了值得复制,而且在复制引用类型复制时,不是仅仅传递引用,而是将引用到的对象真正的复制(分配内存),此时的对象拷贝就叫做深拷贝。

深拷贝和浅拷贝的区别

其实上面在引申概念时,就已经得出深拷贝和浅拷贝的区别了,深拷贝不光要拷贝进本数据类型的值,还要完成对引用类型创建一个新的对象,并复制其内的成员变量。

深拷贝和浅拷贝的实例

  • 浅拷贝用例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    public class CloneTest implements Cloneable {
    public int x;
    public SonClone son;

    public int getX() {
    return x;
    }

    public void setX(int x) {
    this.x = x;
    }

    public SonClone getSon() {
    return son;
    }

    public void setSon(SonClone son) {
    this.son = son;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
    return super.clone();
    }

    @Test
    public void testClone(){
    CloneTest test = new CloneTest();
    test.setX(127);
    test.setSon(new SonClone());
    try {
    CloneTest clone = (CloneTest) test.clone();
    // 比较test和复制对象copy
    System.out.println("test == clone --> "
    +(test == clone));
    System.out.println("test.hash == clone.hash --> "
    +(test.hashCode() == clone.hashCode()));
    System.out.println("test.getClass() == clone.getClass() --> "
    + (test.getClass() == clone.getClass()));
    System.out.println("test.son == clone.son --> "
    +(test.getSon() == clone.getSon()));
    System.out.println("test.son.hash == clone.son.hash --> "
    +(test.getSon().hashCode() == clone.getSon().hashCode()));
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    }
    }
    class SonClone implements Cloneable{
    int a;
    }

    }

浅拷贝的执行结果如下:

1
2
3
4
5
test == clone --> false
test.hash == clone.hash --> false
test.getClass() == clone.getClass() --> true
test.son == clone.son --> true
test.son.hash == clone.son.hash --> true

可以看到,使用clone可以复制对象,对象的hashcode已经不相同了,但是引用对象却没有执行复制对象的过程,返回的hashcode值仍然是相同的,也就是仅仅复制了引用。

  • 深拷贝用例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    public class DeepClone implements Cloneable{ public int x; public SonClone son;
    public int getX() {
    return x;
    }

    public void setX(int x) {
    this.x = x;
    }

    public SonClone getSon() {
    return son;
    }

    public void setSon(SonClone son) {
    this.son = son;
    }

    class SonClone implements Cloneable{
    int name;

    public int getName() {
    return name;
    }

    public void setName(int name) {
    this.name = name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
    return super.clone();
    }
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
    DeepClone clone = (DeepClone) super.clone();
    clone.son = (SonClone) this.son.clone();
    return clone;
    }

    @Test
    public void testClone(){
    DeepClone test = new DeepClone();
    test.setX(127);
    test.setSon(new SonClone());
    try {
    DeepClone clone = (DeepClone) test.clone();
    // 比较test和复制对象copy
    System.out.println("test == clone --> "
    +(test == clone));
    System.out.println("test.hash == clone.hash --> "
    +(test.hashCode() == clone.hashCode()));

    System.out.println("test.getClass() == clone.getClass() --> "
    + (test.getClass() == clone.getClass()));
    System.out.println("test.x == clone.x --> "
    +(test.getX() == clone.getX()));
    System.out.println("test.son == clone.son --> "
    +(test.getSon() == clone.getSon()));
    System.out.println("test.son.hash == clone.son.hash --> "
    +(test.getSon().hashCode() == clone.getSon().hashCode()));
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    }
    }
    }

这里深度拷贝可以看出在父类使用clone时,会手动将clone出的父类中的引用指向复制clone出来的子类对象。这时对父类执行了深拷贝,但实则对子类进行了一次浅拷贝。结果显而易见,最后的引用类型值和hashcode都不相同。

总结

拷贝对象需要使用clone方法,并需要继承Cloneable接口,如果不手动重写clone方法,则默认会只能执行浅拷贝。

浅拷贝只会复制基本数据类型的值,而不会复制引用类型的对象,而深拷贝需要手动编写clone方法来达到既能复制基本数据值,又能够完成对引用类型的对象的复制。

Java IO浅析

Posted on 2020-04-05 | In Java
Words count in article: 3.6k | Reading time ≈ 14

Java中的IO操作

Java总的来说有三类IO,效率不高,操作简单的BIO(blocking IO),非阻塞的NIO(New IO),和异步非阻塞IO,也就是升级版的NIO(Asynchronous I/O).

IO分类

IO分类

在学习这三类IO前,需要了解什么是阻塞.什么是异步.两个的含义有什么区别.

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)所谓同步,就是在发出一个调用*时,在没有得到结果之前,该 *调用 就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,但是没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态来通知调用者。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会重启线程。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

BIO

BIO过程就如同名字一样,是一个阻塞的IO,服务端通常为每一个客户端都建立一个独立的线程来通过调用accept()来监听客户端消息.如果想处理多个客户端请求则服务端需要建立等同数量的线程来处理这些消息,这就是普遍的一请求一应答的模型.处理完成后返回应答给客户端后销毁线程,因为线程是一个昂贵的资源,这样重复的新建线程,销毁线程,很浪费处理器资源,所以使用BIO同时能够尽可能的少创建线程,就可以用到线程池的方式实现,来达到服务端创建线程数远远小于客户端数的目的,但这种方法只是伪异步IO.

在处理链接数量少的情况下,BIO的效率还不错,并且主要逻辑模型清晰明了,代码简单.但是在上万的链接的情况下,BIO处理起来就非常吃紧了.

NIO

NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

NIO特性和NIO与传统IO的区别

  • 传统IO(BIO)是一种阻塞IO模型,而NIO是非阻塞的IO模型,区别为当线程读取数据的时候,非阻塞IO可以不用等,而阻塞IO需要一直等待IO完成后才能继续.
  • IO面向流,而NIO面向缓冲区.
  • 通道(channel) NIO通过通道进行数据读写.通道是双向的,而传统的IO是单向的.通道链接的都是Buffer,所以通道可以异步的读写.
  • 选择器(Selectors) NIO拥有选择器,而IO没有.选择器的作用就是用来使用单个线程来处理多个通道(NIO面向buffer,通道只与buffer交互).

Selector图解

AIO

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。

BIO的流操作

根据上面的IO分类图可以看到IO流按照流的类型可以分为两类,一类是字节流,一类是字符流

两者之间的区别在于

操作单位不同,字节流以字节为单位进行数据传输,而字符流是以字符为单位进行传输
处理元素不同,字节流可处理所有类型数据,但字符流只可以处理以字符类型的数据,也就是说字符流只可处理纯文本数据

输入输出流

输入输出按照字面理解,就是流中的输入和输出

流类型 输入 输出
字节流 InputStream OutputStream
字符流 Reader Writer

字节流

输入字节流 InputStream

InputStream

inputStream作为抽象类,必须依靠子类实现具体的操作.在抽象类中定义了如下几个方法:

  1. 三个重载的read方法,用来读取数据,其中必须在子类中实现抽象的read方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract int read() throws IOException;

// 定义b.length读取范围
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
// 使用子类中的read进行数据读取
int c = read();
//....
}
  1. skip(long n) 方法,用来掉过并丢弃n个字节的数据,并返回被丢弃的数据
  2. available()方法,用来返回输入流中可以读取的字节数,子类需要单独实现该方法,否则会返回0;
  3. void close() 方法,子类实现,用来关闭流
  4. synchronized void mark(int readlimit)方法,用来标记输入流的当前位置,同样由子类来具体实现
  5. synchronized void reset()方法,用来返回输入流最后一次调用mark方法的位置

来看看几种不同的InputStream:

  1. FileInputStream 把一个文件作为InputStream,实现对文件的读取操作
  2. ByteArrayInputStream 把内存中的一个缓冲区作为InputStream使用
  3. StringBufferInputStream 把一个String对象作为InputStream
  4. PipedInputStream 实现了pipe的概念,主要在线程中使用
  5. SequenceInputStream把多个InputStream合并为一个InputStream

输出字节流 outputStream

  1. OutputStream提供了3个重载的write方法来做数据的输出
1
2
3
4
5
6
// 将参数b中的字节写到输出流 
public void write(byte b[ ])
// 将参数b的从偏移量off开始的len个字节写到输出流
public void write(byte b[ ], int off, int len)
// 先将int转换为byte类型,把低字节写入到输出流中
public abstract void write(int b)
  1. public void flush() 将数据缓冲区中数据全部输出,并清空缓冲区。
  2. public void close() 关闭输出流并释放与流相关的系统资源。

字符流

字符输入流 Reader

字符输入流和字节流相似,同样定义了read相关方法,但是不同的点在于Reader操作char而不是byte,并且在声明Reader时,将自身作为一个对象,在相关操作上使用synchronized进行同步操作.

1
2
3
4
5
6
7
protected Reader() {
this.lock = this;
}
// 实现方法
public int read(char cbuf[], int off, int len) throws IOException {
synchronized (lock) {
// ...

同理字符输出流 Writer

如何使用BIO流

  1. 首先确定是输入还是输出
  2. 其次确认对象是否为纯文本,如果是纯文本可以选择 字符流的 Reader 和 Wirter ,否则需要使用字节流的 inputStream 和 outputStream
  3. 然后确定是否要通过流转换来达到增加处理效率的目的,如果需要则使用 InputStreamReader 等进行转换
  4. 最后,需要确认是否需要使用buffer缓冲来提高效率
  • inputStream 字节输入流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void inputStreamTest(){
String filePath = "/Users/gaoguoxing/Work/temp/attachment/20200403/65bf6a2b0dbf3049dc08e800c2ac617385bb.xml";
try (InputStream in
= new FileInputStream(filePath)
) {
// 获取文件IO流
byte[] content = new byte[100];
StringBuffer bf = new StringBuffer();
// 没有返回 -1
while (true) {
if (in.read(content) < 0) {
break;
}
bf.append(new String(content));
}
System.out.println(bf.toString());
} catch (IOException e) {

}
}
  • outputStream 字节输出流
1
2
3
4
5
6
7
8
public void outputStreamTest(){
String content = "this is my content";
try (OutputStream out = new FileOutputStream("copy.txt")) {
out.write(content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
  • Reader 字符输入流
1
2
3
4
5
6
7
8
9
10
11
public void readerTest(){
String filePath = "/Users/gaoguoxing/Work/temp/attachment/20200403/65bf6a2b0dbf3049dc08e800c2ac617385bb.xml";
String s;
try(BufferedReader in = new BufferedReader(new FileReader(filePath))) {
while ((s = in.readLine()) != null){
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
  • Writer 字符输出流
1
2
3
4
5
6
7
8
public void writerTest(){
String content = "this is my content";
try (FileWriter out = new FileWriter("copy.txt");) {
out.write(content + "\n");
} catch (IOException e) {
e.printStackTrace();
}
}

NIO操作

在NIO特性一章中提到了NIO是面向Buffer的,双向的数据处理形式。因为NIO分为buffer缓冲区和Channel管道,NIO使用管道操作缓冲区,可以说Channel不与数据打交道,它只负责运输数据。更可以抽象的简单理解为Channel管道为铁路,buffer缓冲区为火车(运载着货物),火车可以去,同样也可以回(双向的)。

Buffer 缓冲区

Buffer是具体的原始类型的容器,Buffer是一个线性的、有限的原始类型元素的集合。Buffer中有三个必要的属性:capacity、limit、position

  • capacity:buffer中包含的元素数量
  • limit:buffer中的limit是缓冲区里的数据的总数
  • position:buffer中position是下一个即将被读写的元素

每个实现子类都需要实现两种方法:get和put,两个是相对的操作,也就是理解为读写数据,每次操作时,都会从buffer中的当前position开始,增长transferred个数量的元素,这里的transferred就是get和put的元素数量。如果使用get操作,超出了limit,那么会出现 BufferUnderflowException ,相反如果使用get超出limit,就会出现 BufferOverflowException,这两种情况下,数据都不会被改变。

Buffer中提供了clear()、 filp()、 rewind()方法用来访问Buffer中的position, limit, 和capacity的值。

  • clear() 清空读缓冲区中的内容,之后可以使用put写数据,将limit设置为capacity,将position设置为0
  • filp() 切换成读模式,之后可以使用 get 读数据,将limlit设置为当前position,再将position设置为0
  • rewind() 可重复读缓冲区的内容

下面通过几个实例来看Buffer相关的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 分配capacity大小
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 初始时4个核心变量的值
System.out.println("limit:"+byteBuffer.limit()); // 1024
System.out.println("position:"+byteBuffer.position()); // 0
System.out.println("capacity:"+byteBuffer.capacity()); // 1024
System.out.println("mark:" + byteBuffer.mark());
//mark:java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]

byteBuffer.put("hello world".getBytes());
// 调用mark方法会返回this也就会输出当前buffer
System.out.println("mark:" + byteBuffer.mark());
// mark:java.nio.HeapByteBuffer[pos=11 lim=1024 cap=1024]
// 在执行put操作后,当前位置发生了变化

// 切换成读模式
byteBuffer.flip();
System.out.println("mark:" + byteBuffer.mark());
// mark:java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024]
// 切换成读模式,limit设置为原来的当前位置,当前位置设置为0
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
// 这样使用get读的时候,只能获取到 0~limit 之间的内容
System.out.println(new String(bytes));

// 清空,再次可以向buffer中写数据
byteBuffer.clear();
byteBuffer.put("HELLO WORLD".getBytes());
//mark:java.nio.HeapByteBuffer[pos=11 lim=1024 cap=1024]
System.out.println("mark:" + byteBuffer.mark());

// 切换成读模式
byteBuffer.flip();
//mark:java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024]
System.out.println("mark:" + byteBuffer.mark());
bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println(new String(bytes));

// 重复读
System.out.println("mark:" + byteBuffer.mark());
//mark:java.nio.HeapByteBuffer[pos=11 lim=11 cap=1024]
// 使用get之后,pos变为了limit,在读的话会出现异常,
// 如果再读,只能使用rewind方法
byteBuffer.rewind();
//mark:java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024]
System.out.println("mark:" + byteBuffer.mark());
bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println(new String(bytes));

Channel 管道

Channel是IO操作的核心,Channel表示与一些硬件设备,文件,网络等实体的开放连接,能够进行多个不同的IO操作(读与写).

Channel有开关的状态,只要channel创建,则channel的状态就是open的,如果chennel一旦被关闭,那么如果再有后续调用channel的io操作,都会出现异常.(类似jdbc的connection),以防万一,可以调用isopen()方法检测是否被关闭了

再NIO中几个重要的Channel实现类:

  • FileChannel: 用于文件的数据读写
  • DatagramChannel: 用于UDP的数据读写
  • SocketChannel: 用于TCP的数据读写,一般是客户端实现
  • ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现

用FileChannel来演示创建和传输的过程.

创建channel

有两种方式创建channel,一种是使用file或者fileStream创建cannel,另一种使用FileChannel的静态方法创建

1
2
3
4
5
6
7
8
// 1. 使用randomAccessFile 创建channel
RandomAccessFile randomAccessFile = new RandomAccessFile("a.txt","rw");
FileChannel in = randomAccessFile.getChannel();
// 2. 使用静态方法创建
FileChannel out = FileChannel.open(Paths.get("b.txt"), StandardOpenOption.WRITE);
// 3. 通过FileInputStream 创建channel
FileInputStream inputStream = new FileInputStream("xxx.txt");
inputStream.getChannel();

数据读写

使用channel进行数据读写,类似普通的BIO使用buffer.但是需要注意的是,每次都需要将buffer打开读模式,再读,读完后使用clear清空buffer

1
2
3
4
5
6
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
while(in.read(readBuffer) != -1){
readBuffer.flip();
out.write(readBuffer);
readBuffer.clear();
}

还可以直接使用通道的transferTo直接复制到另外一个通道中,完成复制

1
2
// 使用通道的transferTo
in.transferTo(0,in.size(),out);

关闭资源

最后需要手动将channel关掉(必须)

1
2
in.close();
out.close();

另外这时候可以使用 try-with-resource 进行简化,整体过程可以参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try (
// 创建资源
RandomAccessFile randomAccessFile = new RandomAccessFile("a.txt", "rw");
FileChannel in = randomAccessFile.getChannel();
FileChannel out = FileChannel.open(Paths.get("b.txt"), StandardOpenOption.WRITE);
) {
// 设置buffer
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
// 将通道中的数据放到缓冲区
while (in.read(readBuffer) != -1) {
// 切换成读模式
readBuffer.flip();
// 向通道内写入buffer
out.write(readBuffer);
// 清空本次的buffer
readBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}

线程生命周期

Posted on 2020-03-29 | In Java
Words count in article: 554 | Reading time ≈ 1

线程生命周期

线程生命周期

上图展示了线程从创建到结束的整个生命周期,下面从状态和控制两个方面分解图中的内容

线程状态

线程具有5中基本的状态:

  1. NEW(新建状态): 创建线程,还未启动
  2. RUNNABLE(就绪状态): 可运行状态,但还未获得时间片,等待执行
  3. RUNNING(执行状态): 运行状态,在就绪状态获得了时间片,进入运行状态
  4. BLOCKED(阻塞状态):当某些情况下,线程被阻止运行,进入阻塞状态,阻塞的状态可分为三种:
    • 第一种为执行了wait()后会进入放入线程等待队列中,这种情况叫等待阻塞.
    • 第二种为等待获取synchronized()同步锁时,会讲线程放入同步锁队列中,等待前一个线程执行完synchronized中的内容,这种情况叫同步阻塞.
    • 第三种为执行了sleep()或join()时,和wait()不同,它不会释放对象锁.
  5. TERMINATED(终止状态):当线程正常结束或异常退出时,会到达终止状态

线程方法

  • run/start
    需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run() 方法,这是由Java的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。
  • wait
    当前线程暂停执行并释放对象锁标志,让其他线程可以进入synchronized数据块,当前线程被放入对象等待池中
  • nodify/nodifyAll
    唤醒等待(wait)的线程
  • sleep
    休眠一段时间后,会自动唤醒。但它并不释放对象锁。也就是如果有 synchronized同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常
  • join
    当前线程停下来等待,直至另一个调用join方法的线程终止,线程在被激活后不一定马上就运行,而是进入到可运行线程的队列中
  • yield
    停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么yield()方法将不会起作用

Java中的位运算

Posted on 2020-03-28 | In Java
Words count in article: 1.1k | Reading time ≈ 4

Java中的位运算

Java提供了多种位运算,包括 左移( << )、右移( >> ) 、无符号右移( >>> ) 、位与( & ) 、位或( | )、位非( ~ )、位异或( ^ ),除了 ~ 为一元操作符,其他都为二元操作符。

左移和右移

什么是左右移操作,产生的结果是什么

使用 符号 << 对数字产生的影响就是左移,同理右移。下面通过例子来看左移(右移)的结果:

在 ArrayList 中,使用了grow()方法进行List的扩容操作,其实,在grow()方法内部就使用到了右移操作。

1
2
3
4
5
6
7
8
9
10
11
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

grow() 函数的作用就是对 List 进行倍数的扩容,这个倍数就是 x + x >> 1 ,具体产生的结果用主函数进行测试。

1
System.out.println(10+(10>>1));

产生的结果为15.也就是说 10>>1的结果为5.那么计算过程是什么样的呢,才会产生这样的结果?

转化为二进制

对位的运算,第一步一定是先转化为二进制,再对二进制进行计算。

例子中的 10 转化为二进制为:

0000 0000 0000 0000 0000 0000 0000 1010

进行为运算

如果是左移,则在右侧补0,同理如果是右移,则在左侧补0。

比如上面的 10>>1 , 右移一位,则在左侧补一个0。

结果为 ...... 0101 结果为 5

如果 10<<1 ,左移一位,则在右侧补一个0

结果为 ......1 0100 结果为 20

无符号左移右移

负数以原码的补码形式表达,如果是负数时,比如 -10 ,转化为二进制就是

1111 1111 1111 1111 1111 1111 1111 1011

也就是高位为1,与正数相反,同理,在进行移动时,也需要将原来的补0,调整为补1;

如 -10>>1,右移一位,左侧补1

1......1101 ,结果为 -5

无符号则始终补0;

综上:

  1. 转化为二进制
  2. 正数反向补0
  3. 负数反向补1
  4. 无符号反向补0

位与(&)

在HashMap中进行put元素的时候,会通过 putVal() 方法进行实际的计算和添加。

1
2
3
4
5
6
7
8
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 使用 (n-1)&hash 查找位置是否已经被占用
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);

这里使用 & 的作用其实是为了保证 hash 的值不超过范围。数组具有2的幂数长度,以用&替换昂贵的取模运算。

运算规则:第一个数的的第n位和第二个数的第n位如果都是1,那么结果的第n为也为1,否则为0 简化描述规则:同一为一,否则为零

5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101

3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0011

结果:0000 0000 0000 0000 0000 0000 0000 0001

转化为10进制为 1

可以作用为关闭(屏蔽)特定位的手段

位或(|)

运算规则:第一个数的的第n位于第二个数的第n位 只要有一个是1,那么结果的第n为也为1,否则为0. 简化规则描述: 有一则一,否则为零

5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101

3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0011

结果:0000 0000 0000 0000 0000 0000 0000 0110

转化为10进制为 7

可以作为将特定位置为1的手段

异或(^)

运算规则:第一个数的的第n位于第二个数的第n位 相反,那么结果的第n为也为1,否则为0,简化规则描述: 相反为一,否则为零

5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101

3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0011

结果:0000 0000 0000 0000 0000 0000 0000 0110

转化为二进制为 7

  1. 按位“异或”运算可以使特定的位取反
  2. 直接交换两个变量的值
1
2
3
a^=b
b^=a
a^=b

这样a的值与b的值就形成了互换。

lambda表达式学习和应用

Posted on 2020-03-23 | In Java
Words count in article: 1.6k | Reading time ≈ 6

Lambda表达式

Lamdba 表达式是Java 8 的新特性,也是Java 8 中最重要的的新功能。Lamdaba表达式促进了Java使用函数方式进行编程。

在进行语法的讲解之前,需要了解和Lambda息息相关的Function Interface,可以说只有函数式接口的存在,才有lambda表达式的存在,换句话可以说lambda表达式是函数式接口的实现.

函数式接口

Function 接口具有单个功能,也就是有单个抽象方法.如 Comparable 接口只有一个方法 compareTo 用来进行比较。

Java8定义了函数接口被用来拓展Lamdba表达式。在 java.util.Function 包下有非常多的函数接口。

常用的接口包括;

接口 抽象方法 用法
Function<T,R> R apply(T t); 接受T类型参数,返回R类型结果
Consumer void accept(T t); 接受T类型参数,不返回结果
Predicate boolean test(T t); 接受T类型参数,返回Boolean结果
Suppler T get(); 无参数,返回T类型结果

定义函数接口

  1. 使用 @FunctionalInterface 注解,该注解不是必须的,但是如果标注,则编译器会检查这个接口下是否只有单个抽象方法,如果不是会报错.
  2. 新增default方法,是为了在现有的类库中中新增功能而不影响他们的实现类
  3. 新增接口内static方法,可定义一个或者多个静态方法,和普通的静态方法没有区别,都是接口名.方法名进行调用
  4. 在函数式接口的定义中是只允许有且只有一个抽象方法(必须),但是可以有多个static方法和default方法。

Lambda语法

一个Lambda表达式有如下:

1
paramter -> expression body

以下是lambda表达式的几个特性

  • 可选的类型描述 - 不需要声明参数的类型,编译器可根据参数的值进行推断
  • 可选的参数两旁的括号 - 声明单个参数的时候不需要使用括号,但是如何声明多个参数,括号还是必须的。
  • 可选的大括号 - 如果函数体中只有单行,则不需要在函数体两侧添加大括号
  • 可选的返回值 - 如果函数中只有单行,编译器自动返回这个单行的返回值

下面根据实例来看上面的四个特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Java8Tester {

public static void main(String args[]) {
GreetingService greetService1 = message -> System.out.println("Say:"+message);
greetService1.sayMessage("Hello!!");
MathOperation add = (int a,int b) -> a+b;
MathOperation sub= (a,b) -> a-b;
MathOperation muti = (a,b) -> {return a*b;};
System.out.println("add 函数结果:"+add.operation(2,2));
}

interface GreetingService {
void sayMessage(String message);
}

interface MathOperation{
int operation(int a,int b);
}
}
// 结果:
Say:Hello!!
add 函数结果:4

通过上面的例子,可以得出以下重要的两点:

  • Lambda 表达式主要用来定义功能接口的内联实现,如:一个接口里只有一个接口方法。在上面的例子里面我们使用了对MathOperation接口进行了多种实现。
  • Lambda 表达式消除了对匿名类的需求,如new Thread(new Runnable{..}) ,为Java提供了非常简单而强大的函数编程能力。

使用方法引用

方法引用(Method Refrences)帮助通过方法名称指向方法。方法引用使用符号“::”表示。方法引用可以用爱指向如下类型的方法:

  • 静态方法
  • 实例方法
  • 构造方法使用new关键字(TreeSet::new)

[方法引用]的格式是 类名::方法名

1
Arrays.stream(objectArray).forEach(System.out::println);

使用Lambda

熟能生巧,常常练习肯定能记住

1. 创建Thread

1
2
3
4
5
6
7
8
9
10
11
// 不使用Lambda创建Thread
// 可以看出内部直接创建了Runnable的匿名类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("新建线程。。");
}
}).start();

// 使用lambda创建Thread
new Thread(() -> System.out.println("使用Lambda新建线程。。")).start();

2. 列表迭代

1
2
3
4
5
6
7
8
9
10

List<Integer> list = Arrays.asList(1, 2, 3);
// 不使用Lambda
for (Integer integer : list) {
System.out.println(integer);
}
//使用Lambda
list.forEach((s) -> System.out.println(s));
//方法引用
list.forEach(System.out::println);

3. Map&Reduce

  • 使用Map处理列表内所有元素全部加2
1
2
3
4
5
6
7
// 不使用Lambda
for (Integer integer : list) {
int order = integer + 2;
System.out.println(order);
}
// 使用Lambda
list.stream().map((a) -> a + 2).forEach(System.out::println);
  • 使用 Map + collect 完成对每个元素计算后返回新的结果列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<Integer> newList = new ArrayList<>();

// 不使用Lambda
for (Integer integer : list) {
int order = integer + 2;
int result = order / integer;
newList.add(result);
}
for (Integer integer : newList) {
System.out.println(integer);
}
// 使用Lambda
newList = list.stream().map((a) -> (a + 2)/a).collect(Collectors.toList());
newList.forEach(System.out::println);

4. 使用filter实现元素的过滤

使用lambda,可直接使用filter进行元素的过滤

1
2
3
4
5
6
7
8
9
10
11
12
// 不使用lambda时
List<Integer> list_1 = new ArrayList<>();
for (Integer integer : list) {
if(integer != 3){
list_1.add(integer);
}
}


// 使用 filter 进行过滤
List<Integer> list_2 = list.stream().filter((a) -> a!=3).collect(Collectors.toList());
list_2.forEach(System.out::println);

5. 执行函数

稍复杂的字符串处理,用lambda合适不过了,下面的例子,只将特定字符的字符串进行 toUpperCase

1
2
3
4
5
6
7
8
9
10
List<String> s = new ArrayList<>();
s.add("hello ");
s.add("world ");
s.add("!! ");

List<String> s_1 = s.stream()
.filter(a -> a.startsWith("h") || a.startsWith("w"))
.map(String::toUpperCase)
.collect(Collectors.toList());
s_1.forEach(System.out::println);

6. 算数运算

支持使用min、max等直接实现Comparator进行运算,如:

1
Optional<Integer> min = list.stream().min(Integer::compareTo);

也支持使用 IntSummaryStatistics 状态,如:

1
2
3
4
5
IntSummaryStatistics intSummaryStatistics = list.stream().mapToInt(x -> x).summaryStatistics();
System.out.println("最大值:"+intSummaryStatistics.getMax());
System.out.println("最小值:"+intSummaryStatistics.getMin());
System.out.println("平均值:"+intSummaryStatistics.getAverage());
System.out.println("数量:"+intSummaryStatistics.getCount());

7. 自定义方法使用Function Inteface实现复杂功能

Java 8 提供了三个常用的函数接口包括;

  • Function<T,R>
  • Predicate 判断
  • Consumer 消费

通过定义方法使用这三类接口,能完成更复杂的调用逻辑,在这里仅仅举一个小例子🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Integer> functionMethods2(List<Integer> a, Function<List<Integer>, List<Integer>> function) {
a = a.stream()
.filter(demoFilter(2))
.collect(Collectors.toList());
return function.apply(a);
}

Predicate<Integer> demoFilter(Integer number) {
return (a) -> !a.equals(number);
}

// 客户端调用
JavaTester javaTester = new JavaTester();
List<Integer> resList = javaTester.functionMethods2(list, a -> {
a.add(2);
return a;
});

Mysql中的INSTR和FIND_IN_SET函数应用

Posted on 2020-03-09 | In MYSQL
Words count in article: 367 | Reading time ≈ 1

INSTR函数

The INSTR() function returns the position of the first occurrence of a string in another string.

This function performs a case-insensitive search.

能够找到第一个匹配到位置,类似java中的firstIndexOf,并且是大小写不敏感的搜索

用法

首先:了解到 INSTR 函数的基本功能时能够 返回字符串在某一个字段的内容中的位置, 没有找到字符串返回0,否则返回位置(从1开始) .比如:

1
2
3
4
-- 结果为10
select INSTR("this is mysql","y")
-- 结果为0
select INSTR("this is mysql","a")

2如果instr当作查询条件,就能起到类似in的作用

1
2
-- 返回用户id为12的数据,同样的,也会返回id为 1,2的数据
select * from user u where INSTR('12',u.id);

更复杂的能够查询数据为字符串,并且用特定符号分隔的特定的数据

1
2
3
4
5
6
7
select (
select GROUP_CONCAT(u.username) as name
from `user` u
where INSTR(concat(',',(p.leaderIds),','),concat(',',(u.id),','))
)
from product p
WHERE p.product_uid ='02124ff31eac4c14934b045358b10cca'

FIND_IN_SET 函数

The FIND_IN_SET() function returns the position of a string within a list of strings.

能够返回list中的字符串位置(使用逗号分隔)

基础的就不多说,直接举例

1
2
-- 会只返回id为12,13的数据
select * from user u where FIND_IN_SET(u.id,'12,13')

find_in_set 和 instr 函数做例子上的对比时,发现instr是模糊匹配,只要符合都会查出来,而find_in_set是针对使用逗号分隔的对instr的进一步处理,只有出现在逗号之间的进行精确匹配.

SpringBoot整合JPA使用

Posted on 2020-03-05 | In SpringBoot
Words count in article: 3.4k | Reading time ≈ 15

SpringBoot整合JPA使用

整合JPA

SpringBoot整合JPA十分方便,在Pom中添加如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 数据库配置,使用druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

JPA基本配置

在application.properties 或者 application.yaml 文件中配置数据库链接和JPA的基本配置(本例使用yaml文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
druid:
initial-size: 5
max-active: 20
max-wait: 60000
stat-view-servlet:
login-password: admin
login-username: admin
password: 123456
url: jdbc:mysql://127.0.0.1:3306/web?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
jpa:
# 配置创建表使用的SQLDialect
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: true
# 自动更新数据库表
hibernate:
ddl-auto: update
properties:
hibernate:
# 使用@Query时SQL的dialect方言
dialect: org.hibernate.dialect.MySQL5InnoDBDialect

使用JPA

在SpringBoot中使用JPA十分简单,只要引用JPA的starter即可

实体类中属性

使用Spring Data JPA,与Mybatis不同,更多的是面向对象查询.所以构造对象实体类就非常重要,如果配置了自动更新表结构,就需要注意如何在实体中使用特定的注解完成对实体的配置.

1. @Entity

使用 @Entity 注解在类上,用来表示该类为一个实体类

2. @Table

使用 @Table 注解标注在类上, 可以指定使用 @Entity 注解标注的类所生成的数据库表信息,如表名称等.

1
2
3
4
5
@Entity // 指定这个类为实体类
@Table(name="user") // 指定@Entity标注的主表
public class User implements Serializable {
// ...
}

3. @Id,@Column

使用 @Id 注解标注在变量或者set方法上,用来标注该变量为主键ID.

使用 @Column 注解同样标注在变量上,能够设置生成表的各种详细信息,如字段名称,字段长度等等…

1
2
3
4
5
6
7
8
@Id
@Column(columnDefinition = "INT(11)")
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;

@Column(name = "is_deleted",columnDefinition = "TINYINT(1)",nullable =
false)
private Short isDeleted=0;

4. @GeneratedValue

使用 @GeneratedValue 注解标注在 和 @Id 相同的位置,用来表示主键ID的生成策略.该注解有两个属性,第一个是常用到的 strategy 即生成策略,第二个是不常用到的generator指定生成器.

strategy

GenerationType 包含四种策略:

  • TABLE,使用特定的表来存储生成的主键,也就是说会自动生成一种记录主键的表,一般会包含两个字段,第一个字段是字段生成策略的名称,第二个是ID的最大序列值.通常会和 @TableGenerator 一起来使用,能够指定特定表来生成主键,如果不指定,会默认生成一个名称为 sequence 的表来记录.
  • SEQUENCE,在特定的数据库,如ORACLE,不支持自增主键,但是会提供一种叫做序列的方式生成主键,此时就需要指定 SEQUENCE 为生成主键的策略,和TABLE相似,通常会使用 @SequenceGenerator 一起使用
  • IDENTITY ,遇到向MYSQL能够让主键自增的数据库,就可以指定生成策略为IDENTITY,生成表后, Mysql会默认将该字段设置为 ‘auto_increment’,
  • AUTO ,使用 @GeneratedValue 默认的生成策略,把具体生成主键的规则交给持久化引擎来实现,也是我使用最多的一种方式.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用 GenerationType.TABLE 指定生成的记录表,其中name必须,其他可选
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "roleSeq")
@TableGenerator(name = "roleSeq", allocationSize = 1, table = "seq_table", pkColumnName = "seq_id", valueColumnName = "seq_count")
private Integer id;
// 使用 SEQUENCE
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "menuSeq")
@SequenceGenerator(name = "menuSeq", initialValue = 1, allocationSize = 1, sequenceName = "MENU_SEQUENCE")
private Integer id;
// 使用 IDENTITY 进行自增
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// 使用AUTO,或者不指定策略
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id
generator

通过上面对策略的描述,也就明白generator的作用,就是指定特定name的Generator

5. @Transient

使用 @Transient 注解,标注在字段上,在JPA生成表字段时能够忽略该字段,让字段不出现在数据库中.

6. @Embedded和@Embeddable

当实体A出现在实体B中,但实体A不需要单独生成一张表的使用,使用@@Embedded和@Embeddable标注在类上,能够防止实体A生成数据库表.

7. @Temporal

使用 @Temporal 注解特殊标记在需要持久化的字段上,并且字段类型为 java.util.Date 或 java.util.Calendar的时候,可指定 TemporalType 来生成对应数据库中的时间格式(java.sql.xxxx).

8. 一对一,一对多,多对多关系

在使用关系型数据库中,免不了上述关系的生成,下面就来说一下,如何针对一对一关系、一对多关系、多对多关系的通用设置,在分别实验前,先了解一下 @JoinColumn 的作用

我认为的JoinColumn是特殊的@Column,当需要使用到“关系”时,需要使用到JoinColumn来替代普通的Column注解

OneToOne

使用OneToOne注解,指定两方关系是一对一的.比如User和Status,Status是User的一个属性,在User表中只需记录Stauts表中的主键,所以需要在User表(主)中声明一个Status(从)即可:

1
2
3
@OneToOne()
@JoinColumn(columnDefinition = "INT(11)",name = "status_id")
private Status status;
OneToMany、ManyToOne

使用 @OneToMany 或 @ManyToOne 注解来表示一对多、多对一的关系.典型的如Project之于Task,一个Project包含多个Task,所以Project为One,Task为Many.

如果在Project(主)中声明Task(从)时,可以使用 OneToMany 表示一对多关系

1
2
3
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name="project_id") // task表指向project表的外键
Set<Task> tasks;

也可以在多的一方使用 ManyToOne 注解,表示多对一的关系

1
2
3
4
@ManyToOne
// 在 one 的一方生成 name为 project_id 的字段,指向task
@JoinColumn(name="project_id")
Project project;

以上是单独使用 @OneToMany 或 @ManyToOne 构造单向的关系,但是这样有缺点,注定有一方找不到另外一方(只建立了单向关系).所以稳妥的方式是构建双向关系,在One和Many双方,使用两个注解相互指定

1
2
3
4
5
6
7
8
// Project中,其中mappedBy是双向关系中必须要使用的属性
@OneToMany(cascade = CascadeType.ALL,mappedBy ="project_id")
Set<Task> tasks;

// Task中
@ManyToOne
@JoinColumn(name="project_id")
Project project
ManyToMany

使用 @ManyToMany 注解来标注多对多关系,比如用户和角色之间就可以理解为多对多的关系,双方需要互相拥有多个.多地多关系需要使用中间表来维护两方关系.建立关系表使用@JoinTable注解来指定实现.

1
2
3
4
5
6
7
8
9
10
11
12
// User表中
@JSONField(name = "roles")
@ManyToMany(mappedBy = "users",fetch = FetchType.EAGER,cascade = CascadeType.ALL)
private Set<Role> roles;

// Role 表中
@JSONField(serialize = false)
@ManyToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
@JoinTable(name = "r_user_role",
joinColumns = {@JoinColumn(name = "role_id")},
inverseJoinColumns = {@JoinColumn(name = "user_id")})
private List<User> users;
关系使用时的坑
  1. 双向关系需要双方都进行维护,否则保存不上 :)
1
2
3
4
User user = one.get();
// 多对多关系需要双向关联保存才起作用
user.getRoles().add(role);
role.getUsers().add(user);
  1. 使用JSON时,如果出现循环引用导出溢出时,在一方加入设置防止序列化
1
2
3
4
5
6
7
// 我使用的是FastJosn,Jackson有不同的实现方式
@JSONField(serialize = false)
@ManyToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
@JoinTable(name = "r_user_routemeta",
joinColumns = {@JoinColumn(name = "role_id")},
inverseJoinColumns = {@JoinColumn(name = "meta_id")})
private Set<RouteMeta> routeMeta;

使用 Repository 查询

Repository 是 Spring Data JPA 中最重要的接口,使用实体类和实体类中的主键ID作为类型参数.其中CURDRepository为实体类提供了复杂增删改查的函数.

1. CrudRepository接口

1
public interface CrudRepository<T, ID> extends Repository<T, ID> {}

同样提供了特定的Repository,比如JPARepository、MongoRepository,这几个都是继承的 CrudRepository 接口,根据不同的技术特性暴露不同的接口方法.一般使用上直接继承CurdRepository就可以完成基本的工作.

2. PagingAndSortingRepository接口

如果使用分页或者排序需要继承 PagingAndSortingRepository 接口,这个接口中包含 Iterable<T> findAll(Sort sort); 和 Page<T> findAll(Pageable pageable);,这样能够具有查询时分页的特性

3. 接口中的名称推导查询

  • 通过命名查询,具体可查看相关文档,写的非常全 create-query
关键字 方法命名 sql where字句
And findByNameAndPwd where name= ? and pwd =?
Or findByNameOrSex where name= ? or sex=?
Is,Equals findById,findByIdEquals where id= ?
Between findByIdBetween where id between ? and ?
LessThan findByIdLessThan where id < ?
LessThanEquals findByIdLessThanEquals where id <= ?
GreaterThan findByIdGreaterThan where id > ?
GreaterThanEquals findByIdGreaterThanEquals where id > = ?
After findByIdAfter where id > ?
Before findByIdBefore where id < ?
IsNull findByNameIsNull where name is null
isNotNull,NotNull findByNameNotNull where name is not null
Like findByNameLike where name like ?
NotLike findByNameNotLike where name not like ?
StartingWith findByNameStartingWith where name like ‘?%’
EndingWith findByNameEndingWith where name like ‘%?’
Containing findByNameContaining where name like ‘%?%’
OrderBy findByIdOrderByXDesc where id=? order by x desc
Not findByNameNot where name <> ?
In findByIdIn(Collection<?> c) where id in (?)
NotIn findByIdNotIn(Collection<?> c) where id not in (?)
True findByAaaTue where aaa = true
False findByAaaFalse where aaa = false
IgnoreCase findByNameIgnoreCase where UPPER(name)=UPPER(?)

4. 使用 Repository 查询实例

  1. 声明一个接口继承特定的Repository
1
2
3
4
5
6
/**
* JpaRepository 继承自 PagingAndSortingRepository,其中类型参数
* 第一个是操作实体类的类型
* 第二个是操作实体类的标识ID
**/
public interface UserRepository extends JpaRepository<User,Integer> {}
  1. (可选)在接口中定义方法
1
2
// 具体名称推导查询可以查看上面的表格
List<User> findByLastname(String lastname);
  1. 使用@AutoWire注解inject到特定的service中调用
1
2
3
4
5
6
@Autowired
UserRepository userRepository;
@Override
public User getUserFromUserName(String name) {
return userRepository.findUserByName(name);
}

到这里,增删改查基本都能够实现,复杂的在于分页、多条件复杂查询.

5. 使用 PagingAndSortingRepository 进行分页查询

前面说到 PagingAndSortingRepository 提供了两个方法,一个是返回Iterable的 findAll(Sort) 方法,另一个是返回Page的findAll方法

  1. 在没有其他查询条件的情况下,直接定义一个Pageable变量,然后使用特定方法即可:
1
2
3
4
5
@Override
public Page<Book> findBookNoCriteria(Integer page,Integer size) {
Pageable pageable = new PageRequest(page, size, Sort.Direction.ASC, "id");
return bookRepository.findAll(pageable);
}
  1. 在存在多样的查询条件的情况下,还需要接口继承 JpaSpecificationExecutor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Page<Book> findBookCriteria(Integer page, Integer size, final BookQuery bookQuery) {
Pageable pageable = new PageRequest(page, size, Sort.Direction.ASC, "id");
Page<Book> bookPage = bookRepository.findAll(new Specification<Book>(){
@Override
public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
Predicate p1 = criteriaBuilder.equal(root.get("name").as(String.class), bookQuery.getName());
Predicate p2 = criteriaBuilder.equal(root.get("isbn").as(String.class), bookQuery.getIsbn());
Predicate p3 = criteriaBuilder.equal(root.get("author").as(String.class), bookQuery.getAuthor());
query.where(criteriaBuilder.and(p1,p2,p3));
return query.getRestriction();
}
},pageable);
return bookPage;
}

使用@Query语句查询

尽管通过上面的方法能够非常方便快捷的使用查询,但是有时候包含特殊字段的查询或者使用名称组合出来的方法名又长有丑…而且担心效率低,这时候JPA提供了一种类似Hibernate的方法,就是使用@Query进行查询

1
2
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);

或者使用原生SQL查询,将Query的参数nativeQuery设置为true

1
2
@Query(nativeQuery = true,value = "select u.name from user u where u.id:id limit 1")
String getUserNameById(Integer id);

使用QueryDSL进行复杂查询

如果上面的查询方式仍然满足不了需求,那么可以尝试使用QueryDSL进行查询 QueryDSL

1. 过程

  1. 添加MAVEN依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
   <dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>

<!-- 添加APT PLUGIN -->

<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>

JPAAnnotationProcessor 会查找所有的 Entity 注解标注的实体类在目标路径下生成 Qxxx 的类.第一次需要使用 maven install 生成一下.

  1. 使用查询
1
2
3
4
5
6
7
8
9
/**
* 注入 jpaQueryFactory
* @param entityManager
* @return
*/
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager){
return new JPAQueryFactory(entityManager);
}

在配置文件中使用@Bean把JPAQueryFactory注入到容器中,然后使用JpaQueryFactory查询.

1
2
3
4
5
6
7
8
9
10
private List<User> doGetrolePerson(Integer role,boolean in,List<User> users){
QUser user = QUser.user;
Predicate predicate = user.isNotNull().or(user.isNull());
if(in){
predicate = role == null ? predicate : ExpressionUtils.and(predicate, user.roles.any().id.in(role));
}else{
predicate = role == null ? predicate : ExpressionUtils.and(predicate, user.notIn(users));
}
return jpaQueryFactory.selectFrom(user).where(predicate).fetch();
}

以上是使用基本的QueryDSL进行查询的方式,更多的增删改查的方法可以参考上面引用的文档,这里不在过多描述.

2. 结合查询

SpringDataJPA 对 QueryDSL 提供了一个通用的Repository – QuerydslPredicateExecutor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface QuerydslPredicateExecutor<T> {
Optional<T> findOne(Predicate var1);

Iterable<T> findAll(Predicate var1);

Iterable<T> findAll(Predicate var1, Sort var2);

Iterable<T> findAll(Predicate var1, OrderSpecifier<?>... var2);

Iterable<T> findAll(OrderSpecifier<?>... var1);

Page<T> findAll(Predicate var1, Pageable var2);

long count(Predicate var1);

boolean exists(Predicate var1);
}

使用需要让定义的Repository继承这个QuerydslPredicateExecutor,然后调用方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Page<User> findUsers(Integer offset, Integer limit, String order, String search, Integer status, Integer sex, Integer role) throws Exception {
Sort sort = null;
String propertie = order.substring(1);
if (order.startsWith("-")) {
sort = Sort.by(propertie).descending();
} else if (order.startsWith("+")) {
sort = Sort.by(propertie).ascending();
}
if(offset == null || limit == null){
offset = 1;
limit = Integer.MAX_VALUE;
}
PageRequest pageRequest = new PageRequest(offset - 1, limit, sort);
QUser user = QUser.user;
com.querydsl.core.types.Predicate predicate = user.isNotNull().or(user.isNull());
predicate = search == null ? predicate : ExpressionUtils.and(predicate, user.name.like("%" + search + "%"));
predicate = sex == null ? predicate : ExpressionUtils.and(predicate, user.sex.id.eq(sex));
predicate = status == null ? predicate : ExpressionUtils.and(predicate, user.status.id.eq(status));
predicate = role == null ? predicate : ExpressionUtils.and(predicate, user.roles.any().id.eq(role));
return userRepository.findAll(predicate, pageRequest);
}
<i class="fa fa-angle-left"></i>123…12<i class="fa fa-angle-right"></i>
NanYin

NanYin

Was mich nicht umbringt, macht mich starker.

111 posts
16 categories
21 tags
RSS
GitHub E-Mail
近期文章
  • ThreadLocal
  • Java的四种引用类型
  • Markdown语法入门
  • IDEA设置代码注释模板和JavaDoc文档生成
  • 基于Java的WebService实践
0%
© 2023 NanYin
|
本站访客数:
|
博客全站共140.1k字
|