开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

TCP与UDP简单对比 TCP UDP TCP与UDP对比

发表于 2021-11-28

TCP

TCP是面向连接的,可靠的流协议
TCP提供可靠性传输,实行“顺序控制”或者“重发控制”,此外还具备“流控制(流量控制)”、“拥塞控制”,提高网络利用率等众多功能。

TCP三次握手

Image [2].png
为什么需要三次握手?
为了防止已经失效的请求报文突然又传到服务器引起错误,如果没有客户端第三次发送的第二次ACK确认报文,服务端会认为又建立了一个新的连接,但是客户端并不知道这个连接的存在,造成状态不一致。因此需要需要服务端收到ACK包才算建立连接。所以三次握手就是为了解决网络信道不可靠的问题。为了在不可靠的信道上建立可靠的连接
Image [3].png

四次挥手

Image [4].png

UDP

Image [6].png
UDP是不具备可靠性的数据报协议。细微的处理会交给上层的应用去处理。UDP虽然能确定发送消息的大小,但是不能保证消息一定会到达,因此,应用有时会根据自己的需要进行重发处理。

TCP与UDP对比

Image [7].png

可能有人会认为,鉴于TCP是可靠的传输协议,那么它一定优于UDP。
其实不然,TCP与UDP的优缺点无法简单地、绝对地去做比较。那么,对这两种协议
应该如何加以区分使用呢?下面,我就对此问题做一简单说明。

TCP用于在传输层有必要实现可靠传输的情况。由于它是面向有连接并具备顺序控制、重发控制等机制的,所以它可以为应用提供可靠传输;
而在一方面,UDP主要用于那些对高速传输和实时性有较高要求的通信或广播通信。我们举一个通过IP电话进行通话的例子。如果使用TCP,数据在传送途中如果丢失会被重发,但这样无法流畅地传输通话人的声音,会导致无法进行正常交流。而采用UDP,它不会进行重发处理。从而也就不会有声音大幅度延迟到达的问题。即使有部分数据丢失,也只是会影响某一小部分的通话。
此外,在多播与广播通信中也使用UDP而不是TCP.RIP、DHCP等基于广播的协议也要依赖于UDP。
因此,TCP和UDP应该根据应用的目的按需使用。

参考资料:

【1】计算机网络Andrew S.TanenBaum David J. WetheraH

【2】www.bilibili.com/video/BV1kV…

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM有哪些垃圾收集器

发表于 2021-11-28

Serial收集器

Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代 收集器的唯一选择。
大家只看名字就能够猜到,这个收集器是一个单线程工作的收集器,但它的“单线 程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强 调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。“Stop The World”这个词语也 许听起来很酷,但这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可知、不可控的情况 下把用户的正常工作的线程全部停掉,这对很多应用来说都是不能接受的。读者不妨试想一下,要是 你的电脑每运行一个小时就会暂停响应五分钟,你会有什么样的心情?下图示意了Serial/Serial Old收集器的运行过程。

Image.png

Image [2].png
每次回收时只有一个线程,因此串行回收器在并发能力较弱的计算机上,其专注性和独占性的特点往往能让其有更好的性能表现。
之所以需要“Stop the world”是因为不希望在进行垃圾收集的时候,又产生新的垃圾导致回收不干净。有一个很好的例子:
比如你在清理垃圾的时候,一边清扫一边丢垃圾肯定永远都扫不干净,所以需要规定打扫的时候不能扔垃圾,还比如,去公共卫生间时,经常会遇到阿姨在里面打扫垃圾,在外面挂一个牌子——保洁中,暂停使用。
这就是简单粗暴地Stop the world,虽然简单粗暴,但是效率也很高,如果上述的打扫只需要几分钟或者阿姨保洁只需要几分钟,我们也是完全可以接受的。JVM的垃圾收集与此有一定的相似之处,但是又远比这些简单的工作要复杂得多。虽然暂停用户线程的时间一直在优化而减少,但是Serial依然因为其“Stop The World”收到诟病。
但事实上,迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内 存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理 器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以 获得最高的单线程收集效率。在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚 拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的 内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一 百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
总结:Serial是一个单线程的垃圾收集器,适用于桌面客户端应用,其新生代回收采用复制算法,老年代回收采用标记整理法。

ParNew收集器

因为Serial在GC的时候是单线程的,而我们又要尽量地缩短STW(Stop The World)的时间,因此在进行GC的时候可以采用多线程并发的方式进行,而ParNew收集器实质上就是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规 则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。ParNew收 集器的工作过程如下所示

Image [3].png

Image [4].png

ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它 却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。

总结:ParNew收集器是Serial的并行版本,它是新生代收集器,依然采用复制算法对新生代进行收集,老年代可搭配Serial Old进行收集,因为它能够搭配CMS收集器,ParNew收集器才成为服务端默认的新生代垃圾收集器。

Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是 能够并行收集的多线程收集器……Parallel Scavenge的诸多特性从表面上看和ParNew非常相似,那它有 什么特别之处呢? Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能 地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值

Image [6].png

Image [5].png
Parallel Scavenge收集器设计的目的是让开发者能够让开发者可以控制垃圾收集器的吞吐量。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
总结:Prallel Scavenge也是一款新生代垃圾收集器,老年代可以搭配Serial Old和Parallel Old进行回收,其特点是可以控制最大垃圾收集停顿时机和吞吐量

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
总结:Serial收集器的老年代版本,使用标记-整理算法

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实 现。这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。
Parallel Old收集器的工作过程如图:

Image [7].png
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组 合。
总结:Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为 关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非 常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作 过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:
1)初始标记(CMS initial mark)
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
2)并发标记(CMS concurrent mark)
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;并发标记的时间可能长达整个垃圾收集过程的70%以上。
3)重新标记(CMS remark)
重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
4)并发清除(CMS concurrent sweep)
并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。
由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的,整个过程如下图:

image.png
CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:
并发收集、低停顿,一些官 方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector)。CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:

  1. 虽然不会导致用户线程停顿,但是会增加GC线程,因此他会占用一部分线程资源而导致用户线程变慢,降低吞吐量。
  2. CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
  3. CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。(老年代的收集器大都采用标记整理算法,就是为了减少产生空间碎片,因为老年代有很多的大对象需要连续的空间来存储

CMS相关的参数:

参数 类型 默认值 备注
-XX:+UseConcMarkSweepGC boolean false 启用CMS,老年代采用CMS收集器收集
-XX:+CMSScavengeBeforeRemark boolean false 在重新标记前先执行一次Minor GC
–XX:-UseCMSCompactAtFullCollection boolean false 对老年代进行压缩,可以消除碎片,但是可能会带来性能消耗
-XX:CMSFullGCsBeforeCompaction=n uintx 0 CMS进行n次full gc后进行一次压缩。如果n=0,每次full gc后都会进行碎片压缩。如果n=0,每次full gc后都会进行碎片压缩
–XX:+CMSIncrementalMode boolean false 并发收集递增进行,周期性把cpu资源让给正在运行的应用
–XX:+CMSIncrementalPacing boolean false 根据应用程序的行为自动调整每次执行的垃圾回收任务的数量
–XX:ParallelGCThreads=n uintx 并发回收线程数量:(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8)
-XX:CMSInitiatingOccupancyFractio=n uintx jdk5 默认是68% jdk6默认92% 当老年代内存使用达到n%,开始回收
注意:有一些参数是联动的,比如设置了UseCMSCompactAtFullCollection之后,对老年代进行压缩的参数CMSFullGCsBeforeCompaction才会生效

Garbage First(G1)收集器

G1垃圾收集器的内容比较多,实现比较复杂因此放到下一篇笔记了

【参考】

【1】《深入理解JAVA虚拟机》

【2】bilibili的UP主子烁爱学习

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

总结一下JVM类加载机制

发表于 2021-11-28

类加载过程:

Image.png

Image [2].png

类加载的时机

v2-1afd38933d28084b21289e59b87fd850_r.jpg
关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之
前开始):

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
* 使用new关键字实例化对象的时候。
* 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外) 的时候。
* 调用一个类型的静态方法的时候。
  1. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  2. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  3. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  4. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

类加载的过程

加载

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,希望读者没有混淆这两个看起来很相似的名词。在加载阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

虚拟机验证到输入的字节流如不符合Class文件格式的约束,就应当抛出一个java.lang.VerifyError异常或其子类异常。验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了

解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7 类符号引用进行,分别对应于常量池的8种常量类型:

  • CONSTANT_Class_info
  • CONSTANT_Fieldref_info
  • CONSTANT_Methodref_info
  • CONSTANT_InterfaceMethodref_info
  • CONSTANT_MethodType_info
  • CONSTANT_MethodHandle_info
  • CONSTANT_Dynamic_info
  • CONSTANT_InvokeDynamic_info

初始化

类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶 段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控 制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程 序。

v2-1857def13e579d3df95fb26d3c308af3_r.jpg

类加载器

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节 流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动 作的代码被称为“类加载器”(Class Loader)。

类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。对于 任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每 一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相 等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

双亲委派模型

Image [3].png
站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有 的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

启动类加载器(Bootstrap Class Loader):这个类加载器负责加载存放<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够 识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类 库加载到虚拟机的内存中。

扩展类加载器(Extension Class Loader):这个类加载器是在sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。

应用程序类加载器(Application Class Loader):这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

JDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的,如果用户认为有必要,还可 以加入自定义的类加载器来进行拓展,典型的如增加除了磁盘位置之外的Class文件来源,或者通过类 加载器实现类的隔离、重载等功能。这些类加载器之间的协作关系“通常”会上图所示。

上图展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

双亲委派模型好处:使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类 加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一 个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类 在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个 类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的 ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应 用程序将会变得一片混乱。

【参考】

【1】《深入理解JVM虚拟机》

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM内存结构

发表于 2021-11-28

概述

其实 Java 虚拟机的内存结构并不是官方的说法,在《Java 虚拟机规范》中用的是「运行时数据区」这个术语。但很多时候这个名词并不是很形象,再加上日积月累的习惯,我们都习惯用虚拟机内存结构这个说法了
根据《Java 虚拟机规范》中的说法,Java 虚拟机的内存结构可以分为公有和私有两部分。公有指的是所有线程都共享的部分,指的是 Java 堆、方法区、常量池。私有指的是每个线程的私有数据,包括:PC寄存器、Java 虚拟机栈、本地方法栈。

2020051912560764.png

2020051915224888.png

公有部分

在 Java 虚拟机中,线程共享部分包括

Java堆

Java 堆指的是从 JVM 划分出来的一块区域,这块区域专门用于 Java 实例对象的内存分配,几乎所有实例对象都在会这里进行内存的分配。之所以说几乎是因为有特殊情况,有些时候小对象会直接在栈上进行分配,这种现象我们称之为「栈上分配」。这里并不深入介绍,后续有章节会介绍。
深入说说 Java 堆。

Java 堆根据对象存活时间的不同,Java 堆还被分为年轻代、老年代两个区域,年轻代还被进一步划分为 Eden 区、From Survivor 0、To Survivor 1 区。如下图所示。

595137-20190103103329413-247778313.png
当有对象需要分配时,一个对象永远优先被分配在年轻代的 Eden 区,等到 Eden 区域内存不够时,Java 虚拟机会启动垃圾回收。此时 Eden 区中没有被引用的对象的内存就会被回收,而一些存活时间较长的对象则会进入到老年代。在 JVM 中有一个名为 -XX:MaxTenuringThreshold 的参数专门用来设置晋升到老年代所需要经历的 GC 次数,即在年轻代的对象经过了指定次数的 GC 后,将在下次 GC 时进入老年代。

这里让我们思考一个问题:为什么 Java 堆要进行这样一个区域划分呢?

根据我们的经验,虚拟机中的对象必然有存活时间长的对象,也有存活时间短的对象,这是一个普遍存在的正态分布规律。如果我们将其混在一起,那么因为存活时间短的对象有很多,那么势必导致较为频繁的垃圾回收。而垃圾回收时不得不对所有内存都进行扫描,但其实有一部分对象,它们存活时间很长,对他们进行扫描完全是浪费时间。因此为了提高垃圾回收效率,分区就理所当然了。

另外一个值得我们思考的问题是:为什么默认的虚拟机配置,Eden:from :to = 8:1:1 呢?

其实这是IBM公司根据大量统计得出的结果。根据IBM公司对对象存活时间的统计,他们发现 80% 的对象存活时间都很短。于是他们将 Eden 区设置为年轻代的 80%,这样可以减少内存空间的浪费,提高内存空间利用率。

方法区、常量池

方法区与永久代、元空间(MetaSpace)

方法区是Java虚拟机规范中的概念,是一种Java虚拟机规范,而永久代,MetaSpace则是对方法区的不同实现。要区分二者的概念。

方法区指的是存储 Java 类字节码数据的一块区域,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造方法等。可以看到常量池其实是存放在方法区中的,但《Java 虚拟机规范》将常量池和方法区放在同一个等级上。

方法区在不同版本的虚拟机有不同的表现形式,例如在 1.7 版本的 HotSpot 虚拟机中,方法区被称为永久代(Permanent Space),而在 JDK 1.8 中则被称之为 MetaSpace。

字符串常量池

并不是所有的常量池都在方法区中,因为自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中,在JDK1.8环境下运行一下代码并不会导致方法区溢出(String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用)

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码public class RuntimeConstantPoolOOM_1 {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<String>();
// 在short范围内足以让6MB的PermSize产生OOM了
short i = 0;
while (true) {
set.add(String.valueOf(i++).intern());
}
}
}

相反,使用-Xmx参数限制最大堆到6MB就能够看到
-Xms6m -Xmx6m -Xss128k -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\heapdump

1
2
3
4
5
6
php复制代码Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.newNode(HashMap.java:1750)
at java.util.HashMap.putVal(HashMap.java:631)
at java.util.HashMap.put(HashMap.java:612)
at java.util.HashSet.add(HashSet.java:220)
at jvm.RuntimeConstantPoolOOM_1.main(RuntimeConstantPoolOOM_1.java:17)

说明JDK1.8把字符串常量池放在堆中

私有部分

Java 堆以及方法区的数据是共享的,但是有一些部分则是线程私有的。线程私有部分可以分为:PC 寄存器、Java 虚拟机栈、本地方法栈三大部分。

PC寄存器(程序计数器)

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

注意:
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

为什么需要程序计数器 ?

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。

Java 虚拟机栈

20200516185854133.png
Java 虚拟机栈,这个栈与线程同时创建,命周期与程序计数器相同,就是我们平时说的栈,用来存储栈帧,即存储局部变量与一些过程结果的地方。栈帧存储的数据包括:局部变量表、操作数栈等信息。

虚拟机栈-过程详情描述

①每创建一个线程,虚拟机就会为这个线程创建一个虚拟机栈,
②虚拟机栈表示Java方法执行的内存模型,每调用一个方法,就会生成一个栈帧(Stack Frame)用于存储方法的本地变量表、操作栈、方法出口等信息,当这个方法执行完后,就会弹出相应的栈帧。

本地方法栈

当 Java 虚拟机使用其他语言(例如 C 语言)来实现指令集解释器时,也会使用到本地方法栈。如果 Java 虚拟机不支持 natvie 方法,并且自己也不依赖传统栈的话,可以无需支持本地方法栈。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。

【参考】

【1】深入理解Java虚拟机(第3版)

【2】www.cnblogs.com/chanshuyi/p…

【3】blog.csdn.net/weixin_4539…

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

DataStream的设计与实现 DataStream的设计

发表于 2021-11-28

DataStream的设计与实现

一. DataStream的含义以及成员构成

1. DataStream的含义

由源码翻译可知,DataStream是一个集合,包含了相同类型元素的数据流,一个DataStream可以通过transformation(如Map,filter操作)变成另外一种DataStream.同时,DataStream主要用于表达业务逻辑,实际上并没有存储真实数据

2.DataStream的继承关系

image.png

图一 DataStream的成员变量以及子类

1.由图一可知,DataStream主要是由StreamExecutionEnvironment 以及transformation构成.其中SingleOutputStreamOperator、keyedStream继承了DataStream抽象类.

  • transformation是当前的DataStream对应的上一次的转换操作,即上个阶段的流完成这个transformation操作然后得到了当前流。
  • StreamExecutionEnvironment会将DataStream之间的转换操作存储至StreamExecutionEnvironment的List中.然后基于这些转换操作构建左右Pipeline拓扑,用于描述整个作业的计算逻辑

图一中也标识了DataStreamSink,主要是用来从数据流拓扑结构生成元素,同时和其他DataStream进行一个区分

3. DataStream的方法

image.png

图二 DataStream的方法介绍

DataStream有大量的transform(转换)操作方法可以调用,主要是将DataStream流进行操作变换成为另外一种流,其底层是调用了Transform方法.

二、DataStream API的应用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码
1. StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
2. DataStream<String> inputStream = env.readTextFile("xxxx.csv");
3. SingleOutputStreamOperator<Role> dataStream = inputStream.map(new MapFunction<String, Role>() {
@Override
public Role map(String value) throws Exception {
String[] arrs = value.split(",");
return new Role(Long.parseLong(arrs[0]), Long.parseLong(arrs[1]), Integer.parseInt(arrs[2]), arrs[3], Long.parseLong(arrs[4]));
}
}).assignTimestampsAndWatermarks(new AscendingTimestampExtractor<Role>() {
@Override
public long extractAscendingTimestamp(Role element) {
return element.timestamp * 1000L;
}
});
  1. 第一句首先构建了一个StreamExecutionEnvironment对象env,env.readText会生成DataStreamSource
  2. 第二句env.readTextFile方法中构建出了DataStream对象,DataStreamSource继承了DataStream
  3. 第三句DataStream对象调用了map方法,生成了SingleOutputStreamOperator对象,因此由当前的dataStream转换成了另外一个dataStream

三、 DataStream的map方法调用流程

image.png

图三 DataStream的map方法调用流程

如图三所示,是DataStream的map方法的调用流程,由图得知,所有调用的方法都在dataStream类中进行调用跳转.最后调用了doTransform方法来生成DataStream,返回SingleOutputStreamOperator.

值得注意的是

SingleOutputStreamOperator是继承了DataStream类,属于特殊的DtataStream

SingleOutputStreamOperator是继承了DataStream类,属于特殊的DtataStream
SingleOutputStreamOperator是继承了DataStream类,属于特殊的DtataStream

因此transformation操作本质上就是一种datastream变成另外一种datastream

doTransform方法实现逻辑
  • 如图三所示, 在调用doTransform方法的时候,主要是把之前map操作中的mapfunction封装成operator.同时,将当前datastream的transformation作为参数,一起构建成新的transformation
  • 将新的tranformation加入至executionenvironment中,用于构建streamGraph
  • 将excutionenvironment和tranformation作为参数,构建新的DataStream

DataStream.doTranform()方法定义

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
java复制代码	protected <R> SingleOutputStreamOperator<R> doTransform(
String operatorName,
TypeInformation<R> outTypeInfo,
StreamOperatorFactory<R> operatorFactory) {

// 获取上一次tranformation的输出类型
transformation.getOutputType();

//将本次tranformation作为输入,生成一个新的tranformation
OneInputTransformation<T, R> resultTransform = new OneInputTransformation<>(
this.transformation,
operatorName,
operatorFactory,
outTypeInfo,
environment.getParallelism());

//将新的environment和新的tranformation作为参数,构建成新的datastream(SingleOutputStreamOperator)
@SuppressWarnings({"unchecked", "rawtypes"})
SingleOutputStreamOperator<R> returnStream = new SingleOutputStreamOperator(environment, resultTransform);

//将当前新生成的tranformation加入到executionenvironment中,用于生成streamGraph
getExecutionEnvironment().addOperator(resultTransform);

return returnStream;
}

参考文献

Flink设计与实现:核心原理与源码解析

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot 实战:在 RequestBody 中优

发表于 2021-11-28

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」

本文被《Spring Boot 实战》专栏收录。

你好,我是看山。

前文说到 优雅的使用枚举参数 和 实现原理,本文继续说一下如何在 RequestBody 中优雅使用枚举。

本文先上实战,说一下如何实现。在 优雅的使用枚举参数 代码的基础上,我们继续实现。如果想要获取源码,可以关注公号「看山的小屋」,回复 spring 即可。

确认需求

需求与前文类似,只不过这里需要是在 RequestBody 中使用。与前文不同的是,这种请求是通过 Http Body 的方式传输到后端,通常是 json 或 xml 格式,Spring 默认借助 Jackson 反序列化为对象。

同样的,我们需要在枚举中定义 int 类型的 id、String 类型的 code,id 取值不限于序号(即从 0 开始的 orinal 数据),code 不限于 name。客户端请求过程中,可以传 id,可以传 code,也可以传 name。服务端只需要在对象中定义一个枚举参数,不需要额外的转换,即可得到枚举值。

好了,接下来我们定义一下枚举对象。

定义枚举和对象

先定义我们的枚举类GenderIdCodeEnum,包含 id 和 code 两个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public enum GenderIdCodeEnum implements IdCodeBaseEnum {
    MALE(1, "male"),
    FEMALE(2, "female");

    private final Integer id;
    private final String code;

    GenderIdCodeEnum(Integer id, String code) {
        this.id = id;
        this.code = code;
    }

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public Integer getId() {
        return id;
    }
}

这个枚举类的要求与前文一致,不清楚的可以再去看一下。

在定义一个包装类GenderIdCodeRequestBody,用于接收 json 数据的请求体:

1
2
3
4
5
6
java复制代码@Data
public class GenderIdCodeRequestBody {
    private String name;
    private GenderIdCodeEnum gender;
    private long timestamp;
}

除了GenderIdCodeEnum参数外,其他都是示例,所以随便定义一下。

实现转换逻辑

前奏铺垫好,接下来入正题了。Jackson 提供了两种方案:

  • 方案一:精准攻击,指定需要转换的字段,不影响其他类对象中的字段
  • 方案二:全范围攻击,所有借助 Jackson 反序列化的枚举字段,全部具备自动转换功能

方案一:精准攻击

这种方案中,我们首先需要实现JsonDeserialize抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class IdCodeToEnumDeserializer extends JsonDeserializer<BaseEnum> {
    @Override
    public BaseEnum deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
            throws IOException {
        final String param = jsonParser.getText();// 1
        final JsonStreamContext parsingContext = jsonParser.getParsingContext();// 2
        final String currentName = parsingContext.getCurrentName();// 3
        final Object currentValue = parsingContext.getCurrentValue();// 4
        try {
            final Field declaredField = currentValue.getClass().getDeclaredField(currentName);// 5
            final Class<?> targetType = declaredField.getType();// 6
            final Method createMethod = targetType.getDeclaredMethod("create", Object.class);// 7
            return (BaseEnum) createMethod.invoke(null, param);// 8
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | NoSuchFieldException e) {
            throw new CodeBaseException(ErrorResponseEnum.PARAMS_ENUM_NOT_MATCH, new Object[] {param}, "", e);
        }
    }
}

然后在指定枚举字段上定义@JsonDeserialize注解,比如:

1
2
java复制代码@JsonDeserialize(using = IdCodeToEnumDeserializer.class)
private GenderIdCodeEnum gender;

具体说一下每行的作用:

  1. 获取参数值。根据需要,此处可能是 id、code 或 name,也就是源值,需要将其转换为枚举;
  2. 获取转换上线文,这个是为 3、4 步做准备的;
  3. 获取标记@JsonDeserialize注解的字段,此时currentName的值是gender;
  4. 获取包装对象,也就是GenderIdCodeRequestBody对象;
  5. 根据包装对象的Class对象,以及字段名gender获取Field对象,为第 5 步做准备;
  6. 获取gender字段对应的枚举类型,也即是GenderIdCodeEnum。之所以这样做,是要实现一个通用的反序列化类;
  7. 这里是写死的一种实现,就是在枚举类中,需要定义一个静态方法,方法名是create,请求参数是Object;
  8. 通过反射调用create方法,将第一步获取的请求参数传入。

我们来看一下枚举类中定义的create方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public static GenderIdCodeEnum create(Object code) {
    final String stringCode = code.toString();
    final Integer intCode = BaseEnum.adapter(stringCode);
    for (GenderIdCodeEnum item : values()) {
        if (Objects.equals(stringCode, item.name())) {
            return item;
        }
        if (Objects.equals(item.getCode(), stringCode)) {
            return item;
        }
        if (Objects.equals(item.getId(), intCode)) {
            return item;
        }
    }
    return null;
}

为了性能考虑,我们可以提前定义三组 map,分别以 id、code、name 为 key,以枚举值为 value,这样就可以通过 O(1) 的时间复杂度返回了。可以参考前文的Converter类的实现逻辑。

这样,我们就可以实现精准转换了。

方案二:全范围攻击

这种方案是全范围攻击了,只要是 Jackson 参与的反序列化,只要其中有目标枚举参数,就会受到这种进入这种方案的逻辑中。这种方案是在枚举类中定义一个静态转换方法,通过@JsonCreator注解注释,Jackson 就会自动转换了。

这个方法的定义与方案一中的create方法完全一致,所以只需要在create方法上加上注解即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@JsonCreator(mode = Mode.DELEGATING)
public static GenderIdCodeEnum create(Object code) {
    final String stringCode = code.toString();
    final Integer intCode = BaseEnum.adapter(stringCode);
    for (GenderIdCodeEnum item : values()) {
        if (Objects.equals(stringCode, item.name())) {
            return item;
        }
        if (Objects.equals(item.getCode(), stringCode)) {
            return item;
        }
        if (Objects.equals(item.getId(), intCode)) {
            return item;
        }
    }
    return null;
}

其中Mode类有四个值:DEFAULT、DELEGATING、PROPERTIES、DISABLED,这四种的差别会在原理篇中说明。还是那句话,对于应用类技术,我们可以先知其然,再知其所以然,也一定要知其所以然。

测试

先定义一个 controller 方法:

1
2
3
4
5
java复制代码@PostMapping("gender-id-code-request-body")
public GenderIdCodeRequestBody bodyGenderIdCode(@RequestBody GenderIdCodeRequestBody genderRequest) {
    genderRequest.setTimestamp(System.currentTimeMillis());
    return genderRequest;
}

然后定义测试用例,还是借助 JUnit5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@ParameterizedTest
@ValueSource(strings = {"\"MALE\"", "\"male\"", "\"1\"", "1"})
void postGenderIdCode(String gender) throws Exception {
    final String result = mockMvc.perform(
            MockMvcRequestBuilders.post("/echo/gender-id-code-request-body")
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"gender\": " + gender + ", \"name\": \"看山\"}")
    )
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andDo(MockMvcResultHandlers.print())
            .andReturn()
            .getResponse()
            .getContentAsString();

    ObjectMapper objectMapper = new ObjectMapper();
    final GenderIdCodeRequestBody genderRequest = objectMapper.readValue(result, GenderIdCodeRequestBody.class);
    Assertions.assertEquals(GenderIdCodeEnum.MALE, genderRequest.getGender());
    Assertions.assertEquals("看山", genderRequest.getName());
    Assertions.assertTrue(genderRequest.getTimestamp() > 0);
}

文末总结

本文主要说明了如何在 RequestBody 中优雅的使用枚举参数,借助了 Jackson 的反序列化扩展,可以定制类型转换逻辑。碍于文章篇幅,没有罗列大段代码。关注公号「看山的小屋」回复 spring 可以获取源码。关注我,下一篇我们进入原理篇。

推荐阅读

  • SpringBoot 实战:一招实现结果的优雅响应
  • SpringBoot 实战:如何优雅的处理异常
  • SpringBoot 实战:通过 BeanPostProcessor 动态注入 ID 生成器
  • SpringBoot 实战:自定义 Filter 优雅获取请求参数和响应结果
  • SpringBoot 实战:优雅的使用枚举参数
  • SpringBoot 实战:优雅的使用枚举参数(原理篇)
  • SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数
  • SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数(原理篇)
  • SpringBoot 实战:JUnit5+MockMvc+Mockito 做好单元测试
  • SpringBoot 实战:加载和读取资源文件内容

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

spark调优(三):持久化减少二次查询 1 起因 2

发表于 2021-11-28

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」

大家好,我是怀瑾握瑜,一只大数据萌新,家有两只吞金兽,嘉与嘉,上能code下能teach的全能奶爸

如果您喜欢我的文章,可以[关注⭐]+[点赞👍]+[评论📃],您的三连是我前进的动力,期待与您共同成长~


  1. 起因

在我们接收到数据的时候,通常都需要etl处理一下,但原始数据最好也是入库保存一下最好,这样一份数据,我们就使用了2次。

Spark中对于一个RDD执行多次算子的默认原理是这样的:每次你对一个RDD执行一个算子操作时,都会重新从源头处计算一遍,计算出那个RDD来,然后再对这个RDD执行你的算子操作。这种方式的性能是很差的。

所以如果你先保存原始数据,再筛选一下的时候,会发现数据会重新被加载,这样是很浪费时间的。

  1. 优化开始

对多次使用的RDD进行持久化。此时Spark就会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头处重新计算一遍这个RDD,再执行算子操作。

如果要对一个RDD进行持久化,只要对这个RDD调用cache()和persist()

cache()方法表示:使用非序列化的方式将RDD中的数据全部尝试持久化到内存中。

persist()方法表示:手动选择持久化级别,并使用指定的方式进行持久化。

1
2
3
4
ini复制代码df = sc.sql(sql)
df1 = df.persist()
df1.createOrReplaceTempView(temp_table_name)
subdf = sc.sql(select * from temp_table_name)

这种情况就不会重新加载RDD。

对于persist()方法而言,我们可以根据不同的业务场景选择不同的持久化级别。

持久化级别 含义解释
MEMORY_ONLY 使用未序列化的Java对象格式,将数据保存在内存中。如果内存不够存放所有的数据,则数据可能就不会进行持久化。那么下次对这个RDD执行算子操作时,那些没有被持久化的数据,需要从源头处重新计算一遍。这是默认的持久化策略,使用cache()方法时,实际就是使用的这种持久化策略。
MEMORY_AND_DISK 使用未序列化的Java对象格式,优先尝试将数据保存在内存中。如果内存不够存放所有的数据,会将数据写入磁盘文件中,下次对这个RDD执行算子时,持久化在磁盘文件中的数据会被读取出来使用。
MEMORY_ONLY_SER 基本含义同MEMORY_ONLY。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。
MEMORY_AND_DISK_SER 基本含义同MEMORY_AND_DISK。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。
DISK_ONLY 使用未序列化的Java对象格式,将数据全部写入磁盘文件中。
MEMORY_ONLY_2, MEMORY_AND_DISK_2, 等等. 对于上述任意一种持久化策略,如果加上后缀_2,代表的是将每个持久化的数据,都复制一份副本,并将副本保存到其他节点上。这种基于副本的持久化机制主要用于进行容错。假如某个节点挂掉,节点的内存或磁盘中的持久化数据丢失了,那么后续对RDD计算时还可以使用该数据在其他节点上的副本。如果没有副本的话,就只能将这些数据从源头处重新计算一遍了。

如何选择一种最合适的持久化策略

  • 默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。
  • 如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。
  • 如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。
  • 通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。

结束语

如果您喜欢我的文章,可以[关注⭐]+[点赞👍]+[评论📃],您的三连是我前进的动力,期待与您共同成长~

可关注公众号【怀瑾握瑜的嘉与嘉】,获取资源下载方式

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL学习-字符串字段如何添加索引

发表于 2021-11-28

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

作者:汤圆

个人博客:javalover.cc

前言

给字符串字段添加索引有多种方式,最简单的就是给整个字段添加索引,这样的好处显而易见就是查询字符串字段时不需要回表操作;

当然还有其他的添加索引的方式,比如给字符串的前半部分添加索引、给字符串倒序处理后再添加索引、以及ha sh处理后再添加索引;

下面我们就挨个介绍,给字符串字段添加索引的这几种方式;

目录

  1. 整个字段添加索引
  2. 前部分字段添加索引
  3. 倒序字段添加索引
  4. hash字段添加索引

正文

这里我们以下面的SQL语句为例,进行介绍:id_card是用户的身份证号

1
sql复制代码select * from t_user where id_card = 142733xxxxx

1. 整个字段添加索引

如果简单的给整个字段id_card添加索引,那么查询也会很简单;

大致的一个查询步骤如下:

  1. 先去id_card索引树中定位到具体的索引值142733xxxxxx;
  2. 然后根据索引中的主键值,回表查询所有的数据,并添加到结果集;
  3. 继续去索引树中查询下一条记录,如果满足则重复步骤2;不满足就结束;

这种方式最大的好处就是:回表查询时,查询的都是满足条件的行;

即都是精确查询,非模糊查询;

2. 前部分字段添加索引

即前缀索引,就是给字符串的前部分N个字符添加索引:add index index_card(id_card(6))

这里我们指定的6就是说给前6个字符添加索引;

这样的查询步骤如下:

  1. 先去id_card索引树中定位前缀为142733的索引值(可能有多个,这里会取第一个);
  2. 然后根据索引中的主键值,回表判断id_card的值是否和where条件的id_card值一致;
    1. 如果一致,则查询行记录的所有数据,并添加到结果集;
    2. 如果不一致,则重复步骤1,继续根据前缀142733定位索引;
  3. 继续去索引中查询下一条记录,如果满足则重复步骤2;不满足就结束;

这种方式跟第一种比起来,有一个很明显的区别就是:回表查询数据时,需要先比对id_card的完整字符串是否跟筛选条件where中的id_card值一致(因为索引是根据前缀6个字符索引的,所以后面的字符串不确定是不是一致);

即都是模糊查询,非精确查询;

这里的优点就是节省空间,比如这里的身份证号,默认有18位,但是用了前缀索引后只需要6位;

但是缺点也很明显:

1
2
markdown复制代码1. 会扫描多行数据,而且是在回表操作之后,才能确定哪些数据是有效;
1. 会影响到覆盖索引:因为每次前缀索引,都要回表查看记录,比较查询的值是否相等(因为前缀索引只包括了前面几个字符);所以如果查询语句有用到覆盖索引,那么覆盖索引就会失效

覆盖索引:就是如果查询的数据只有索引值和主键值,那么就不用回表查询记录

问:那要怎么优化呢?

答:选择合适的前缀索引长度,确保前缀的索引值足够区分字符串,也就是索引的区分度越高,前缀索引的查询性能越好;

比如上面的身份证号的例子,前6位是根据省市县进行区分的(细节如下图所示),那就是说:如果是一个县城或者市区的人,那么他们的前6位都是一致的,这样就会大大增加查询的工作量;

这时我们就可以稍微往后加几位,将生日包括进去,这样索引的区分度就会大大增加,查询的工作量会减小很多;

查看源图像

3. 倒序字段添加索引

就是将字段中的字符串倒序存储,然后再给前部分字段添加索引;也就是先倒序、再前缀索引;

这样看起来会有点走弯路,但是对于上面的身份证号的例子来说,会很适用;

mysql中倒序处理对应的函数为:reverse();

因为身份证号的后面几位区分度远高于前面几位,这样我们在查询的时候只需要用reverse函数进行倒序查询就可以了,如下所示:

1
sql复制代码select * from t_user where id_card = reverse(142733xxxxx)

4. hash字段添加索引

就是先将字段值hash处理再添加索引;

这个跟上面的倒序存储添加索引有点类似,都是要对现有的字段值进行处理后再存储;

不过这个hash处理字段后,结果会有冲突的可能性,所以这里的hash处理结果需要额外添加一个字段用来存储;

mysql中hash处理对应的函数为:crc32();

此时插入数据时,需要对字段值进行crc32()处理后再插入;

查询数据时,需同时查询crc32()的结果和原有的字段:因为crc32的结果有冲突的可能(虽然几率比较小)

1
sql复制代码select * from t_user where id_card_crc=crc32('142733xxxx') and id_card='142733xxxx'

总结

上面介绍了四种存储方式,前两种比较简单,整个字段索引和前缀索引;

后面的两种会有点复杂,这里我们总结下他们两种的区别:

倒序存储 hash存储
存储空间 正常 多一个字段,来存储hash值
CPU消耗 调用reverse()函数 调用crc32()函数,比reverse()消耗高一些
查询效率 可能会扫描多行 只要hash值不冲突,就只需要扫描一行
范围查找 不支持 不支持

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

前缀树(Trie)构建与应用

发表于 2021-11-28

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

1、前缀树介绍

  • 前缀树(Trie树),即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。
  • 典型应用是用于 统计 和 排序 大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
  • 优点:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
  • 以下介绍图源于网络

字典树.jpg

字典树1.jpg

2、前缀树应用(剑指 Offer 替换单词)

  • 在英语中,有一个叫做 词根(root) 的概念,它可以跟着其他一些词组成另一个较长的单词——我们称这个词为 继承词(successor)。例如,词根an,跟随着单词 other(其他),可以形成新的单词 another(另一个)。现在,给定一个由许多词根组成的词典和一个句子,需要将句子中的所有继承词用词根替换掉。如果继承词有许多可以形成它的词根,则用最短的词根替换它。需要输出替换之后的句子。
  • 示例1
1
2
3
ini复制代码输入: dictionary = ["cat","bat","rat"], 
sentence = "the cattle was rattled by the battery"
输出: "the cat was rat by the bat"
  • 示例2
1
2
3
ini复制代码输入:dictionary = ["ac","ab"], 
sentence = "it is abnormal that this solution is accepted"
输出: "it is ab that this solution is ac"
  • 实现代码以及注释
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
ini复制代码class Solution {

class TreeNode { // 构建前缀树节点
TreeNode[] kids; // 指向的孩子节点
boolean word = false;
public TreeNode() {
kids = new TreeNode[26];
}
}
TreeNode root = new TreeNode();
public String replaceWords(List<String> dictionary, String sentence) {
String[] dicts = dictionary.toArray(new String[dictionary.size()]);
String[] words = sentence.split(" ");
for (String s : dicts) {
buildTree(s); // 建立前缀树
}

StringBuilder sb = new StringBuilder();
for (String s : words) {
sb.append(find(s)); // 寻找符合的前缀单词
sb.append(" ");
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}

private void buildTree(String s) {
char[] cs = s.toCharArray();
TreeNode node = root;
for (char c : cs) {
if (node.kids[c - 'a'] == null) {
node.kids[c - 'a'] = new TreeNode(); // 若是该节点为空,则为它分配一个孩子节点
}
node = node.kids[c - 'a']; // 当前节点指向下一个目标节点
}
node.word = true; // 标志着此处为前缀树单词结束位置
}

private String find(String s) {
char[] cs = s.toCharArray();
TreeNode node = root;
int p = 0;
for (char c : cs) {
if (node.word == true) return s.substring(0,p); // 前缀树结束单词最后一个字母,返回 s[0,p) 即可
if (node.kids[c - 'a'] == null) return s;
node = node.kids[c - 'a'];
p++;
}
return s.substring(0,p);
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

C# DataGridView数据导出Excel文件 前言:

发表于 2021-11-28

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

前言:

博主在做项目的时候需要把数据库的数据用DataGridView展示,然后把展示的数据导出为Excel文件,很多时候我们做项目都会有一个下载文件的按钮,我们需要用微软的的接口,Microsoft.Office.Interop.Excel,我们需要导入这个引用对DataGridView数据进行处理,利用Microsoft.Office.Interop.Excel提供的类对数据进行导出。博主把具体的操作步骤写下来教大家使用!!!!

每日一遍,快乐就完事了

image-20211123170358983

1.创建窗体文件并设计好界面

博主直接用的之前的项目,大家需要可以看博主之前的文章

image-20211123161449321

2.导入Microsoft.Office.Interop.Excel引用

在窗体项目里面我们找到引用,右击添加引用在COM类型库里面搜索Excel找到Microsoft Excel 14.0这个引用

image-20211123161808092

3.双击生成函数对代码处理

双击按钮自动生成触发函数,在函数里面写入代码

image-20211123161948620

3.1对命名空间的引用

在命名空间添加 using Excel = Microsoft.Office.Interop.Excel;这条命名空间。

image-20211123163841147

3.2 代码分析

博主对每一条代码都写了注解方便大家理解,这里使用了文件的操作。

image-20211123163407049

image-20211123163546619

3.3 注:博主这里的代码可以直接复制使用

博主这个导出文件的操作的代码,可以直接复制到你需要导出的按钮进行粘贴就可以使用,前提是你把前面的步骤做完了。

image-20211123164331640

3.3.1代码展示

博主对每条导出文件的操作都基本上做出了注解方便大家阅读

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
ini复制代码using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Data.SqlClient;
using MySql.Data.MySqlClient;
using Excel = Microsoft.Office.Interop.Excel;
namespace student
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e)
{


}
//博主这里使用的是MySQL,需要SQLsever看博主之前的文章
private void button1_Click(object sender, EventArgs e)
{
string connString = "server=localhost; database=student; uid=root; pwd=88888888;Character Set=utf8;";
MySqlConnection conn = new MySqlConnection(connString);
MySqlCommand comm = new MySqlCommand();
comm.Connection = conn;
try
{
conn.Open();
string sql = "select course_id ,course_name,teacher_naem from T_course";
MySqlDataAdapter da = new MySqlDataAdapter(sql, connString);
DataSet ds = new DataSet();
da.Fill(ds,"studens");
dataGridView1.DataSource = ds;
dataGridView1.DataMember ="studens";
conn.Close();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "操作数据库出错!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);

}
}



//从这里复制代码哦,上面是数据库连接
private void button2_Click(object sender, EventArgs e) //点击导出到Excel表按钮
{
string fileName = "IC00";//可以在这里设置默认文件名
string saveFileName = "";//文件保存名
SaveFileDialog saveDialog = new SaveFileDialog();//实例化文件对象
saveDialog.DefaultExt = "xlsx";//文件默认扩展名
saveDialog.Filter = "Excel文件|*.xlsx";//获取或设置当前文件名筛选器字符串,该字符串决定对话框的“另存为文件类型”或“文件类型”框中出现的选择内容。
saveDialog.FileName = fileName;
saveDialog.ShowDialog();//打开保存窗口给你选择路径和设置文件名
saveFileName = saveDialog.FileName;
if (saveFileName.IndexOf(":") < 0) return; //被点了取消
Microsoft.Office.Interop.Excel.Application xlApp = new Microsoft.Office.Interop.Excel.Application();
if (xlApp == null)
{
MessageBox.Show("无法创建Excel对象,您的电脑可能未安装Excel");
return;
}
Microsoft.Office.Interop.Excel.Workbooks workbooks = xlApp.Workbooks;//Workbooks代表一个 Microsoft Excel 工作簿
Microsoft.Office.Interop.Excel.Workbook workbook = workbooks.Add(Microsoft.Office.Interop.Excel.XlWBATemplate.xlWBATWorksheet);//新建一个工作表。 新工作表将成为活动工作表。
Microsoft.Office.Interop.Excel.Worksheet worksheet = (Microsoft.Office.Interop.Excel.Worksheet)workbook.Worksheets[1];//取得sheet1
//写入标题
for (int i = 0; i < dataGridView1.ColumnCount; i++)//遍历循环获取DataGridView标题
{ worksheet.Cells[1, i + 1] = dataGridView1.Columns[i].HeaderText; }// worksheet.Cells[1, i + 1]表示工作簿第一行第i+1列,Columns[i].HeaderText表示第i列的表头
//写入数值
for (int r = 0; r < dataGridView1.Rows.Count; r++)//这里表示数据的行标,dataGridView1.Rows.Count表示行数
{
for (int i = 0; i < dataGridView1.ColumnCount; i++)//遍历r行的列数
{
worksheet.Cells[r + 2, i + 1] = dataGridView1.Rows[r].Cells[i].Value;//Cells[r + 2, i + 1]表示工作簿从第二行开始第一行保存为表头了,dataGridView1.Rows[r].Cells[i].Value获取列的r行i值
}
System.Windows.Forms.Application.DoEvents();//实时更新表格
}
worksheet.Columns.EntireColumn.AutoFit();//列宽自适应
MessageBox.Show(fileName + "资料保存成功", "提示", MessageBoxButtons.OK);//提示保存成功
if (saveFileName != "")//saveFileName保存文件名不为空
{
try
{
workbook.Saved = true;//获取或设置一个值,该值指示工作簿自上次保存以来是否进行了更改
workbook.SaveCopyAs(saveFileName); //fileSaved = true;将工作簿副本保存到文件中,但不修改内存中打开的工作簿
}
catch (Exception ex)
{//fileSaved = false;
MessageBox.Show("导出文件时出错,文件可能正被打开!\n" + ex.Message);
}
}
xlApp.Quit();
GC.Collect();//强行销毁
}
}
}
  1. 效果展示

会在你选择的路径导出Excel文件,我们可以对文件进行处理。

image-20211123163650489

总结:

这个操作虽然简单,但是多多少少还是会有一些难度的,我们需要注意要添加引用不然代码可能不起作用,另外对Workbooks有了解的童鞋,上手肯定快,如果你对这个不了解也没关系可以去微软官网了解,学习C#可以多看看官方文档,对你帮助会很大的,好了,传作不易,点赞关注评论收藏哦哈哈哈!!!

p57

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…141142143…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%