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

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


  • 首页

  • 归档

  • 搜索

详解tomcat的连接数与线程池 一、Nio、Bio、APR

发表于 2018-12-25

前言

  在使用tomcat时,经常会遇到连接数、线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector)。

  Connector的主要功能,是接收连接请求,创建Request和Response对象用于和请求端交换数据;然后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine。当Engine处理完请求后,也会通过Connector将响应返回给客户端。

  可以说,Servlet容器处理请求,是需要Connector进行调度和控制的,Connector**是Tomcat**处理请求的主干,因此Connector的配置和使用对Tomcat的性能有着重要的影响。这篇文章将从Connector入手,讨论一些与Connector有关的重要问题,包括NIO/BIO模式、线程池、连接数等。

  根据协议的不同,Connector可以分为HTTP Connector、AJP Connector等,本文只讨论HTTP Connector。

一、Nio、Bio、APR

1、Connector的protocol

  Connector在处理HTTP请求时,会使用不同的protocol。不同的Tomcat版本支持的protocol不同,其中最典型的protocol包括BIO、NIO和APR(Tomcat7中支持这3种,Tomcat8增加了对NIO2的支持,而到了Tomcat8.5和Tomcat9.0,则去掉了对BIO的支持)。

  BIO是Blocking IO,顾名思义是阻塞的IO;NIO是Non-blocking IO,则是非阻塞的IO。而APR是Apache Portable Runtime,是Apache可移植运行库,利用本地库可以实现高可扩展性、高性能;Apr是在Tomcat上运行高并发应用的首选模式,但是需要安装apr、apr-utils、tomcat-native等包。

2、如何指定protocol

  Connector使用哪种protocol,可以通过元素中的protocol属性进行指定,也可以使用默认值。

  指定的protocol取值及对应的协议如下:

  • HTTP/1.1:默认值,使用的协议与Tomcat版本有关
  • org.apache.coyote.http11.Http11Protocol:BIO
  • org.apache.coyote.http11.Http11NioProtocol:NIO
  • org.apache.coyote.http11.Http11Nio2Protocol:NIO2
  • org.apache.coyote.http11.Http11AprProtocol:APR

  如果没有指定protocol,则使用默认值HTTP/1.1,其含义如下:在Tomcat7中,自动选取使用BIO或APR(如果找到APR需要的本地库,则使用APR,否则使用BIO);在Tomcat8中,自动选取使用NIO或APR(如果找到APR需要的本地库,则使用APR,否则使用NIO)。

3、BIO/NIO有何不同

  无论是BIO,还是NIO,Connector处理请求的大致流程是一样的:

  在accept**队列中接收连接(当客户端向服务器发送请求时,如果客户端与OS完成三次握手建立了连接,则OS将该连接放入accept队列);在连接中获取请求的数据,生成request;调用servlet容器处理请求;返回response。**为了便于后面的说明,首先明确一下连接与请求的关系:连接是TCP层面的(传输层),对应socket;请求是HTTP层面的(应用层),必须依赖于TCP的连接实现;一个TCP连接中可能传输多个HTTP请求。

  在BIO实现的Connector中,处理请求的主要实体是JIoEndpoint对象。JIoEndpoint维护了Acceptor和Worker:Acceptor接收socket,然后从Worker线程池中找出空闲的线程处理socket,如果worker线程池没有空闲线程,则Acceptor将阻塞。其中Worker是Tomcat自带的线程池,如果通过配置了其他线程池,原理与Worker类似。

  在NIO实现的Connector中,处理请求的主要实体是NIoEndpoint对象。NIoEndpoint中除了包含Acceptor和Worker外,还是用了Poller,处理流程如下图所示:

  Acceptor接收socket后,不是直接使用Worker中的线程处理请求,而是先将请求发送给了Poller,而Poller是实现NIO的关键。Acceptor向Poller发送请求通过队列实现,使用了典型的生产者-消费者模式。在Poller中,维护了一个Selector对象;当Poller从队列中取出socket后,注册到该Selector中;然后通过遍历Selector,找出其中可读的socket,并使用Worker中的线程处理相应请求。与BIO类似,Worker也可以被自定义的线程池代替。

  通过上述过程可以看出,在NIoEndpoint处理请求的过程中,无论是Acceptor接收socket,还是线程处理请求,使用的仍然是阻塞方式;但在“读取socket并交给Worker中的线程”的这个过程中,使用非阻塞的NIO实现,这是NIO模式与BIO模式的最主要区别(其他区别对性能影响较小,暂时略去不提)。而这个区别,在并发量较大的情形下可以带来Tomcat效率的显著提升:

  目前大多数HTTP请求使用的是长连接(HTTP/1.1默认keep-alive为true),而长连接意味着,一个TCP的socket在当前请求结束后,如果没有新的请求到来,socket不会立马释放,而是等timeout后再释放。如果使用BIO,“读取socket并交给Worker中的线程”这个过程是阻塞的,也就意味着在socket等待下一个请求或等待释放的过程中,处理这个socket的工作线程会一直被占用,无法释放;因此Tomcat可以同时处理的socket数目不能超过最大线程数,性能受到了极大限制。而使用NIO,“读取socket并交给Worker中的线程”这个过程是非阻塞的,当socket在等待下一个请求或等待释放时,并不会占用工作线程,因此Tomcat可以同时处理的socket数目远大于最大线程数,并发性能大大提高。

二、3个参数:acceptCount、maxConnections、maxThreads

  再回顾一下Tomcat处理请求的过程:在**accept队列中接收连接(当客户端向服务器发送请求时,如果客户端与OS完成三次握手建立了连接,则OS将该连接放入accept队列);在连接中获取请求的数据,生成request;调用servlet容器处理请求;返回response**。

  相对应的,Connector中的几个参数功能如下:

1、acceptCount

  accept队列的长度;当accept队列中连接的个数达到acceptCount时,队列满,进来的请求一律被拒绝。默认值是100。

2、maxConnections

  Tomcat在任意时刻接收和处理的最大连接数。当Tomcat接收的连接数达到maxConnections时,Acceptor线程不会读取accept队列中的连接;这时accept队列中的线程会一直阻塞着,直到Tomcat接收的连接数小于maxConnections。如果设置为-1,则连接数不受限制。

  默认值与连接器使用的协议有关:NIO的默认值是10000,APR/native的默认值是8192,而BIO的默认值为maxThreads(如果配置了Executor,则默认值是Executor的maxThreads)。

3、maxThreads

  请求处理线程的最大数量。默认值是200(Tomcat7和8都是的)。如果该Connector绑定了Executor,这个值会被忽略,因为该Connector将使用绑定的Executor,而不是内置的线程池来执行任务。

  maxThreads规定的是最大的线程数目,并不是实际running的CPU数量;实际上,maxThreads的大小比CPU核心数量要大得多。这是因为,处理请求的线程真正用于计算的时间可能很少,大多数时间可能在阻塞,如等待数据库返回数据、等待硬盘读写数据等。因此,在某一时刻,只有少数的线程真正的在使用物理CPU,大多数线程都在等待;因此线程数远大于物理核心数才是合理的。

  换句话说,Tomcat通过使用比CPU核心数量多得多的线程数,可以使CPU忙碌起来,大大提高CPU的利用率。

4、参数设置

  (1)maxThreads的设置既与应用的特点有关,也与服务器的CPU核心数量有关。通过前面介绍可以知道,maxThreads数量应该远大于CPU核心数量;而且CPU核心数越大,maxThreads应该越大;应用中CPU越不密集(IO越密集),maxThreads应该越大,以便能够充分利用CPU。当然,maxThreads的值并不是越大越好,如果maxThreads过大,那么CPU会花费大量的时间用于线程的切换,整体效率会降低。

  (2)maxConnections的设置与Tomcat的运行模式有关。如果tomcat使用的是BIO,那么maxConnections的值应该与maxThreads一致;如果tomcat使用的是NIO,那么类似于Tomcat的默认值,maxConnections值应该远大于maxThreads。

  (3)通过前面的介绍可以知道,虽然tomcat同时可以处理的连接数目是maxConnections,但服务器中可以同时接收的连接数为maxConnections+acceptCount 。acceptCount的设置,与应用在连接过高情况下希望做出什么反应有关系。如果设置过大,后面进入的请求等待时间会很长;如果设置过小,后面进入的请求立马返回connection refused。

三、线程池Executor

  Executor元素代表Tomcat中的线程池,可以由其他组件共享使用;要使用该线程池,组件需要通过executor属性指定该线程池。

  Executor是Service元素的内嵌元素。一般来说,使用线程池的是Connector组件;为了使Connector能使用线程池,Executor元素应该放在Connector前面。Executor与Connector的配置举例如下:

?

1 2 <Executor name=``"tomcatThreadPool" namePrefix =``"catalina-exec-" maxThreads=``"150" minSpareThreads=``"4" /> <Connector executor=``"tomcatThreadPool" port=``"8080" protocol=``"HTTP/1.1" connectionTimeout=``"20000" redirectPort=``"8443" acceptCount=``"1000" />

  Executor的主要属性包括:

  • name:该线程池的标记
  • maxThreads:线程池中最大活跃线程数,默认值200(Tomcat7和8都是)
  • minSpareThreads:线程池中保持的最小线程数,最小值是25
  • maxIdleTime:线程空闲的最大时间,当空闲超过该值时关闭线程(除非线程数小于minSpareThreads),单位是ms,默认值60000(1分钟)
  • daemon:是否后台线程,默认值true
  • threadPriority:线程优先级,默认值5
  • namePrefix:线程名字的前缀,线程池中线程名字为:namePrefix+线程编号

四、查看当前状态

  上面介绍了Tomcat连接数、线程数的概念以及如何设置,下面说明如何查看服务器中的连接数和线程数。

  查看服务器的状态,大致分为两种方案:(1)使用现成的工具,(2)直接使用Linux的命令查看。

  现成的工具,如JDK自带的jconsole工具可以方便的查看线程信息(此外还可以查看CPU、内存、类、JVM基本信息等),Tomcat自带的manager,收费工具New Relic等。下图是jconsole查看线程信息的界面:

  下面说一下如何通过Linux命令行,查看服务器中的连接数和线程数。

1、连接数

  假设Tomcat接收http请求的端口是8083,则可以使用如下语句查看连接情况:

1
复制代码netstat –nat | grep 8083

  结果如下所示:

  可以看出,有一个连接处于listen状态,监听请求;除此之外,还有4个已经建立的连接(ESTABLISHED)和2个等待关闭的连接(CLOSE_WAIT)。

2、线程

  ps命令可以查看进程状态,如执行如下命令:

1
复制代码ps –e | grep java

   结果如下图:

  可以看到,只打印了一个进程的信息;27989是线程id,java是指执行的java命令。这是因为启动一个tomcat,内部所有的工作都在这一个进程里完成,包括主线程、垃圾回收线程、Acceptor线程、请求处理线程等等。

  通过如下命令,可以看到该进程内有多少个线程;其中,nlwp含义是number of light-weight process。

1
复制代码ps –o nlwp 27989

  可以看到,该进程内部有73个线程;但是73并没有排除处于idle状态的线程。要想获得真正在running的线程数量,可以通过以下语句完成:

1
复制代码ps -eLo pid ,stat | grep 27989 | grep running | wc -l

  其中ps -eLo pid ,stat可以找出所有线程,并打印其所在的进程号和线程当前的状态;两个grep命令分别筛选进程号和线程状态;wc统计个数。其中,ps -eLo pid ,stat | grep 27989输出的结果如下:

图中只截图了部分结果;Sl表示大多数线程都处于空闲状态。

原文地址:https://www.cnblogs.com/kismetv/p/7806063.html

本文转载自: 掘金

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

springmvc工作原理及源码分析

发表于 2018-12-20

一、JavaEE体系结构

JavaEE体系结构

二、mvc 设计模式|思想

mvc设计思想

1
2
3
4
5
复制代码Model 模型层  (javaBean组件 = 领域模型(javaBean) + 业务层 + 持久层)

View 视图层( html、jsp…)

Controller 控制层(委托模型层进行数据处理)

三、springmvc简介

1
复制代码springmvc是一个web层mvc框架,类似struts2。

四、springmvc和spring之间关系

spring

1
复制代码springmvc是spring的部分,其实就是spring在原有基础上,又提供了web应用的mvc模块。

五、sprigmvc和struts2的比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码实现机制:

struts2是基于过滤器实现的。

springmvc是基于servlet实现的。

运行速度:

因为过滤器底层是servlet,所以springmvc的运行速度会稍微比structs2快。

struts2是多例的

springmvc单例的

参数封装:

struts2参数封装是基于属性进行封装。

springmvc是基于方法封装。颗粒度更细。

六、springmvc的工作原理图

springmvc工作原理图

七、springmvc具体流程步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码⑴ 用户发送请求至DispatcherServlet。

⑵ DispatcherServlet收到请求调用HandlerMapping查询具体的Handler。

⑶ HandlerMapping找到具体的处理器(具体配置的是哪个处理器的实现类),生成处理器对象及处理器拦截器(HandlerExcutorChain包含了Handler以及拦截器集合)返回给DispatcherServlet。

⑷ DispatcherServlet接收到HandlerMapping返回的HandlerExcutorChain后,调用HandlerAdapter请求执行具体的Handler(Controller)。

⑸ HandlerAdapter经过适配调用具体的Handler(Controller即后端控制器)。

⑹ Controller执行完成返回ModelAndView(其中包含逻辑视图和数据)给HandlerAdaptor。

⑺ HandlerAdaptor再将ModelAndView返回给DispatcherServlet。

⑻ DispatcherServlet请求视图解析器ViewReslover解析ModelAndView。

⑼ ViewReslover解析后返回具体View(物理视图)到DispatcherServlet。

⑽ DispatcherServlet请求渲染视图(即将模型数据填充至视图中) 根据View进行渲染视图。

⑾ 将渲染后的视图返回给DispatcherServlet。

⑿ DispatcherServlet将响应结果返回给用户。

八、springmvc核心组件说明

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
复制代码(1)前端控制器DispatcherServlet(配置即可)

功能:中央处理器,接收请求,自己不做任何处理,而是将请求发送给其他组件进行处理。DispatcherServlet 是整个流程的控制中心。

(2)处理器映射器HandlerMapping(配置即可)

功能:根据DispatcherServlet发送的url请求路径查找Handler

常见的处理器映射器:BeanNameUrlHandlerMapping,SimpleUrlHandlerMapping,

ControllerClassNameHandlerMapping,DefaultAnnotationHandlerMapping(不建议使用)

(3)处理器适配器HandlerAdapter(配置即可)

功能:按照特定规则(HandlerAdapter要求的规则)去执行Handler。

通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展多个适配器对更多类型的处理器进行执行。

常见的处理器适配器:HttpRequestHandlerAdapter,SimpleControllerHandlerAdapter,AnnotationMethodHandlerAdapter

(4)处理器Handler即Controller(程序猿编写)

功能:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler。

(5)视图解析器ViewReslover(配置即可)

功能:进行视图解析,根据逻辑视图名解析成真正的视图。

ViewResolver负责将处理结果生成View视图,ViewResolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。

springmvc框架提供了多种View视图类型,如:jstlView、freemarkerView、pdfView...

(6)视图View(程序猿编写)

View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf...)

九、springmvc入门案例

  • 创建一个maven工程
    maven工程
  • 配置pom.xml

引入相关依赖:spring的基本包、springmvc需要的spring-webmvc,日志相关的slf4j-log4j12,jsp相关的jstl、servlet-api、jsp-api。

pom.xml

  • 配置web.xml

因为DispatcherServlet本身就是一个Servlet,所以需要在web.xml配置。

web.xml

  • 配置springmvc.xml

一、使用默认加载springmvc配置文件的方式,必须按照以下规范:

①命名规则:-servlet.xml ====> springmvc-servlet.xml

②路径规则:-servlet.xml必须放在WEB-INF下边

二、如果要不按照默认加载位置,则需要在web.xml中通过标签来指定springmvc配置文件的加载路径,如上图所示。

springmvc.xml

  • 自定义Controller(处理器)

将自定义的 Controller 处理器配置到 spring 容器中交由 spring 容器来管理,因为这里的 springmvc.xml 配置文件中处理器映射器配置的是 BeanNameUrlHandlerMapping ,根据名字可知这个处理器映射器是根据 bean (自定义Controller) 的 name 属性值url去寻找执行类 Handler(Controller) , 所以bean的name属性值即是要和用户发送的请求路径匹配的 url 。

MyController.java

  • 定义视图页面

根据视图解析路径:WEB-INF/jsps/index.jsp

inde.jsp

  • 根据代码分析springmvc执行流程

springmvc代码执行流程

  • 处理器映射器(配置多个处理器映射器可以共存)
1
复制代码<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>

功能:根据bean(自定义Controller)的name属性的url去寻找执行类Controller。

  • 处理器适配器(配置多个处理器适配器可以共存)
1
复制代码<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>

功能:自定义的处理器(Controller)实现了Controller接口时,适配器就会执行Controller的具体方法。

  • 处理器适配器源码SimpleControllerHandlerAdapter

SimpleControllerHandlerAdapter源码分析

SimpleControllerHandlerAdapter会自动判断自定义的处理器(Controller)是否实现了Controller接口,如果是,它将会自动调用处理器的handleRequest方法。
Controller接口中有一个方法叫handleRequest,也就是处理器方法。

因此,自定义的Controller要想被调用就必须实现Controller接口,重写Controller接口中的处理器方法。

  • 添加日志

log4j.properties

  • 运行结果

运行结果

1
2
3
4
5
复制代码                                 如果觉得这篇文章对你有帮助  
麻烦帮忙点个赞或关注
这对我来说是一种很好的鼓励
在此先谢谢各位
O(∩_∩)O

本文转载自: 掘金

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

一口(很长的)气了解 babel

发表于 2018-12-19

最近几年,如果你是一名前端开发者,如果你没有使用甚至听说过 babel,可能会被当做穿越者吧?

说到 babel,一连串名词会蹦出来:

  • babel-cli
  • babel-core
  • babel-runtime
  • babel-node
  • babel-polyfill
  • …

这些都是 babel 吗?他们分别是做什么的?有区别吗?

babel 到底做了什么?怎么做的?

简单来说把 JavaScript 中 es2015/2016/2017/2046 的新语法转化为 es5,让低端运行环境(如浏览器和 node )能够认识并执行。本文以 babel 6.x 为基准进行讨论。最近 babel 出了 7.x,放在最后聊。

严格来说,babel 也可以转化为更低的规范。但以目前情况来说,es5 规范已经足以覆盖绝大部分浏览器,因此常规来说转到 es5 是一个安全且流行的做法。

如果你对 es5/es2015 等等也不了解的话,那你可能真的需要先补补课了。

使用方法

总共存在三种方式:

  1. 使用单体文件 (standalone script)
  2. 命令行 (cli)
  3. 构建工具的插件 (webpack 的 babel-loader, rollup 的 rollup-plugin-babel)。

其中后面两种比较常见。第二种多见于 package.json 中的 scripts 段落中的某条命令;第三种就直接集成到构建工具中。

这三种方式只有入口不同而已,调用的 babel 内核,处理方式都是一样的,所以我们先不纠结入口的问题。

运行方式和插件

babel 总共分为三个阶段:解析,转换,生成。

babel 本身不具有任何转化功能,它把转化的功能都分解到一个个 plugin 里面。因此当我们不配置任何插件时,经过 babel 的代码和输入是相同的。

插件总共分为两种:

  1. 当我们添加 语法插件 之后,在解析这一步就使得 babel 能够解析更多的语法。(顺带一提,babel 内部使用的解析类库叫做 babylon,并非 babel 自行开发)

举个简单的例子,当我们定义或者调用方法时,最后一个参数之后是不允许增加逗号的,如 callFoo(param1, param2,) 就是非法的。如果源码是这种写法,经过 babel 之后就会提示语法错误。

但最近的 JS 提案中已经允许了这种新的写法(让代码 diff 更加清晰)。为了避免 babel 报错,就需要增加语法插件 babel-plugin-syntax-trailing-function-commas

  1. 当我们添加 转译插件 之后,在转换这一步把源码转换并输出。这也是我们使用 babel 最本质的需求。

比起语法插件,转译插件其实更好理解,比如箭头函数 (a) => a 就会转化为 function (a) {return a}。完成这个工作的插件叫做 babel-plugin-transform-es2015-arrow-functions。

同一类语法可能同时存在语法插件版本和转译插件版本。如果我们使用了转译插件,就不用再使用语法插件了。

配置文件

既然插件是 babel 的根本,那如何使用呢?总共分为 2 个步骤:

  1. 将插件的名字增加到配置文件中 (根目录下创建 .babelrc 或者 package.json 的 babel 里面,格式相同)
  2. 使用 npm install babel-plugin-xxx 进行安装

具体书写格式就不详述了。

preset

比如 es2015 是一套规范,包含大概十几二十个转译插件。如果每次要开发者一个个添加并安装,配置文件很长不说,npm install 的时间也会很长,更不谈我们可能还要同时使用其他规范呢。

为了解决这个问题,babel 还提供了一组插件的集合。因为常用,所以不必重复定义 & 安装。(单点和套餐的差别,套餐省下了巨多的时间和配置的精力)

preset 分为以下几种:

  1. 官方内容,目前包括 env, react, flow, minify 等。这里最重要的是 env,后面会详细介绍。
  2. stage-x,这里面包含的都是当年最新规范的草案,每年更新。

这里面还细分为

* Stage 0 - 稻草人: 只是一个想法,经过 TC39 成员提出即可。
* Stage 1 - 提案: 初步尝试。
* Stage 2 - 初稿: 完成初步规范。
* Stage 3 - 候选: 完成规范和浏览器初步实现。
* Stage 4 - 完成: 将被添加到下一年度发布。例如 `syntax-dynamic-import` 就是 stage-2 的内容,`transform-object-rest-spread` 就是 stage-3 的内容。

此外,低一级的 stage 会包含所有高级 stage 的内容,例如 stage-1 会包含 stage-2, stage-3 的所有内容。

stage-4 在下一年更新会直接放到 env 中,所以没有单独的 stage-4 可供使用。
3. es201x, latest

这些是已经纳入到标准规范的语法。例如 es2015 包含 arrow-functions,es2017 包含 syntax-trailing-function-commas。但因为 env 的出现,使得 es2016 和 es2017 都已经废弃。所以我们经常可以看到 es2015 被单独列出来,但极少看到其他两个。

latest 是 env 的雏形,它是一个每年更新的 preset,目的是包含所有 es201x。但也是因为更加灵活的 env 的出现,已经废弃。

执行顺序

很简单的几条原则:

  • Plugin 会运行在 Preset 之前。
  • Plugin 会从前到后顺序执行。
  • Preset 的顺序则 刚好相反(从后向前)。

preset 的逆向顺序主要是为了保证向后兼容,因为大多数用户的编写顺序是 ['es2015', 'stage-0']。这样必须先执行 stage-0 才能确保 babel 不报错。因此我们编排 preset 的时候,也要注意顺序,其实只要按照规范的时间顺序列出即可。

插件和 preset 的配置项

简略情况下,插件和 preset 只要列出字符串格式的名字即可。但如果某个 preset 或者插件需要一些配置项(或者说参数),就需要把自己先变成数组。第一个元素依然是字符串,表示自己的名字;第二个元素是一个对象,即配置对象。

最需要配置的当属 env,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码"presets": [
// 带了配置项,自己变成数组
[
// 第一个元素依然是名字
"env",
// 第二个元素是对象,列出配置项
{
"module": false
}
],

// 不带配置项,直接列出名字
"stage-2"
]

env (重点)

因为 env 最为常用也最重要,所以我们有必要重点关注。

env 的核心目的是通过配置得知目标环境的特点,然后只做必要的转换。例如目标浏览器支持 es2015,那么 es2015 这个 preset 其实是不需要的,于是代码就可以小一点(一般转化后的代码总是更长),构建时间也可以缩短一些。

如果不写任何配置项,env 等价于 latest,也等价于 es2015 + es2016 + es2017 三个相加(不包含 stage-x 中的插件)。env 包含的插件列表维护在这里

下面列出几种比较常用的配置方法:

1
2
3
4
5
6
7
8
9
复制代码{
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}

如上配置将考虑所有浏览器的最新2个版本(safari大于等于7.0的版本)的特性,将必要的代码进行转换。而这些版本已有的功能就不进行转化了。这里的语法可以参考 browserslist

1
2
3
4
5
6
7
8
9
复制代码{
"presets": [
["env", {
"targets": {
"node": "6.10"
}
}]
]
}

如上配置将目标设置为 nodejs,并且支持 6.10 及以上的版本。也可以使用 node: 'current' 来支持最新稳定版本。例如箭头函数在 nodejs 6 及以上将不被转化,但如果是 nodejs 0.12 就会被转化了。

另外一个有用的配置项是 modules。它的取值可以是 amd, umd, systemjs, commonjs 和 false。这可以让 babel 以特定的模块化格式来输出代码。如果选择 false 就不进行模块化处理。

其他配套工具

以上讨论了 babel 的核心处理机制和配置方法等,不论任何入口调用 babel 都走这一套。但文章开头提的那一堆 babel-* 还是让人一头雾水。实际上这些 babel-* 大多是不同的入口(方式)来使用 babel,下面来简单介绍一下。

babel-cli

顾名思义,cli 就是命令行工具。安装了 babel-cli 就能够在命令行中使用 babel 命令来编译文件。

在开发 npm package 时经常会使用如下模式:

  • 把 babel-cli 安装为 devDependencies
  • 在 package.json 中添加 scripts (比如 prepublish),使用 babel 命令编译文件
  • npm publish

这样既可以使用较新规范的 JS 语法编写源码,同时又能支持旧版环境。因为项目可能不太大,用不到构建工具 (webpack 或者 rollup),于是在发布之前用 babel-cli 进行处理。

babel-node

babel-node 是 babel-cli 的一部分,它不需要单独安装。

它的作用是在 node 环境中,直接运行 es2015 的代码,而不需要额外进行转码。例如我们有一个 js 文件以 es2015 的语法进行编写(如使用了箭头函数)。我们可以直接使用 babel-node es2015.js 进行执行,而不用再进行转码了。

可以说:babel-node = babel-polyfill + babel-register。那这两位又是谁呢?

babel-register

babel-register 模块改写 require 命令,为它加上一个钩子。此后,每当使用 require 加载 .js、.jsx、.es 和 .es6 后缀名的文件,就会先用 babel 进行转码。

使用时,必须首先加载 require('babel-register')。

需要注意的是,babel-register 只会对 require 命令加载的文件转码,而 不会对当前文件转码。

另外,由于它是实时转码,所以 只适合在开发环境使用。

babel-polyfill

babel 默认只转换 js 语法,而不转换新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码。

举例来说,es2015 在 Array 对象上新增了 Array.from 方法。babel 就不会转码这个方法。如果想让这个方法运行,必须使用 babel-polyfill。(内部集成了 core-js 和 regenerator)

使用时,在所有代码运行之前增加 require('babel-polyfill')。或者更常规的操作是在 webpack.config.js 中将 babel-polyfill 作为第一个 entry。因此必须把 babel-polyfill 作为 dependencies 而不是 devDependencies

babel-polyfill 主要有两个缺点:

  1. 使用 babel-polyfill 会导致打出来的包非常大,因为 babel-polyfill 是一个整体,把所有方法都加到原型链上。比如我们只使用了 Array.from,但它把 Object.defineProperty 也给加上了,这就是一种浪费了。这个问题可以通过单独使用 core-js 的某个类库来解决,core-js 都是分开的。
  2. babel-polyfill 会污染全局变量,给很多类的原型链上都作了修改,如果我们开发的也是一个类库供其他开发者使用,这种情况就会变得非常不可控。

因此在实际使用中,如果我们无法忍受这两个缺点(尤其是第二个),通常我们会倾向于使用 babel-plugin-transform-runtime。

但如果代码中包含高版本 js 中类型的实例方法 (例如 [1,2,3].includes(1)),这还是要使用 polyfill。

babel-runtime 和 babel-plugin-transform-runtime (重点)

我们时常在项目中看到 .babelrc 中使用 babel-plugin-transform-runtime,而 package.json 中的 dependencies (注意不是 devDependencies) 又包含了 babel-runtime,那这两个是不是成套使用的呢?他们又起什么作用呢?

先说 babel-plugin-transform-runtime。

babel 会转换 js 语法,之前已经提过了。以 async/await 举例,如果不使用这个 plugin (即默认情况),转换后的代码大概是:

1
2
3
4
5
6
7
复制代码// babel 添加一个方法,把 async 转化为 generator
function _asyncToGenerator(fn) { return function () {....}} // 很长很长一段

// 具体使用处
var _ref = _asyncToGenerator(function* (arg1, arg2) {
yield (0, something)(arg1, arg2);
});

不用过于纠结具体的语法,只需看到,这个 _asyncToGenerator 在当前文件被定义,然后被使用了,以替换源代码的 await。但每个被转化的文件都会插入一段 _asyncToGenerator 这就导致重复和浪费了。

在使用了 babel-plugin-transform-runtime 了之后,转化后的代码会变成

1
2
3
4
5
6
7
8
复制代码// 从直接定义改为引用,这样就不会重复定义了。
var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');
var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);

// 具体使用处是一样的
var _ref = _asyncToGenerator3(function* (arg1, arg2) {
yield (0, something)(arg1, arg2);
});

从定义方法改成引用,那重复定义就变成了重复引用,就不存在代码重复的问题了。

但在这里,我们也发现 babel-runtime 出场了,它就是这些方法的集合处,也因此,在使用 babel-plugin-transform-runtime 的时候必须把 babel-runtime 当做依赖。

再说 babel-runtime,它内部集成了

  1. core-js: 转换一些内置类 (Promise, Symbols等等) 和静态方法 (Array.from 等)。绝大部分转换是这里做的。自动引入。
  2. regenerator: 作为 core-js 的拾遗补漏,主要是 generator/yield 和 async/await 两组的支持。当代码中有使用 generators/async 时自动引入。
  3. helpers, 如上面的 asyncToGenerator 就是其中之一,其他还有如 jsx, classCallCheck 等等,可以查看 babel-helpers。在代码中有内置的 helpers 使用时(如上面的第一段代码)移除定义,并插入引用(于是就变成了第二段代码)。

babel-plugin-transform-runtime 不支持 实例方法 (例如 [1,2,3].includes(1))

此外补充一点,把 helpers 抽离并统一起来,避免重复代码的工作还有一个 plugin 也能做,叫做 babel-plugin-external-helpers。但因为我们使用的 transform-runtime 已经包含了这个功能,因此不必重复使用。而且 babel 的作者们也已经开始讨论这两个插件过于类似,正在讨论在 babel 7 中把 external-helpers 删除,讨论在 issue#5699 中。

babel-loader

前面提过 babel 的三种使用方法,并且已经介绍过了 babel-cli。但一些大型的项目都会有构建工具 (如 webpack 或 rollup) 来进行代码构建和压缩 (uglify)。理论上来说,我们也可以对压缩后的代码进行 babel 处理,但那会非常慢。因此如果在 uglify 之前就加入 babel 处理,岂不完美?

所以就有了 babel 插入到构建工具内部这样的需求。以(我还算熟悉的) webpack 为例,webpack 有 loader 的概念,因此就出现了 babel-loader。

和 babel-cli 一样,babel-loader 也会读取 .babelrc 或者 package.json 中的 babel 段作为自己的配置,之后的内核处理也是相同。唯一比 babel-cli 复杂的是,它需要和 webpack 交互,因此需要在 webpack 这边进行配置。比较常见的如下:

1
2
3
4
5
6
7
8
9
复制代码module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader'
}
]
}

如果想在这里传入 babel 的配置项,也可以把改成:

1
2
3
4
5
6
7
复制代码// loader: 'babel-loader' 改成如下:
use: {
loader: 'babel-loader',
options: {
// 配置项在这里
}
}

这里的配置项优先级是最高的。但我认为放到单独的配置文件中更加清晰合理,可读性强一些。

小结一下

名称 作用 备注
babel-cli 允许命令行使用 babel 命令转译文件
babel-node 允许命令行使用 babel-node 直接转译+执行 node 文件 随 babel-cli 一同安装 babel-node = babel-polyfill + babel-register
babel-register 改写 require 命令,为其加载的文件进行转码,不对当前文件转码 只适用于开发环境
babel-polyfill 为所有 API 增加兼容方法 需要在所有代码之前 require,且体积比较大
babel-plugin-transform-runtime & babel-runtime 把帮助类方法从每次使用前定义改为统一 require,精简代码 babel-runtime 需要安装为依赖,而不是开发依赖
babel-loader 使用 webpack 时作为一个 loader 在代码混淆之前进行代码转换

Babel 7.x

最近 babel 发布了 7.0。因为上面部分都是针对 6.x 编写的,所以我们关注一下 7.0 带来的变化(核心机制方面没有变化,插件,preset,解析转译生成这些都没有变化)

我只挑选一些和开发者关系比较大的列在这里,省略的多数是针对某一个 plugin 的改动。完整的列表可以参考官网。

preset 的变更:淘汰 es201x,删除 stage-x,强推 env (重点)

淘汰 es201x 的目的是把选择环境的工作交给 env 自动进行,而不需要开发者投入精力。凡是使用 es201x 的开发者,都应当使用 env 进行替换。但这里的淘汰 (原文 deprecated) 并不是删除,只是不推荐使用了,不好说 babel 8 就真的删了。

与之相比,stage-x 就没那么好运了,它们直接被删了。这是因为 babel 团队认为为这些 “不稳定的草案” 花费精力去更新 preset 相当浪费。stage-x 虽然删除了,但它包含的插件并没有删除(只是被更名了,可以看下面一节),我们依然可以显式地声明这些插件来获得等价的效果。完整列表

为了减少开发者替换配置文件的机械工作,babel 开发了一款 babel-upgrade 的工具,它会检测 babel 配置中的 stage-x 并且替换成对应的 plugins。除此之外它还有其他功能,我们一会儿再详细看。(总之目的就是让你更加平滑地迁移到 babel 7)

npm package 名称的变化 (重点)

这是 babel 7 的一个重大变化,把所有 babel-* 重命名为 @babel/*,例如:

  1. babel-cli 变成了 @babel/cli。
  2. babel-preset-env 变成了 @babel/preset-env。进一步,还可以省略 preset 而简写为 @babel/env。
  3. babel-plugin-transform-arrow-functions 变成了 @babel/plugin-transform-arrow-functions。和 preset 一样,plugin 也可以省略,于是简写为 @babel/transform-arrow-functions。

这个变化不单单应用于 package.json 的依赖中,包括 .babelrc 的配置 (plugins, presets) 也要这么写,为了保持一致。例如

1
2
3
4
5
6
复制代码{
"presets": [
- "env"
+ "@babel/preset-env"
]
}

顺带提一句,上面提过的 babel 解析语法的内核 babylon 现在重命名为 @babel/parser,看起来是被收编了。

上文提过的 stage-x 被删除了,它包含的插件虽然保留,但也被重命名了。babel 团队希望更明显地区分已经位于规范中的插件 (如 es2015 的 babel-plugin-transform-arrow-functions) 和仅仅位于草案中的插件 (如 stage-0 的 @babel/plugin-proposal-function-bind)。方式就是在名字中增加 proposal,所有包含在 stage-x 的转译插件都使用了这个前缀,语法插件不在其列。

最后,如果插件名称中包含了规范名称 (-es2015-, -es3- 之类的),一律删除。例如 babel-plugin-transform-es2015-classes 变成了 @babel/plugin-transform-classes。(这个插件我自己没有单独用过,惭愧)

不再支持低版本 node

babel 7.0 开始不再支持 nodejs 0.10, 0.12, 4, 5 这四个版本,相当于要求 nodejs >= 6 (当前 nodejs LTS 是 8,要求也不算太过分吧)。

这里的不再支持,指的是在这些低版本 node 环境中不能使用 babel 转译代码,但 babel 转译后的代码依然能在这些环境上运行,这点不要混淆。

only 和 ignore 匹配规则的变化

在 babel 6 时,ignore 选项如果包含 *.foo.js,实际上的含义 (转化为 glob) 是 ./**/*.foo.js,也就是当前目录 包括子目录 的所有 foo.js 结尾的文件。这可能和开发者常规的认识有悖。

于是在 babel 7,相同的表达式 *.foo.js 只作用于当前目录,不作用于子目录。如果依然想作用于子目录的,就要按照 glob 的完整规范书写为 ./**/*.foo.js 才可以。only 也是相同。

这个规则变化只作用于通配符,不作用于路径。所以 node_modules 依然包含所有它的子目录,而不单单只有一层。(否则全世界开发者都要爆炸)

@babel/node 从 @babel/cli 中独立了

和 babel 6 不同,如果要使用 @babel/node,就必须单独安装,并添加到依赖中。

babel-upgrade

在提到删除 stage-x 时候提过这个工具,它的目的是帮助用户自动化地从 babel 6 升级到 7。

这款升级工具的功能包括:(这里并不列出完整列表,只列出比较重要和常用的内容)

  1. package.json
  • 把依赖(和开发依赖)中所有的 babel-* 替换为 @babel/*
  • 把这些 @babel/* 依赖的版本更新为最新版 (例如 ^7.0.0)
  • 如果 scripts 中有使用 babel-node,自动添加 @babel/node 为开发依赖
  • 如果有 babel 配置项,检查其中的 plugins 和 presets,把短名 (env) 替换为完整的名字 (@babel/preset-env)
  1. .babelrc
  • 检查其中的 plugins 和 presets,把短名 (env) 替换为完整的名字 (@babel/preset-env)
  • 检查是否包含 preset-stage-x,如有替换为对应的插件并添加到 plugins

使用方式如下:

1
2
3
4
5
6
复制代码# 不安装到本地而是直接运行命令,npm 的新功能
npx babel-upgrade --write

# 或者常规方式
npm i babel-upgrade -g
babel-upgrade --write

babel-upgrade 工具本身也还在开发中,还列出了许多 TODO 没有完成,因此之后的功能可能会更加丰富,例如上面提过的 ignore 的通配符转化等等。

本文转载自: 掘金

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

Flume将 kafka 中的数据转存到 HDFS 中

发表于 2018-12-19

flume1.8 kafka Channel + HDFS sink(without sources)

将 kafka 中的数据转存到 HDFS 中, 用作离线计算, flume 已经帮我们实现了, 添加配置文件, 直接启动 flume-ng 即可.

The Kafka channel can be used for multiple scenarios:

  1. With Flume source and sink - it provides a reliable and highly available channel for events
  2. With Flume source and interceptor but no sink - it allows writing Flume events into a Kafka topic, for use by other apps
  3. With Flume sink, but no source - it is a low-latency, fault tolerant way to send events from Kafka to Flume sinks such as HDFS, HBase or Solr
  • $FLUME_HOME/conf/kafka-hdfs.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码# kafka Channel + HDFS sink(without sources)
a1.channels = c1
a1.sinks = k1

# 定义 KafkaChannel
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
a1.channels.c1.parseAsFlumeEvent = false
a1.channels.c1.kafka.bootstrap.servers = kafka-1:9092,kafka-2:9092,kafka-3:9092
a1.channels.c1.kafka.topic = user
a1.channels.c1.kafka.consumer.group.id = g1

# 定义 HDFS sink
a1.sinks.k1.channel = c1
a1.sinks.k1.type = hdfs
a1.sinks.k1.hdfs.path = hdfs://hadoop-1:9000/flume/%Y%m%d/%H
a1.sinks.k1.hdfs.useLocalTimeStamp = true
a1.sinks.k1.hdfs.filePrefix = log
a1.sinks.k1.hdfs.fileType = DataStream
# 不按照条数生成文件
a1.sinks.k1.hdfs.rollCount = 0
# HDFS 上的文件达到128M 生成一个文件
a1.sinks.k1.hdfs.rollSize = 134217728
# HDFS 上的文件达到10分钟生成一个文件
a1.sinks.k1.hdfs.rollInterval = 600

记得配 hosts

  • 添加 HDFS 相关jar包和配置文件
1
2
3
4
5
6
7
8
9
复制代码commons-configuration-1.6.jar
commons-io-2.4.jar
hadoop-auth-2.8.3.jar
hadoop-common-2.8.3.jar
hadoop-hdfs-2.8.3.jar
hadoop-hdfs-client-2.8.3.jar
htrace-core4-4.0.1-incubating.jar
core-site.xml
hdfs-site.xml
  • flume-1.8 kafka客户端默认版本0.9 但是向上兼容(别用这个 有巨坑 _#)
    kafka-clients-2.0.0.jar kafka_2.11-2.0.0.jar
  • 先启动 zookeeper kafka 和 HDFS(否则会各种报错,)
  • 进入$FLUME_HOME启动 flume
    root@common:/usr/local/flume# ./bin/flume-ng agent -c conf/ -f conf/kafka-hdfs.conf -n a1 -Dflume.root.logger=INFO,console

本文转载自: 掘金

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

java基础学习:JavaWeb之Cookie和Sessio

发表于 2018-12-16

其他更多java基础文章:
java基础学习(目录)


一、会话概述

1.1、什么是会话?

会话可简单理解为:用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一个会话其中不管浏览器发送多少请求,都视为一次会话,直到浏览器关闭,本次会话结束。

其中注意,一个浏览器就相当于一部电话,如果使用火狐浏览器,访问服务器,就是一次会话了,然后打开google浏览器,访问服务器,这是另一个会话,虽然是在同一台电脑,同一个用户在访问,但是,这是两次不同的会话。

1.2、会话机制

  Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是Cookie与Session。Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

二、Cookie

Cookie是客户端技术,程序把每个用户的数据以cookie的形式写给用户各自的浏览器。当用户使用浏览器再去访问服务器中的web资源时,就会带着各自的数据去。这样,web资源处理的就是用户各自的数据了。由于cookie是由客户端浏览器保存和携带的,所以称之为客户端技术

2.1、Cookie的工作流程

1)servlet创建cookie,保存少量数据,发送浏览器。

2)浏览器获得服务器发送的cookie数据,将自动的保存到浏览器端。

3)下次访问时,浏览器将自动携带cookie数据发送给服务器。

2.2、Cookie特点

1)每一个cookie文件大小:4kb , 如果超过4kb浏览器不识别

2)一个web站点(web项目):发送20个

3)一个浏览器保存总大小:300个

4)cookie 不安全,可能泄露用户信息。浏览器支持禁用cookie操作。

5) 默认情况生命周期:与浏览器会话一样,当浏览器关闭时cookie销毁的。—临时cookie

1
2
3
4
5
6
复制代码cookie.setMaxAge(expiry);&emsp;&emsp;//设置cookie被浏览器保存的时间。
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;
expiry:单位秒,默认为-1,
expiry=-1:代表浏览器关闭后,也就是会话结束后,cookie就失效了,也就没有了。
expiry>0:代表浏览器关闭后,cookie不会失效,仍然存在。并且会将cookie保存到硬盘中,直到设置时间过期才会被浏览器自动删除,
expiry=0:删除cookie。不管是之前的expiry=-1还是expiry>0,当设置expiry=0时,cookie都会被浏览器给删除。

2.3、Cookie操作

操作cookie

1
2
3
复制代码&emsp;&emsp;1)创建cookie:new Cookie(name,value)
&emsp;&emsp;2)发送cookie到浏览器:HttpServletResponse.addCookie(Cookie)
&emsp;&emsp;3)servlet接收cookie:HttpServletRequest.getCookies() 浏览器发送的所有cookie

cookie API

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码    getName() 获得名称,cookie中的key
getValue() 获得值,cookie中的value
&emsp;&emsp;setValue(java.lang.String newValue) 设置内容,用于修改key对应的value值。
&emsp;&emsp;setMaxAge(int expiry) 设置有效时间【】
&emsp;&emsp;setPath(java.lang.String uri) 设置路径【】&emsp;&emsp;
&emsp;&emsp;setDomain(java.lang.String pattern) 设置域名 , 一般无效,有浏览器自动设置,setDomain(".zyh.com")
&emsp;&emsp;&emsp;&emsp;www.zyh.com / bbs.zyh.com 都可以访问
&emsp;&emsp;&emsp;&emsp;a.b.zyh.com无法访问
&emsp;&emsp;&emsp;&emsp;作用:设置cookie的作用范围,域名+路径在一起就构成了cookie的作用范围,上面单独设置的setPath有用,是因为有浏览器自动设置该域名属性,但是我们必须知道有这么个属性进行域名设置的
&emsp;&emsp;isHttpOnly() 是否只是http协议使用。只能servlet的通过getCookies()获得,javascript不能获得。
&emsp;&emsp;setComment(java.lang.String purpose) (了解)&emsp;&emsp;//对该cookie进行描述的信息(说明作用),浏览器显示cookie信息时能看到
&emsp;&emsp;setSecure(boolean flag) (了解)&emsp;&emsp;是否使用安全传输协议。为true时,只有当是https请求连接时cookie才会发送给服务器端,而http时不会,但是服务端还是可以发送给浏览端的。
&emsp;&emsp;setVersion(int v) (了解)&emsp;&emsp;参数为0(传统Netscape cookie规范编译)或1(RFC 2109规范编译)。这个没用到,不是很懂

注意事项

cookie不能发送中文,如果要发送中文,就需要进行特别处理。

1
2
3
4
5
复制代码Cookie cookie = new Cookie("country", URLEncoder.encode("中国", "UTF-8"));
response.addCookie(cookie);

//经过URLEncoding就要URLDecoding
String value = URLDecoder.decode(cookies[i].getValue(), "UTF-8");

三、Session

在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)

因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,为用户服务。

3.1、Session的原理

  1. 首先浏览器请求服务器访问web站点时,程序需要为客户端的请求创建一个session的时候,服务器首先会检查这个客户端请求是否已经包含了一个session标识、称为SESSIONID
  2. 如果已经包含了一个sessionid则说明以前已经为此客户端创建过session,服务器就按照sessionid把这个session检索出来使用,如果客户端请求不包含session id,则服务器为此客户端创建一个session并且生成一个与此session相关联的session id,sessionid 的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个sessionid将在本次响应中返回到客户端保存,保存这个sessionid的方式一般就是cookie。
  3. 这样在交互的过程中,浏览器可以自动的按照规则把这个标识发回给服务器,服务器根据这个sessionid就可以找得到对应的session,又回到了步骤1。

3.2、Session的生命周期

常常听到这样一种误解“只要关闭浏览器,session就消失了”。其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对session来说也是一样的,除非程序通知服务器删除一个session,否则服务器会一直保留,程序一般都是在用户做log off的时候发个指令去删除session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分session机制都使用会话cookie来保存session id,而关闭浏览器后这个session id就消失了,再次连接服务器时也就无法找到原来的session。如果服务器设置的cookie被保存到硬盘上,或者使用某种手段改写浏览器发出的HTTP请求头,把原来的session id发送给服务器,则再次打开浏览器仍然能够找到原来的session。

恰恰是由于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间,一般是30分钟,当距离客户端上一次使用session的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把session删除以节省存储空间。Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,无论是否对Session进行读写,服务器都会认为Session活跃了一次,重新开始计算失效时间。

我们也可以自己来控制session的有效时间:

1
2
复制代码session.invalidate()将session对象销毁
setMaxInactiveInterval(int interval) 设置有效时间,单位秒

在web.xml中配置session的有效时间: 

1
2
3
复制代码<session-config>
<session-timeout>30</session-timeout> 单位:分钟
<session-config>

所以,讨论了这么久,session的生命周期就是:

创建:Session存储在服务器端,一般放置在服务器的内存中(为了高速存取),Sessinon在用户访问第一次访问服务器时创建,需要注意只有访问JSP、Servlet等程序时才会创建Session,只访问HTML、IMAGE等静态资源并不会创建Session,可调用request.getSession(true)强制生成Session。

销毁:

1)超时,默认30分钟

2)执行api:session.invalidate()将session对象销毁、setMaxInactiveInterval(int interval) 设置有效时间,单位:秒

3)服务器非正常关闭(自杀,直接将JVM马上关闭)

如果正常关闭,session就会被持久化(写入到文件中,因为session默认的超时时间为30分钟,正常关闭后,就会将session持久化,等30分钟后,就会被删除)

位置: D:\java\tomcat\apache-tomcat-7.0.53\work\Catalina\localhost\test01\SESSIONS.ser

3.3、session id的URL重写

当浏览器将cookie禁用,基于cookie的session将不能正常工作,每次使用request.getSession() 都将创建一个新的session。达不到session共享数据的目的,但是我们知道原理,只需要将session id 传递给服务器session就可以正常工作的。

解决:通过URL将session id 传递给服务器:URL重写

1)手动方式: url;jsessionid=….

2)api方式:

1
2
复制代码encodeURL(java.lang.String url) 进行所有URL重写
encodeRedirectURL(java.lang.String url) 进行重定向 URL重写

这两个用法基本一致,只不过考虑特殊情况,要访问的链接可能会被Redirect到其他servlet去进行处理,这样你用上述方法带来的session的id信息不能被同时传送到其他servlet。这时候用encodeRedirectURL()方法就可以了。如果浏览器禁用cooke,api将自动追加session id ,如果没有禁用,api将不进行任何修改。

注意:如果浏览器禁用cookie,web项目的所有url都需进行重写。否则session将不能正常工作。

当cookie禁用时:

image.png

四、Session和Cookie的区别

4.1、从存储方式上比较

Cookie只能存储字符串,如果要存储非ASCII字符串还要对其编码。

Session可以存储任何类型的数据,可以把Session看成是一个容器

4.2、从隐私安全上比较

Cookie存储在浏览器中,对客户端是可见的。信息容易泄露出去。如果使用Cookie,最好将Cookie加密

Session存储在服务器上,对客户端是透明的。不存在敏感信息泄露问题。

4.3、从有效期上比较

Cookie保存在硬盘中,只需要设置maxAge属性为比较大的正整数,即使关闭浏览器,Cookie还是存在的

Session的保存在服务器中,设置maxInactiveInterval属性值来确定Session的有效期。并且Session依赖于名为JSESSIONID的Cookie,该Cookie默认的maxAge属性为-1。如果关闭了浏览器,该Session虽然没有从服务器中消亡,但也就失效了。

4.4、从对服务器的负担比较

Session是保存在服务器的,每个用户都会产生一个Session,如果是并发访问的用户非常多,是不能使用Session的,Session会消耗大量的内存。

Cookie是保存在客户端的。不占用服务器的资源。像baidu、Sina这样的大型网站,一般都是使用Cookie来进行会话跟踪。

4.5、从浏览器的支持上比较

如果浏览器禁用了Cookie,那么Cookie是无用的了!

如果浏览器禁用了Cookie,Session可以通过URL地址重写来进行会话跟踪。

4.6、从跨域名上比较

Cookie可以设置domain属性来实现跨域名

Session只在当前的域名内有效,不可夸域名

本文转载自: 掘金

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

技术分享:内存管理 正文

发表于 2018-12-14

导读:如何写一篇技术文章

  1. 确定目标读者群体
  2. 从问题出发,带着问题一步一步引出要讲解的内容
  3. 基于问题用逻辑推演导出内容
    1. 要有逻辑
  4. 斐波那契原则
    1. 层层递进

ps: 来自知乎问答


正文

本次分享的主题是:内存管理。
首先讲下为什么做这次分享,之前自己看过很多东西,但是呢,由于工作中没有用到,看完后呢就都忘了,就拿redis来说,相信很多人都看过《redis设计与实现》这本书,记得当时自己也一边看书一边看源码,但是现在也不记得什么了。所以呢,我就想有什么方法能够让自己更好的掌握理解所学的知识,即使这些内容在工作中暂时用不到,一个很好的办法就是做测验,即我们经常那一些问题来问自己,检验自己对内容的理解程度,所以本次分享我会先提问,然后为了回答这个问题,一步一步的给出本次分享的内容,当然一是由于内存管理主题太大。二是因为自己所知道的知识也有限,不可能面面俱到,因为自己也不知道自己不知道什么。只能后续不断补充完善内存管理的内容。

内存分配

先来讲内存分配,我们先看下面的一小段代码:

1
2
3
4
5
6
7
复制代码func main() {
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
fmt.Printf("pid: %d\n", os.Getpid())
http.ListenAndServe(":8080", nil)
}

功能非常简单,就是在8080端口启动了一个http服务,我们编译并且运行起来

1
2
3
复制代码go build main.go
./main
> pid: 3240424

通过ps命令查看进程详情(mac 下ps)

我们重点看两个指标:VSZ、RSS.
VSZ 是 Virtual Memory Size 虚拟内存大小的缩写, RSS 是 Resident Set Size 缩写,代表了程序实际使用物理内存。
这就很奇怪了,我们看到程序虚拟内存占用了约213.69MB,物理内存占有了5.30MB,那问题来了:
为什么虚拟内存比物理内存多这么多?
为了回答上这个问题,我们先介绍虚拟内存。


虚拟内存

要讲虚拟内存,我们首先从冯诺依曼体系说起,冯诺依曼体系将计算机主要分为了:cpu、内存、IO 这三部分,其中
我们先回答一个问题:可执行程序是怎么能够执行的?我们日常开发中go build main.go; ./main 当我们执行main的时候,为什么程序能够被执行?

首先用高级语言编写的程序要经过预处理、编译、汇编、链接,然后产生可执行的文件,只有经过链接的文件才能够被执行,我们可以看下线上可执行文件的类型:

1
2
复制代码file output/bin/xx.api
output/bin/xx.api: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped

里面几个关键字 ELF、dynamically linked、ld-linux-x86-64.so.2,我们分别来解释下。
首先 ELF 是 Linux下的一种可执行程序的类型,我们可以 man elf 查看具体的说明,

通读完manual后可以知道elf文件开始是一个elf头,然后是program header或者section table,这两个头描述了余下文件的内容。

先来看 elf 头,我们可以通过 readelf -h output/bin/x.api 查看

elf头的具体含义是定义在 elf.h 中,里面有个Elf64_Ehdr结构,每个字段的含义可以看手册。
在64位机器上elf header大小是64字节,我们可以通过命令hexdump -C output/bin/xx.api |head -n 10来查看数据,然后跟实际情况对比,借用一张网上图片:

更多关于elf的介绍可以看博客Introduction to the ELF Format

介绍完elf header,下面就是非常重要的两个概念:

  • program header
  • Section header

我们知道程序要经过预处理、编译、汇编、链接四个步骤才能成为可执行文件,其中汇编是将汇编文件转换为机器可执行的指令,里面还有一个非常重要的工作就是将文件按语义分段存储,常见的一些section就是代码段,数据段,debug段等,那为什么我们要按不同功能分段呢?
以下面的代码为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码int printf(const char *format, ...);

void func1(int i) {
printf("%d\n", i);
}

int main() {
static int static_var = 85;
static int static_var2;

int a = 1;
int b;

func1(static_var + static_var2 + a + b);

return a;
}

上面代码中我们声明了printf,但是没有定义它,我们在链接阶段需要重定位printf这个符号,为了能够方便去别的文件查找printf这个符号,自然文件需要有个导出符号表,这样子方便别的文件进行符号查找,所以我们为了更好的划分程序功能,同时也方便链接时进行查找,debug信息读取等,就有了Section header ,我们可以通过 readelf -S output/bin/xx.api来查看Section header

1
2
3
4
复制代码Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

上图我们可以看到虽然程序分为很多段,但是好多段的权限都是相同的,我们先记住这一点。

最后我们再来介绍下program header。现在我们已经有了可执行文件了,而且通过文件的开头的64字节呢,我们能校验这个文件是否确实是elf格式的,校验通过后呢,我们为了能够执行文件,自然就需要将文件加载进内存了,只有加载进来程序才能够执行。那问题就是如何加载程序了?

我们先通过readelf -l output/bin/xx.api来查看程序的program header

其中第3个 segment 是由 .text, .interp 等section组成,这个划分的原则就是按照相同权限的section合并。
另外我们可以看到每段字段的含义是:

  • offset:在文件中的偏移
  • virtAddr:虚拟地址空间,即程序加载进来后,在进程的地址空间中的地址
  • fileSize: segment 在文件中占用大大小
  • memSize: segment 加载进内存中占用的虚拟空间大小

到这,我们总结下 program header 和 Section header

  • Segment program header 执行视图,即被装载近内存中,地址空间分布
  • Section header 则是链接视图,elf在存储的时候是按照 section存储的,然后装载的时候,为了减少内存碎片,将相同权限的section合并为一个Segment装载进来,一个Segment基本可以对应一个vma。

上图是关于 section 和 segment的关系图,图片来自Interfacing with ELF files
再来一个关于可执行程序的静态视图和内存中的动态视图

图片来自Executable and Linkable Format 101

TLB

现在我们在重新整理下思路:

我们通过ps命令看到程序的虚拟内存比物理内存占用很多,为了回答为什么会存在这个现象,我们先得知道虚拟内存是啥,于是我们介绍了一个可执行程序是在磁盘上的静态视图:可执行程序按照不同功能划分为不同的section,当程序被加载进内存的时候,不同的section按照相同的权限组成一个segment被分配到程序的虚拟地址空间中。

现在我们已经提到了虚拟地址空间,每个程序都会有一个自己的虚拟地址空间,为什么每个程序都会有独立的地址空间呢?

首先程序执行其实是cpu在一行一行执行指令,cpu需要有地址才能去读取内存,然后再执行,这个地址最早呢就是物理内存地址,这就意味着所有程序链接的时候,都必须要指定彼此不同的物理地址,不然会加载进入内存后彼此覆盖,这个苛刻的要求显然随着程序越来越多是不可能的,于是呢,就有了虚拟地址空间一说,即每个程序都有自己的虚拟地址空间,然后cpu看到的是虚拟地址空间,但是物理内存肯定是需要物理地址才能访问的,所有就有个中间件将虚拟地址转换为物理地址。

evernotecid://684F00FC-2900-4AF6-B7AA-D9B72CB9AC48/appyinxiangcom/5364228/ENResource/p14937

上图是一个内存管理的硬件结构,图片来自Virtual Memory, Paging, and Swapping
整个一个翻译过程是cpu进行虚拟地址寻址,此时有个MMU:内存管理单元 memory management unit 负责将虚拟地址转换为物理地址,由于cpu的速度和物理内存之间速度存在差距大(大概200倍差),所以会有一个TLB: 翻译后背缓冲器(Translation Lookaside Buffer)专门来缓存这个映射关系,然后这个映射关系在实现上呢,需要用到页表,当发现虚拟地址还没有分配物理地址空间的时候,会触发缺页中断,此时会去查看这段虚拟地址对应到文件内容是啥,将其加载到内存中,在页表中建立起映射关系后,程序就可以继续执行了。
针对上面描述的这个过程,我们来回答几个问题:

  1. 页表是什么,以及为什么要使用页表?
  2. TLB中缓存的是什么?

我们先来回答为什么需要页表?
我们现在的目标是要建立虚拟地址和物理地址之间的映射关系,而内存一般我们可以将其看成是一个大数组,数组中每个元素大小1字节,那就意味着1G内存的物理空间我们就需要4G映射关系,即一个关系我们就需要4字节,简单表示就是

1
2
复制代码var maps [4*1024*1024*1024]int32
// 下标就是虚拟地址,值就是物理地址

即4G内存映射我们却需要16G来存储映射关系。这显然不可能,于是我们需要对物理内存进行大力度的划分,一般在32位机器时代,我们将物理内存按页划分,每页大小为4K,为了方便,我们假设虚拟地址也是按页划分,此时4G被划分为了1M个页,需要4M来存储这个映射关系,4M内存也就是需要1000个页。此时即使物理内存有4G,光保存进程的页表,我们就只能同时运行1000个。所以我们就采用多级页表的方案,下图是2级示例:

图片来自TLB and Pagewalk Coherence in x86 Processors
如果我们按照4M划分虚拟地址,则第一个映射关系只需要1K个项,即4k内存,一个物理页就可以了。


上面我们回答了我们为什么需要页表,下面我们回答TLB中缓存的是什么?
首先我们看一张图:
evernotecid://684F00FC-2900-4AF6-B7AA-D9B72CB9AC48/appyinxiangcom/5364228/ENResource/p14939

图片来自CPU Cache Flushing Fallacy
从上图可以看到内存访问速度是cpu的60多倍,因此如果每次做虚拟地址到物理地址的转换都要访问主存,显然速度是无法忍受的,因此我们就有了TLB作为cache加快访问。
我们可以通过命令cpuid来查看tlb信息,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码cpuid -1
L1 TLB/cache information: 2M/4M pages & L1 TLB (0x80000005/eax):
instruction # entries = 0xff (255)
instruction associativity = 0x1 (1)
data # entries = 0xff (255)
data associativity = 0x1 (1)
L1 TLB/cache information: 4K pages & L1 TLB (0x80000005/ebx):
instruction # entries = 0xff (255)
instruction associativity = 0x1 (1)
data # entries = 0xff (255)
data associativity = 0x1 (1)
L2 TLB/cache information: 2M/4M pages & L2 TLB (0x80000006/eax):
instruction # entries = 0x0 (0)
instruction associativity = L2 off (0)
data # entries = 0x0 (0)
data associativity = L2 off (0)
L2 TLB/cache information: 4K pages & L2 TLB (0x80000006/ebx):
instruction # entries = 0x200 (512)
instruction associativity = 4-way (4)
data # entries = 0x200 (512)
data associativity = 4-way (4)

一个在线观看地址Cache Organization on Your Machine
上面我们可以看到TLB也像cache一样分为L1和L2,L1 cache如果缓存大页2M/4M有256个项,缓存4k小页也是256项,L2 cache只能缓存4k小页512个。另外TLB也像cache一样分为 instruction-TLB (ITLB) 和 data-TLB (DTLB)。

所以现在我们知道了由于cpu和内存之间速度存在巨大差异,如果每次地址转换都需要访问内存,肯定性能会下降,所以TLB中缓存的就是虚拟地址到物理地址关系,一个示意图如下:

现在我们知道了页表是用来存储虚拟地址和物理地址映射关系的,也知道了为了加速转换过程,TLB作为高速缓存存储了这个映射关系,但是我们想下,之前我们说4G空间按4K页划分的话,就会有1K个表项,之前我们通过cpuid命令查看的时候,发现即使是TLB 2也只有512个项,那意味着着必须要做一个1024 -> 512的映射,这个怎么做呢?还有一个问题,之前我们看TLB信息的时候有个叫
instruction associativity = 4-way (4) 的概念,这个是什么意思?

我们想我们现在4G的虚拟地址被分为了1K份,每份是4M大小,那32位地址的话,就是前10位是页编号,后22位是页内偏移,但是我们现在只有256个TLB表项,那最简单的就是比较所有的表项,看256个表项中是否存在虚拟页号,这就需要在同一个时刻同时比较256行,随着这个数字的增加,会越来越难,所以我们就有了另一种方法。

我们将10位的虚拟页号的后8位用来选择256个表项,用高2位来比较是否是当前行

evernotecid://684F00FC-2900-4AF6-B7AA-D9B72CB9AC48/appyinxiangcom/5364228/ENResource/p14943

一个示意图
额外补充一个问题:我们为什么不用高位来选择组,低位来做tag比较呢?
evernotecid://684F00FC-2900-4AF6-B7AA-D9B72CB9AC48/appyinxiangcom/5364228/ENResource/p14944

图片来自《深入理解计算机系统》
原因很容易理解,如果我们采用高位来选择组,但是由于每组只有一个,意味着相邻的组映射到了同一行cache,程序的局部性不好。
然后此处 associativity = 4-way 意味着有4组,即2位来选择组,剩余的位做tag比较,更详细的内容可以看《深入理解计算机系统》第六章,写的非常详细。


小结

总结下,目前我们的内容:

  • 首先我们这次分享的主题是内存管理,会按照内存分配和垃圾回收两大块内容
  • 对于第一部分内存分配呢,我们从一个简单程序执执行起来后虚拟内存和物理内存占用非常大这个现象出发,想来回来为什么
  • 为了回答为什么会出现虚拟内存远大于物理内存的现象,我们先得知道什么是虚拟内存
  • 为了解释虚拟内存,我们引出了可执行程序两个视图:
    • 静态视图:在磁盘上格式是elf,按照程序功能分section存储
    • 动态视图:加载进内存的时候,会将section权限相同的进行合并,分配到同一个虚拟地址空间中
  • 在上面提到进程动态视图的时候,我们尝试回答为什么每个程序都会有自己的虚拟地址空间
    • 隔离:每个程序都可以在链接阶段自己分配程序执行地址
    • 安全性:每个程序只能当访问自己的地址空间内容
  • 因为每个程序都有自己的虚拟地址空间,但是实际机器执行的指令和数据都需要保存在物理内存中,这就需要对虚拟地址->物理地址进行翻译
  • 翻译时候,为了保存虚拟地址和物理地址的对应关系,我们有了页表,而为了减少页表占用的空间,我们有了多级页表
  • 由于cpu和内存速度之间的巨大差异,我们不可能每次都到内存中读取页表,所有有了TLB,而TLB本质上是一个cache,由于cache容量小于所有的对应关系的,就需要解决:
    • 快速查找对应关系是否在TLB中
    • 当TLB满的时候,进行淘汰
    • 保证TLB中数据和内存数据一致性
    • 。。。。
    • 以上这些问题本次没有具体展开,后续补上 mark。更新文章地址

现在我们有了上面的这些知识后,我们在整体上来描述一下在日常开发中我们执行go build main.go; ./main 时候都发生了什么,为什么程序能够被执行?

  1. 由于我们在是在bash中执行这个命令的,所有bash会首先通过fork创建一个新的进程出来
  2. 新进程通过execve系统调用指定执行elf文件,即main文件
  3. execve通过系统调用 sys_execve 进入内核态
  4. 内核调用链路:sys_execve -> do_execve -> search_binary_handle -> load_elf_binary
  5. 主要过程首先读取头128字确定文件格式,然后选择合适的装载程序将程序加载进内存
    1. 此处加载进内存只是读取program header,分配了虚拟地址空间,建立虚拟地址空间与可执行文件的映射关系
    2. 此处建立虚拟空间和可执行文件的映射关系就是为了在缺页异常的时候能够正确加载内容进来
    3. 发生缺页异常的时候,分配物理页,然后从磁盘将文件加载进来,这个时候才会真正占用物理内存
  6. 最后,我们将cpu指令寄存器设置为可执行文件的入口地址。程序就开始执行了。。。。

通过以上的这些内容,相信可以非常轻松的回答开始的问题了,每个程序被加载的时候,分配了虚拟空间地址,但是只有真正访问这些地址的时候,才会触发缺页中断,分配物理内存。
ps: cat /proc/21121/maps; cat /proc/21121/smaps可以查看进程详细的地址空间。

应用层内存管理

以上我们介绍了虚拟内存的概念,知道一个程序要执行,要经过层层步骤加载到内存中,才能被执行,上面介绍的这些内容我们如果将内存管理进行分层的话,应该是属于内核层和硬件层(TLB,cache)的内容,先来一张图:
evernotecid://684F00FC-2900-4AF6-B7AA-D9B72CB9AC48/appyinxiangcom/5364228/ENResource/p14945

图片来自《奔跑吧linux内核》
ps:这一本书也是待读的书单,但是暂时没找到工作中应用场景,可能先当做一本工具书了。
我们之前的内容是讲了非常少的一点内核层和硬件层的内存管理,现在我们要讲下应用层的内存管理,首先我们得知道,为什么内核有了内存管理,我们在应用层还需要做内存管理。

  1. 为了减少系统调用(brk,mmap)
  2. 操作系统不知道如何对内存进行复用,一旦进程申请内存后,这块内存就被进程占用了,只要它不释放,我们再也不能分配给别人
  3. 应用层自己做内存管理,可以更好的复用内存,同时能够和垃圾回收器配合,更好的管理内存

我们来看常见语言的内存管理实现:

  • C:malloc,free
  • C++:new,delete
  • Go:逃逸分析和垃圾回收

动手项目:如何自己实现个最小的malloc。

注意:以下内容,由于自己站的高度问题,不保证完全的正确。如果错误请不吝指出。


我们自己去设计内存分配器的时候,衡量的重要的指标是:

  • 吞吐率
  • 内存利用率

吞吐率指的是内存分配器每秒能够处理的请求数(包括分配和回收),内存利用率则是尽可能减少内部碎片和外部碎片。
ps:

  • 内部碎片是已经被分配出去的的内存空间大于请求所需的内存空间。
  • 外部碎片是指还没有分配出去,但是由于大小太小而无法分配给申请空间的新进程的内存空间空闲块。

下面我们来看下GO语言的内存分配器设计, 我们会分2部分来介绍:

  • tcmalloc
  • go语言allocator实现

tcmalloc

tcmalloc大名鼎鼎,对于tcmalloc的分析网上内容很多,此处就不多说了,大家自行Google。此处推荐一篇文章图解 TCMalloc,一图胜千言。
此处只提下为啥tcmalloc那么好。

  • 通过 thread-local-cache 分散了锁竞争(一把锁变多把锁)
  • 有借有还,当有连续内存不用的时候,还给内核

ps:在64位机器将内存还给操作系统非常有意思,释放内存的时候不释放 VA,把 VA 挂在 Heap 上,只是向操作系统提个建议,某一段 VA 暂时不用,可以解除 VA 和 PA 的 mmu 映射,操作系统可能会触发两种行为,第一种行为操作系统物理内存的确不够用有大量的换入换出操作,就解除,第二种操作系统觉得物理内存挺大的,就搁那,但是知道了这段 PA 空间不可用了,在 Heap 中并没有把 VA 释放掉,下次分配正好用到当时解除的 VA,有可能会引发两种行为,第一种是 PA 映射没有解除直接拿过来用,第二种是 PA 被解除掉了会引发操作系统缺页异常,操作系统就会补上这段物理内存,这个过程对用户空间来说是不可见的,这样在用户空间觉得这段内存根本没有释放过,因为用户空间看到的永远是 VA,VA 上某段内存可能存在也可能不存在,它是否存在对用户逻辑来说根本不关心,这地方实际上是操作系统来管理,操作系统通过 mmu 建立映射,这会造成 64 位 VA 地址空间非常的大,只要申请就不释放,下次重复使用,只不过重复使用会补上。现在内存管理在 64 位下简单的多,无非 VA 用就用了,就不释放。Windows 操作系统没有建议解除,只能说全部释放掉。

go allocator

此处也是推荐材料
Go’s Memory Allocator - Overview
GopherCon 2018 - Allocator Wrestling
此处只说 go的内存分配是从tcmalloc发展而来,基本思想一致, 下面是一个简图:
evernotecid://684F00FC-2900-4AF6-B7AA-D9B72CB9AC48/appyinxiangcom/5364228/ENResource/p15012

go 垃圾回收

好资料推荐
Golang’s Realtime GC in Theory and Practice
Getting to Go: The Journey of Go’s Garbage Collector

调优

上面介绍这么多后,我们来介绍一些实际开发中,应该怎么优化我们自己程序的例子。这部分后续再写一篇了专门介绍的

参考

Go Memory Management
CppCon 2017: Bob Steagall “How to Write a Custom Allocator
Go’s Memory Allocator - Overview
Golang’s Realtime GC in Theory and Practice
GopherCon 2018 - Allocator Wrestling
Golang’s Realtime GC in Theory and Practice
Getting to Go: The Journey of Go’s Garbage Collector

本文转载自: 掘金

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

SpringBoot21脚手架(种子)项目

发表于 2018-12-10

项目地址:github.com/ocubexo/spr…

项目已更新至2.0.0 Beta 查看更新内容

简介

这是一个基于SpringBoot 2.1.1 RELEASE,用于搭建RESTful API工程的脚手架,只需三分钟你就可以开始编写业务代码,不再烦恼于构建项目与风格统一。

快速开始

  1. 构建数据库
  2. 运行/src/test下的CodeGenerator.java进行代码生成
  3. 开始编写业务代码

内置功能与使用方法

RESTful风格Result生成器

1.成功且不带数据的结果

1
2
复制代码// 不带数据的成功结果
return new Result().success();

返回结果示例:

1
2
3
4
5
复制代码{
"code": 200,
"message": "Success",
"data": null
}

2.成功且带返回数据的结果

1
2
3
4
5
复制代码return new Result().success("Hello,world");

// 当然你也可以返回对象或其他类型的数据
User user = new User();
return new Result().success(user);

返回结果示例:

1
2
3
4
5
复制代码{
"code": 200,
"message": "Success",
"data": "Hello,world"
}

或者是:

1
2
3
4
5
6
7
8
复制代码{
"code": 200,
"message": "Success",
"data": {
"name": "jack",
"age": 20
}
}

3.错误结果:

1
2
3
4
5
复制代码// fail方法的参数(错误代码,错误信息)
return new Result().fail(10400, "登陆失败,密码错误");

// 你还可以自定义错误结果的code
return new Result().fail(null, "未登录", 401);

返回结果示例:

1
2
3
4
5
复制代码{
"code": 400,
"message": "登陆失败,密码错误",
"data": 10400
}

或者:

1
2
3
4
5
复制代码{
"code": 401,
"message": "未登录",
"data": null
}

RESTful风格的异常接管

1
2
3
4
5
复制代码// 参数说明(错误信息, 错误Code)
throw new ServiceException("未登录", 401);

// 你也可以返回错误代码
throw new ServiceException(10404, "服务器维护中", 404);

返回结果示例:

1
2
3
4
5
复制代码{
"code": 401,
"message": "未登录",
"data": null
}

或者:

1
2
3
4
5
复制代码{
"code": 404,
"message": "服务器维护中",
"data": 10404
}

基于JWT的认证机制

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Autowired
private TokenService tokenService;

// 生成Payload
Map<String,Object> payload = new HashMap<String,Object>();
payload.put("id",1);
// 生成Token
tokenService.generate(TokenType.ACCESS, payload, 1);


// 格式化Token
String token = getYourToken();
tokenService.parse(token); // 返回的结果是一个Jwt对象,详见JJWT文档

Auth注解

Auth注解用于获取当前用户的Token中的userId,在获取的同时会自动校验用户Token,若用户未登录则会抛出未登录的异常。

1
2
3
4
5
复制代码// 在controller中使用
@PostMapping("/user/1/edit")
public Result edit(@Auth int userId, @RequestBody sthPosted) {
// 根据ID判断权限
}

更新内容:

  1. 更加详细全面的功能文档
  2. 重构部分代码
  3. 添加更多的异常接管
  4. 添加自动插入创建与更新日期功能(基于MybatisPlus 特性)
  5. 优化代码结构,将功能性的部分模块化,便于后期的维护或升级为 SpringCloud 项目
  6. 增强@Auth 注解的代码健壮性

本文转载自: 掘金

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

从一道简单的“SpringBoot配置文件”相关面试题,我就

发表于 2018-12-05

面试要套路,也要技巧。别被背题目的兄弟们给忽悠了。

【你来发挥】你比较喜欢什么技术,哪一种最熟?

一般自信的面试官都喜欢问这个问题,这次面试的小伙比较年轻,咱也装回B,不然都对不起自己。

答: 我比较喜欢Spring,比较有趣。
目的: 希望应聘者能够有广度且有深度。如果最感兴趣的是Spring本身,而不是其上的解决方案,那顶多会承担被分解后的编码工作。

1
复制代码巧了,咱也熟。

【工作经验】SpringBoot相比较SpringMVC,有什么优缺点?

答: 有很多方面。觉得最好的就是不用写那么多配置文件了,直接写个注解,通过自动配置,就完成了初始化。

目的: 说什么无所谓,主要看有没有总结能力。判断是否用过早期的Spring版本,经历过版本更新更能了解软件开发之痛,接受新知识会考虑兼容和迭代。

【实现原理】我想要SpringBoot自动读取我的自定义配置,该做哪些事情?

答: 写一个相应的starter

目的: 判断是否了解和写过Spring Boot Starter,主要是META-INF目录下的spring.factories文件和AutoConfiguration。了解AOP更佳。

【烟幕弹】配置文件是yml格式,这种格式你喜欢么?

答: 比较喜欢properties格式,感觉yml格式的配置文件缩进比较难处理。比如当我从网上拷贝一些别人长长的配置文件,可能要花较多时间整理文件格式。

目的 此问题没有具体的意图,主要是过渡用。

【动手能力】这么喜欢properties方式,能够写一段代码,将yml翻译成properties么? 要是回答相反则反着来。

目的 通过简单的伪代码,判断应聘者的动手能力和编码风格。是喜欢问题抽象化还是喜欢立刻动手去写。我希望回答能够有条理,而且能够考虑各种异常情况,比如把自己判断不了的配置交给用户处理;比如空格和的处理。

【提示】提示一下,你用什么数据结构进行存储?

目的 假如应聘者在一段时间内不能有任何产出,会给出简单的提示。找准了存储结构,你就基本完成了工作,此问题还判断了应聘者的培养成本和价值。

【基础算法】哦,是树结构,遍历方式是怎样的?前序,后序,中序?

目的 判断是否有基础的算法知识。做工程先不要求会什么动态规划或者贪心算法,但起码的数据结构是要了解的。

【基础知识】你用到了Map?Java中如何做可排序的Map?

目的 是否对java的基础集合类熟悉,期望回答TreeMap,如果回答上来,可能会追问它是什么数据结构(红黑树)。

##【知识广度】你还接触过哪些配置方式?比较喜欢那种?

目的 了解应聘者的知识广度,说不出来也无所谓,了解的多会加分。比如ini、cfg、json、toml、序列化等。

【项目规模】我想要把我的配置放在云端,比如数据库密码等,改怎么做?

目的 是否了解SpringBoot的组件SpringConfig,或者了解一些其他的开源组件如携程的apollo等。

【知识广度】我想要配置文件改动的时候,所有机器自动更新,该怎么办?

目的 了解是否知晓常用的同步方式。有两种:一种是定时去轮询更新;一种是使用zk或者etcd这种主动通知的组件。

【实现细节】Spring是如何进行配置文件刷新的?

目的 这个可真是没写过就真不知道了,主要是
org.springframework.cloud.context.scope.refresh.RefreshScope
这个类

【架构能力】现在我想要将配置文件分发到一部分机器,也就是带有版本的灰度概念,你该如何处理?

目的 如果能够从网关、微服务约定,后台操作原型方面去多方位描述一下,更佳。

这样筛选的小伙伴,都很棒!能力多少,心中有数。

本文转载自: 掘金

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

SQL中 where 子句和having子句中的区别

发表于 2018-12-03

前言:
1.where 不能放在GROUP BY 后面
2.HAVING 是跟GROUP BY 连在一起用的,放在GROUP BY 后面,此时的作用相当于WHERE
3.WHERE 后面的条件中不能有聚集函数,比如SUM(),AVG()等,而HAVING 可以
Where和Having都是对查询结果的一种筛选,说的书面点就是设定条件的语句。下面分别说明其用法和异同点。注:本文使用字段为oracle数据库中默认用户scott下面的emp表,sal代表员工工资,deptno代表部门编号。

一、聚合函数

说明前我们先了解下聚合函数:聚合函数有时候也叫统计函数,它们的作用通常是对一组数据的统计,比如说求最大值,最小值,总数,平均值(
MAX,MIN,COUNT, AVG)等。这些函数和其它函数的根本区别就是它们一般作用在多条记录上。简单举个例子:SELECT SUM(sal) FROM emp,这里的SUM作用是统计emp表中sal(工资)字段的总和,结果就是该查询只返回一个结果,即工资总和。通过使用GROUP BY 子句,可以让SUM 和 COUNT 这些函数对属于一组的数据起作用。

二、where子句

where自居仅仅用于从from子句中返回的值,from子句返回的每一行数据都会用where子句中的条件进行判断筛选。where子句中允许使用比较运算符(>,<,>=,<=,<>,!=|等)和逻辑运算符(and,or,not)。由于大家对where子句都比较熟悉,在此不在赘述。

三、having子句

having子句通常是与order by 子句一起使用的。因为having的作用是对使用group by进行分组统计后的结果进行进一步的筛选。举个例子:现在需要找到部门工资总和大于10000的部门编号?
第一步:

1
复制代码select deptno,sum(sal) from emp group by deptno;

筛选结果如下:

DEPTNO SUM(SAL)

—— ———-

30 9400

20 10875

10 8750

可以看出我们想要的结果了。不过现在我们如果想要部门工资总和大于10000的呢?那么想到了对分组统计结果进行筛选的having来帮我们完成。
第二步:

1
复制代码select deptno,sum(sal) from emp group by deptno having sum(sal)>10000;

筛选结果如下:

DEPTNO SUM(SAL)

—— ———-

20 10875

当然这个结果正是我们想要的。

四、下面我们通过where子句和having子句的对比,更进一步的理解它们。

在查询过程中聚合语句(sum,min,max,avg,count)要比having子句优先执行,简单的理解为只有有了统计结果后我才能执行筛选。where子句在查询过程中执行优先级别优先于聚合语句(sum,min,max,avg,count),因为它是一句一句筛选的。HAVING子句可以让我们筛选成组后的对各组数据筛选。,而WHERE子句在聚合前先筛选记录。如:现在我们想要部门号不等于10的部门并且工资总和大于8000的部门编号?
我们这样分析:通过where子句筛选出部门编号不为10的部门,然后在对部门工资进行统计,然后再使用having子句对统计结果进行筛选。

1
2
3
复制代码select deptno,sum(sal) from emp 
where deptno!='10' group by deptno
having sum(sal)>8000;

筛选结果如下:

DEPTNO SUM(SAL)

—— ———-

30 9400

20 10875

五、异同点

它们的相似之处就是定义搜索条件,不同之处是where子句为单个筛选而having子句与组有关,而不是与单个的行有关。
最后:理解having子句和where子句最好的方法就是基础select语句中的那些句子的处理次序:where子句只能接收from子句输出的数据,而having子句则可以接受来自group by,where或者from子句的输入。

本文转载自: 掘金

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

阿里内部的那个牛逼带闪电的Java诊断工具终于开源了

发表于 2018-12-03

在阿里巴巴内部,有很多自研工具供开发者使用,其中有一款工具,是几乎每个Java开发都使用过的工具,那就是Arthas,这是一款Java诊断工具,是一款牛逼带闪电的工具。该工具已于2018年9月份开源。

1
复制代码GitHub地址:https://github.com/alibaba/arthas用户文档:https://alibaba.github.io/arthas/

在日常开发中,你是否遇到过以下问题:

这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?

我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?

遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?

线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!

是否有一个全局视角来查看系统的运行状况?

有什么办法可以监控到JVM的实时运行状态?

以上问题,通通可以通过Arthas来进行问题诊断!!!是不是很好很强大。

Arthas支持JDK 6+,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

Arthas安装


1、使用arthas-boot安装

下载arthas-boot.jar,然后用java -jar的方式启动:

1
复制代码wget https://alibaba.github.io/arthas/arthas-boot.jarjava -jar arthas-boot.jar

打印帮助信息:

1
复制代码java -jar arthas-boot.jar -h

如果下载速度比较慢,可以使用aliyun的镜像:

1
复制代码java -jar arthas-boot.jar --repo-mirror aliyun --use-http

2、使用as.sh安装

Arthas 支持在 Linux/Unix/Mac 等平台上一键安装,请复制以下内容,并粘贴到命令行中,敲 回车 执行即可:

1
复制代码curl -L https://alibaba.github.io/arthas/install.sh | sh

上述命令会下载启动脚本文件 as.sh 到当前目录,你可以放在任何地方或将其加入到 $PATH 中。

直接在shell下面执行./as.sh,就会进入交互界面。

也可以执行./as.sh -h来获取更多参数信息。

快速入门


1. 启动Demo

1
复制代码wget https://alibaba.github.io/arthas/arthas-demo.jarjava -jar arthas-demo.jar

arthas-demo是一个简单的程序,每隔一秒生成一个随机数,再执行质因式分解,并打印出分解结果。

2. 启动arthas

在命令行下面执行:

1
复制代码wget https://alibaba.github.io/arthas/arthas-boot.jarjava -jar arthas-boot.jar
  • 执行该程序的用户需要和目标进程具有相同的权限。比如以admin用户来执行:sudo su admin && java -jar arthas-boot.jar 或 sudo -u admin -EH java -jar arthas-boot.jar。
  • 如果attatch不上目标进程,可以查看~/logs/arthas/ 目录下的日志。
  • 如果下载速度比较慢,可以使用aliyun的镜像:java -jar arthas-boot.jar --repo-mirror aliyun --use-http
  • java -jar arthas-boot.jar -h 打印更多参数信息。

选择应用java进程:

1
复制代码$ $ java -jar arthas-boot.jar* [1]: 35542  [2]: 71560 arthas-demo.jar

Demo进程是第2个,则输入2,再输入回车/enter。Arthas会attach到目标进程上,并输出日志:

1
复制代码[INFO] Try to attach process 71560[INFO] Attach process 71560 success.[INFO] arthas-client connect 127.0.0.1 3658  ,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---. /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'wiki: https://alibaba.github.io/arthasversion: 3.0.5.20181127201536pid: 71560time: 2018-11-28 19:16:24$

3. 查看dashboard

输入dashboard,按enter/回车,会展示当前进程的信息,按ctrl+c可以中断执行。

1
复制代码$ dashboardID     NAME                   GROUP          PRIORI STATE  %CPU    TIME   INTERRU DAEMON17     pool-2-thread-1        system         5      WAITIN 67      0:0    false   false27     Timer-for-arthas-dashb system         10     RUNNAB 32      0:0    false   true11     AsyncAppender-Worker-a system         9      WAITIN 0       0:0    false   true9      Attach Listener        system         9      RUNNAB 0       0:0    false   true3      Finalizer              system         8      WAITIN 0       0:0    false   true2      Reference Handler      system         10     WAITIN 0       0:0    false   true4      Signal Dispatcher      system         9      RUNNAB 0       0:0    false   true26     as-command-execute-dae system         10     TIMED_ 0       0:0    false   true13     job-timeout            system         9      TIMED_ 0       0:0    false   true1      main                   main           5      TIMED_ 0       0:0    false   false14     nioEventLoopGroup-2-1  system         10     RUNNAB 0       0:0    false   false18     nioEventLoopGroup-2-2  system         10     RUNNAB 0       0:0    false   false23     nioEventLoopGroup-2-3  system         10     RUNNAB 0       0:0    false   false15     nioEventLoopGroup-3-1  system         10     RUNNAB 0       0:0    false   falseMemory             used   total max    usage GCheap               32M    155M  1820M  1.77% gc.ps_scavenge.count  4ps_eden_space      14M    65M   672M   2.21% gc.ps_scavenge.time(m 166ps_survivor_space  4M     5M    5M           s)ps_old_gen         12M    85M   1365M  0.91% gc.ps_marksweep.count 0nonheap            20M    23M   -1           gc.ps_marksweep.time( 0code_cache         3M     5M    240M   1.32% ms)Runtimeos.name                Mac OS Xos.version             10.13.4java.version           1.8.0_162java.home              /Library/Java/JavaVir                       tualMachines/jdk1.8.0                       _162.jdk/Contents/Hom                       e/jre

4. 通过sysenv命令来获取到进程的Main Class

1
复制代码$ sysenv | grep MAIN JAVA_MAIN_CLASS_71560              demo.MathGame

5. 通过jad来反编绎Main Class

1
复制代码$ jad demo.MathGameClassLoader:+-sun.misc.Launcher$AppClassLoader@3d4eac69  +-sun.misc.Launcher$ExtClassLoader@66350f69Location:/tmp/arthas-demo.jar/* * Decompiled with CFR 0_132. */package demo;import java.io.PrintStream;import java.util.ArrayList;import java.util.Iterator;import java.util.List;import java.util.Random;import java.util.concurrent.TimeUnit;public class MathGame {    private static Random random = new Random();    private int illegalArgumentCount = 0;    public static void main(String[] args) throws InterruptedException {        MathGame game = new MathGame();        do {            game.run();            TimeUnit.SECONDS.sleep(1L);        } while (true);    }    public void run() throws InterruptedException {        try {            int number = random.nextInt();            List<Integer> primeFactors = this.primeFactors(number);            MathGame.print(number, primeFactors);        }        catch (Exception e) {            System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage());        }    }    public static void print(int number, List<Integer> primeFactors) {        StringBuffer sb = new StringBuffer("" + number + "=");        Iterator<Integer> iterator = primeFactors.iterator();        while (iterator.hasNext()) {            int factor = iterator.next();            sb.append(factor).append('*');        }        if (sb.charAt(sb.length() - 1) == '*') {            sb.deleteCharAt(sb.length() - 1);        }        System.out.println(sb);    }    public List<Integer> primeFactors(int number) {        if (number < 2) {            ++this.illegalArgumentCount;            throw new IllegalArgumentException("number is: " + number + ", need >= 2");        }        ArrayList<Integer> result = new ArrayList<Integer>();        int i = 2;        while (i <= number) {            if (number % i == 0) {                result.add(i);                number /= i;                i = 2;                continue;            }            ++i;        }        return result;    }}Affect(row-cnt:1) cost in 970 ms.

6. watch

通过watch命令来查看demo.MathGame#primeFactors函数的返回值:

1
复制代码$ watch demo.MathGame primeFactors returnObjPress Ctrl+C to abort.Affect(class-cnt:1 , method-cnt:1) cost in 107 ms.ts=2018-11-28 19:22:30; [cost=1.715367ms] result=nullts=2018-11-28 19:22:31; [cost=0.185203ms] result=nullts=2018-11-28 19:22:32; [cost=19.012416ms] result=@ArrayList[    @Integer[5],    @Integer[47],    @Integer[2675531],]ts=2018-11-28 19:22:33; [cost=0.311395ms] result=@ArrayList[    @Integer[2],    @Integer[5],    @Integer[317],    @Integer[503],    @Integer[887],]ts=2018-11-28 19:22:34; [cost=10.136007ms] result=@ArrayList[    @Integer[2],    @Integer[2],    @Integer[3],    @Integer[3],    @Integer[31],    @Integer[717593],]ts=2018-11-28 19:22:35; [cost=29.969732ms] result=@ArrayList[    @Integer[5],    @Integer[29],    @Integer[7651739],]

5. 退出arthas

如果只是退出当前的连接,可以用quit或者exit命令。Attach到目标进程上的arthas还会继续运行,端口会保持开放,下次连接时可以直接连接上。

如果想完全退出arthas,可以执行shutdown命令。

常用命令


基础命令

  • help——查看命令帮助信息
  • cls——清空当前屏幕区域
  • session——查看当前会话的信息
  • reset——重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类
  • version——输出当前目标 Java 进程所加载的 Arthas 版本号
  • quit——退出当前 Arthas 客户端,其他 Arthas 客户端不受影响
  • shutdown——关闭 Arthas 服务端,所有 Arthas 客户端全部退出
  • keymap——Arthas快捷键列表及自定义快捷键

jvm相关

  • dashboard——当前系统的实时数据面板
  • thread——查看当前 JVM 的线程堆栈信息
  • jvm——查看当前 JVM 的信息
  • sysprop——查看和修改JVM的系统属性
  • New! getstatic——查看类的静态属性

class/classloader相关

  • sc——查看JVM已加载的类信息
  • sm——查看已加载类的方法信息
  • dump——dump 已加载类的 byte code 到特定目录
  • redefine——加载外部的.class文件,redefine到JVM里
  • jad——反编译指定已加载类的源码
  • classloader——查看classloader的继承树,urls,类加载信息,使用classloader去getResource

monitor/watch/trace相关

  • monitor——方法执行监控
  • watch——方法执行数据观测
  • trace——方法内部调用路径,并输出方法路径上的每个节点上耗时
  • stack——输出当前方法被调用的调用路径
  • tt——方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测

请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 shutdown 或将增强过的类执行 reset 命令。

options

  • options——查看或设置Arthas全局开关

管道

Arthas支持使用管道对上述命令的结果进行进一步的处理,如sm org.apache.log4j.Logger | grep
  • grep——搜索满足条件的结果
  • plaintext——将命令的结果去除颜色
  • wc——按行统计输出结果

以上,就是关于Arthas的简单介绍,要想真正的融会贯通,真正的把他作为一个排查问题的利器,还需要自己动手实践下!所谓实践出真知。

Hollis公众号的文章已经授权<维权骑士>进行原创维权,为避免不必要的版权追责问题,转载请注明出处!

2018年最后一个月,Hollis的知识星球限时折扣中。深入理解Java中的并发编程:到底什么是线程安全?欢迎您的加入。


直面Java第175期:什么是Java8 中的LocalDate和localTime?

成神之路第015期:深入学习Java中的枚举。

- MORE | 更多精彩文章 -

  • 阿里员工的Java问题排查工具单
  • 贡献Dubbo生态,阿里开源Nacos项目
  • 外行人都能看懂的SpringCloud,错过了血亏!
  • 为什么阿里禁止工程师直接使用日志系统的 API

如果你喜欢本文。

请长按二维码,关注Hollis


转发朋友圈,是对我最大的支持。

本文转载自: 掘金

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

1…881882883…956

开发者博客

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