首页 文章 精选 留言 我的

精选列表

搜索[java],共10000篇文章
优秀的个人博客,低调大师

Java 开源办公开发平台 O2OA V6.3 发布 | ElementUI 组件上线!

O2OA6.3版本中,新增了一组基于Vue的ElementUI组件,在流程表单设计界面,就可以在左边的工具栏找到ElementUI组件。它能让界面设计更加简洁直观,只需要将对应的组件拖动到表单设计区域就可以创建组件,还能保证产品设计人员搭建逻辑清晰、搭建出结构合理且高效易用的产品。 创建界面的布局容器后,可以选择字段类型组件、自动完成框、计数器、选择框、级联选择器、按钮、通用组件等进行快速搭建,提升设计效率,让界面更加美观,结构更加合理。以日期范围选择器为例,点击"Preview"按钮可预览组件样式: 保存表单就创建了一个用与选择日期范围的组件,组件的值会自动绑定到“v-model”属性指向的key,本例中,选择框的值被绑定到了this.data.value1中。我们再给日期选择框添加一些快捷选项,保存表单后即可看到效果: 除此之外,此次版本在流程管理中新增了onlyoffice控件、wps控件、金格控件、永中控件作为正文控件,支持在线编辑word、excel、ppt、pdf等文件,加强与同事之间的远程协同;新增了LibreOffice预览,满足了用户在线预览各类文件的需求,助力协同办公,实现高效的团队管理。 O2OA V6.3还包含其他的功能更新和问题修正,让平台更稳定,用户操作更方便: 功能新增 [通用]HTML编辑器CKEditor升级到4161,增加了阅读状态的图片延迟加载功能、浏览原图功能。涉及的应用有流程表单、内容管理表单和论坛帖子 [数据中心]新增了查询视图中导出Excel的功能 [内容管理]新增了内容管理文档的操作条中置顶的功能,并为内容管理列表增加置顶标记 [服务管理]新增了服务管理的代理和接口开发界面调试的功能 [流程管理]新增了表单设计界面增加数据模板的时候带入默认相关组件,数据模板导出字段配置自动获取的功能 [流程管理]新增了onlyoffice控件 [流程管理]新增了wps控件 [流程管理]新增了金格控件 [流程管理]新增了永中控件 [流程管理]新增了LibreOffice预览 [移动办公]新增了微信公众号,关注回复的消息的功能 [移动办公]新增了企业微信考勤数据导入查询功能 [移动办公]新增了移动端App支持tokenName修改的功能 [移动办公]新增了移动端App支持通讯录权限控制的功能 [移动办公]新增了移动端App个人信息页面的个人属性展现的功能 [数据库]新增了南大通用GBASE华库数据库支持 [日志]新增了服务器http request access log [流程平台]新增了流程起草权限增加群组设置的功能 [内容管理]新增了根据条件查找附件的接口 [流程平台]新增了流程起草增加权限校验 [人员组织]新增了人员组织管理模块接口mockput和mockdelete [流程平台]新增了待办、待阅根据title查询 [平台架构]新增了平台审计日志自定义程序分析功能 [流程平台]增加了公文编辑器转换Word后加密的功能 [流程平台]增加了公文编辑器加盖图片章的功能 [流程平台]新增了公文编辑器增加标题字体定义的功能 [流程平台]新增了公文编辑器保证版记在偶数页的功能 [流程平台]新增了公文编辑器增加附件内容编辑的功能 [流程平台]新增了公文编辑器增加编辑器属性配置的功能 [平台架构]新增了主菜单排序设置功能,管理员可设置默认和强制方式 [平台架构]新增了一组ElementUI组件 功能优化 [考勤管理]优化了考勤管理界面 [脚本API]优化了脚本API(增加了后台脚本API,增加了发送待阅、添加参阅) [内容管理]优化了内容管理表单事件,增加postSave、 postPublish [内容管理]整理了内容管理操作条的图标 [移动办公]优化了主题切换功能 [移动办公]优化了移动端已阅意见功能支持 [移动办公]O2云连接配置UI修改,以及一些页面功能调整优化 [移动办公]优化了移动端App分享功能 [移动办公]优化了移动端App拍照功能 [服务器]集群增加健康检查 [服务器]优化了线程池 [服务器]优化了缓存机制 [认证]验证码改为全匹配,并更新验证码实现 [内容管理]优化了文档发布消息发送条件,未配置消息类型不往消息处理器发送消息 [人员组织]优化了人员身份唯一编码,使其根据组织编码和人员唯一编码生成 [内容管理]优化了文档权限刷新,增加多线程处理,增加根据文档类型刷新权限 [内容管理]优化了分页查询的查询速度 [内容管理]优化了review表索引,减少不必要索引,增加联合索引 [流程平台]优化了公文编辑器格式展现 [流程平台]公文编辑器粘贴表格时,控制合适的宽度 [流程平台]公文编辑器只有一个附件时不显示序号 [平台架构]修复this.data绑定的Array数据类型问题 [平台优化]基于Authorization请求头的系统认证 问题修复 [流程管理]修复了重置处理人未剔除待办人的问题 [流程管理]修复了人员选择保存范围刷新后变回精简的问题 [内容管理]修复了内容管理设计端语言包上的一些问题 [流程管理]修复了表单Tab组件设置宽度无效的问题 [流程管理]修复了表单中TextArea组件只读时setData的问题 [流程管理]修复了数据表格移动端条目为0时无添加按钮的问题 [移动办公]修复了钉钉、企业微信考勤数据展现的bug [流程引擎]修复了定时节点无法清空已选值的bug [内容管理]修复了文档阅读记录出现cipher用户的问题 [内容管理]修复了根据创建时间查询文档错误的问题 [流程平台]修复了根据状态分页查找work的bug [内容管理]修复了文档保存时未保存置顶信息的问题 [流程平台]修复了执行this.data.save脚本时,在表单未载入完全时,可能造成数据丢失的问题 [平台架构]修复了loadCss方法会多次载入的问题 [流程平台]去除流程启动前和流程结束前两个事件 [平台架构]修复Promise的uncatch错误

优秀的个人博客,低调大师

🏆Java技术专题-JVM研究系列(36) 性能调优之CMS垃圾回收器(上)

前提概要 如果没有冬天,春天不会如此悦人;如果没有偶尔的不幸,幸运不会如此受人欢迎。 CMS垃圾回收的6个重要阶段 initial-mark 初始标记(CMS的第一个STW阶段),标记GC Root直接引用的对象,GC Root直接引用的对象不多,所以很快。 concurrent-mark并发标记阶段,由第一阶段标记过的对象出发,所有可达的对象都在本阶段标记。 concurrent-preclean 并发预清理阶段,也是一个并发执行的阶段。在本阶段,会查找前一阶段执行过程中,[从新生代晋升或新分配或被更新的对象]。通过并发地重新扫描这些对象,预清理阶段可以减少下一个stop-the-world 重新标记阶段的工作量。 concurrent-abortable-preclean,并发可中止的预清理阶段。这个阶段其实跟上一个阶段做的东西一样,也是为了减少下一个STW重新标记阶段的工作量。增加这一阶段是为了让我们可以控制这个阶段的结束时机,比如扫描多长时间(默认5秒)或者Eden区使用占比达到期望比例(默认50%)就结束本阶段。 remark重标记阶段(CMS的第二个STW阶段),暂停所有用户线程,从GC Root开始重新扫描整堆,标记存活的对象。需要注意的是,虽然CMS只回收老年代的垃圾对象,但是这个阶段依然需要扫描新生代,因为很多GC Root都在新生代,而这些GC Root指向的对象又在老年代,这称为“跨代引用”。 concurrent-sweep ,并发清理。 分析 分析其GC日志,发现GC发生在CMS的收集阶段。 箭头1 显示abortable-preclean阶段耗时4.04秒。 箭头2 显示的是remark阶段,耗时0.11秒。 虽然abortable-preclean阶段是concurrent的,不会暂停其他的用户线程。就算不优化,可能影响也不大。 调优之前先看下该应用的GC统计数据,包括GC次数,耗时: 统计期间内(18天)发生CMS GC 69次,其中abortable preclean阶段平均耗时2.45秒,final remark阶段平均112ms,最大耗时170ms。 优化目标 降低abortable preclean时间,而且不增加final remark的时间(因为remark是STW的)。 JVM参数调优 第一次调优 先尝试调低abortable preclean阶段的时间,看看效果。 有两个参数可以控制这个阶段何时结束: -XX:CMSMaxAbortablePrecleanTime=5000 默认值5s,代表该阶段最大的持续时间 -XX:CMSScheduleRemarkEdenPenetration=50 默认值50%,代表Eden区使用比例超过50%就结束该阶段进入remark 调整为最大持续时间为1s,Eden区使用占比10%,如下: -XX:CMSMaxAbortablePrecleanTime=1000 -XX:CMSScheduleRemarkEdenPenetration=10 为什么调整成这样两个值:首先每次CMS都发生在老年代使用占比达到80%时,因为这是由下面两个参数决定的: -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly 这两个设置一般配合使用,一般用于『降低CMS GC频率或者增加频率、减少GC时长』的需求 -XX:CMSInitiatingOccupancyFraction=80 是指设定CMS在对内存占用率达到80%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC); -XX:+UseCMSInitiatingOccupancyOnly :标志来命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期。 当该标志被开启时,JVM通过CMSInitiatingOccupancyFraction的值进行每一次CMS收集,而不仅仅是第一次。(否则后续会动态控制回收阈值) (慎用) 因此,只有当我们充足的理由(比如测试)并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。 老年代的增长是由于部分对象在Minor GC后仍然存活,被晋升到老年代,导致老年代使用占比增长的,也就是在每次CMS GC发生之前刚刚发生过一次Minor GC,所以在那一刻新生代的使用占比是很低的。 那么我们预计这个时候尽快结束abortable preclean阶段,在remark时就不需要扫描太多的Eden区对象,remark STW的时间也就不会太长。 第一次调整参数 在统计期间(17小时左右)内,发生过2次CMS GC。Abortable Preclean 平均耗时835ms,这是预期内的。但是Final Remark 平均耗时495ms(调整前是112ms),其中一次是80ms,另一次是910ms!将近1秒钟!Remark是STW的!对于要求低延时的应用来说这是无法接受的! [YG occupancy: 181274 K (1887488 K)] - 年轻代当前占用情况和总容量 耗时80ms的这次remark发生时(早上9点,非高峰时段),新生代(YG)占用181.274M。 remark耗时910ms的那次GC日志 [YG occupancy: 773427 K (1887488 K)] 耗时910ms的这次remark发生时(晚上10点左右,高峰时段),新生代(YG)占用773.427M。因为这个时候高峰期,新生代的占用量上升的非常快,几乎同样的时间内,非高峰时段仅上升到181M,但是高峰时段就上升到773M。 如果abortale preclean阶段时间太短,随后在remark时,新生代占用越大,则remark持续的时间(STW)越长。 不缩短abortale preclean耗时会出现过程gc;缩短的话,remark阶段又会变长,而且是STW,更不能接受。 对于这种情况,CMS提供了CMSScavengeBeforeRemark参数,尝试在remark阶段之前进行一次Minor GC,以降低新生代的占用。 第二次调优 增加 -XX:+CMSScavengeBeforeRemark 不是没有代价的,因为这会增加一次Minor GC停顿。所以这个方案好或者不好的判断标准就是:增加CMSScavengeBeforeRemark参数之后的minor GC停顿时间 + remark 停顿时间如果比增加之前的remark GC停顿时间要小,这才是好的方案。 -XX:+CMSScavengeBeforeRemark: 在CMS GC前启动一次ygc,目的在于减少old gen对ygc gen的引用,降低remark时的开销-----一般CMS的GC耗时 80%都在remark阶段 第二次调整的结果 在统计期间(20小时左右)内,发生3次CMS GC。Abortable preclean 平均耗时693ms。Final remark平均耗时50ms,最大耗时60ms。Final remark的时间比调优前的平均时间(112ms)更低。 3次CMS GC remark前的Minor GC日志分析 第1次是非高峰时段的表现,Minor GC 耗时 0.01s + remark耗时 0.06s = 0.07s = 70ms,如下 第2次是高峰时段,Minor GC 耗时 0.01s + remark耗时 0.05s = 0.06s = 60ms,如下 第3次是非高峰时段,Minor GC 耗时 0.00s + remark耗时 0.04s = 0.04s = 40ms,如下 所以,3次Minor GC + remark耗时的平均耗时 < 60ms,这比第一次调优时remark平均耗时495ms好得多了。 总结 解决abortable preclean 时间过长的方案可以归结为两步: 缩短abortable preclean 时长,通过调整这两个参数: -XX:CMSMaxAbortablePrecleanTime=xxx -XX:CMSScheduleRemarkEdenPenetration=xxx 调整为多少的一个判断标准是:abortable preclean阶段结束时,新生代的空间占用不能大于某个参考值。 在前面第一次调优后,新生代(YG)占用181.274M,remark耗时80ms;新生代(YG)占用773.427M时,remark耗时910ms。所以这个参考值可以是300M。 而如果新生代增长过快,像这次调优应用2秒内就能用光2G新生代堆空间的,就只能通过CMSScavengeBeforeRemark做一次Minor GC了。 增加CMSScavengeBeforeRemark参数开启remark前进行Minor GC的尝试 虽然官方说明这个增加这个参数是尝试进行Minor GC,不一定会进行。但实际使用起来,几乎每次remark前都会Minor GC。

优秀的个人博客,低调大师

Java技术探索】HashMap深入浅出的源码分析(JDK1.7版本)

每日一句 有望得到的要努力,无望得到的不介意,则无论输赢姿态都会好看。 概念回顾 HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可; 如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,急需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的equals方法逐一比对查找。 所以,性能考虑,HashMap中的链表出现越少,性能才会越好。 不同JVM版本HashMap的展现形式 本章内容主要为介绍JDK7的版本文章学习。 JDK7 HashMap的数据结构为:数组 + 链表 JDK8 可以查看相关对应的另外一篇【JDK8的HashMap源码分析】 HashMap的数据结构为:数组 + 链表 + 红黑树 基本简介 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理就是基于此。 数据结构的分析 针对于多种数据结构进行分析为什么会选择Hash表的方式进行存储和查询。 数组 采用一段连续的存储单元来存储数据。 查询操作场景 对于指定下标的查找,时间复杂度为O(1); 通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n)。 对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn); 插入删除场景 对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。 对应到集合类是ArrayList。 线性链表 查询操作场景 查找操作需要遍历链表逐一进行比对,复杂度为O(n)。 插入删除场景 链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1)。 对应的集合类是LinkedList。 二叉树 对一棵相对平衡的有序二叉树。 对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。 对应的集合类有TreeSet和TreeMap。 哈希表 相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。 对应的集合类就是HashMap。 哈希表的主干就是数组。我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。 即:存储位置 = hash(关键字) 其中,这个函数hash一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。这会涉及到哈希冲突。 哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀。 Hash冲突机制 当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。 数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢? 哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址)、再散列函数法、链地址法。 而HashMap即是采用了链地址法,也就是数组+链表的方式。 HashMap的源码实现 存储结构 Hash桶(bucket) 当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。 每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。 Entry HashMap的基本组成单元,每一个Entry包含一个key-value键值对。 Entry是HashMap中的一个静态内部类。代码如下: static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; //存储指向下一个Entry的引用,单链表结构 Entry<K,V> next; //对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算 int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } 经过以上分析,HashMap的存储结构图如下: 一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。 一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。 比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。 在存储一对值时(Key ->Value对),实际上是存储在一个Entry的对象e中,程序通过key计算出Entry对象的存储位置。 Key->Value的对应关系是通过key—-Entry—-value这个过程实现的,所以就有我们表面上知道的key存在哪里,value就存在哪里。 重要属性 先看HashMap中的几个重要属性: //默认初始化化容量,即16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量,即2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默认装载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态 static final Entry<?,?>[] EMPTY_TABLE = {}; //空的存储实体 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //实际存储的key-value键值对的个数 transient int size; //阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold int threshold; //负载因子,代表了table的填充度有多少,默认是0.75 final float loadFactor; //用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException transient int modCount; //默认的threshold值 static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE; //计算Hash值时的key种子值 transient int hashSeed = 0; 构造方法 HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值。initialCapacity默认为16,loadFactory默认为0.75。 //通过初始容量和状态因子构造HashMap public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0)//参数有效性检查 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY)//参数有效性检查 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor))//参数有效性检查 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; // init方法在HashMap中没有实际实现,不过在其子类如 // linkedHashMap中就会有对应实现 init(); } //通过扩容因子构造HashMap,容量去默认值,即16 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //装载因子取0.75,容量取16,构造HashMap public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } //通过其他Map来初始化HashMap, // 容量通过其他Map的size来计算,装载因子取0.75 public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); inflateTable(threshold);//初始化HashMap底层的数组结构 putAllForCreate(m);//添加m中的元素 } 从上面这段代码我们可以看出,在常规构造器中,并没有马上为数组table分配内存空间(有一个入参为指定Map的构造器例外),事实上是在执行第一次put操作的时候才真正构建table数组。 put操作 如果两个key通过hash%Entry[].length得到的index相同,为了解决这个问题,HashMap里面用到链式数据结构的一个概念。 上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。 一会后又进来一个键值对B,通过计算其index也等于0,HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C; 这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。 也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现,我们应该已经清楚了。 public V put(K key, V value) { //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(=16) if (table == EMPTY_TABLE) { inflateTable(threshold);//分配数组空间 } //如果key为null,存储位置为table[0]或table[0]的冲突链上 if (key == null) return putForNullKey(value); int hash = hash(key); //对key的hashcode进一步计算,确保散列均匀 int i = indexFor(hash, table.length);//获取在table中的实际位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this);//调用value的回调函数,其实这个函数也为空实现 return oldValue; } } modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败 addEntry(hash, key, value, i);//新增一个entry return null; } putForNullKey 方法 key为null的时候,只会放在hashMap的0位置(即key的hashCode为0,对数组长度取余后的下标也是0),不会有链表 在HashMap源码中对put方法对null做了处理,key为null的判断后进入putForNullKey(V value)这个方法,里面for循环是在talbe[0]链表中查找key为null的元素,如果找到,则将value重新赋值给这个元素的value,并返回原来的value。如果没找到则将这个元素添加到talbe[0]链表的表头 /** * Offloaded version of put for null keys */ private V putForNullKey(V value) { // for循环处理key为空的情况 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; } inflateTable的源码如下: private void inflateTable(int toSize) { //capacity一定是2的次幂 int capacity = roundUpToPowerOf2(toSize); //此处为threshold赋值,取capacity*loadFactor和 MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //分配空间 table = new Entry[capacity]; //选择合适的Hash因子 initHashSeedAsNeeded(capacity); } inflateTable这个方法用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。 二次幂其实现如下: private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; } roundUpToPowerOf2中的这段处理使得数组长度一定为2的次幂,Integer.highestOneBit是用来获取最左边的bit(其他bit位为0)所代表的数值。 在对数组进行空间分配后,会根据hash函数计算散列值,其实现如下: //用了很多的异或,移位等运算,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀 final int hash(Object k) { int h = hashSeed; //这里针对String优化了Hash函数,是否使用新的Hash函数和Hash // 因子有关 if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } 从上面的操作看以看出,影响HashMap元素的存储位置的只有key的值,与value值无关。 通过hash函数得到散列值后,再通过indexFor进一步处理来获取实际的存储位置 其实现如下: //返回数组下标 static int indexFor(int h, int length) { return h & (length-1); } h &(length-1)保证获取的index一定在数组范围内,举个例子,默认容量16,length-1=15,h=18,转换成二进制计算为 最终计算出的index=2。有些版本的对于此处的计算会使用取模运算,也能保证index一定在数组范围内,不过位运算对计算机来说,性能更高一些(HashMap中有大量位运算)。 通过以上分析,我们看到,要得到一个元素的存储位置,需要如下几步: 获取该元素的key值 通过hash方法得到key的散列值,其中需要用到key的hashcode值。 通过indexFor计算得到存储的下标位置。 最后,得到存储的下标位置后,我们就可以将元素放入HashMap中,具体通过addEntry实现: void addEntry(int hash, K key, V value, int bucketIndex) { //当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容,新容量为旧容量的2倍 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; //扩容后重新计算插入的位置下标 bucketIndex = indexFor(hash, table.length); } //把元素放入HashMap的桶的对应位置 createEntry(hash, key, value, bucketIndex); } //创建元素 void createEntry(int hash, K key, V value, int bucketIndex) { //获取待插入位置元素 Entry<K,V> e = table[bucketIndex]; //这里执行链接操作,使得新插入的元素指向原有元素。 table[bucketIndex] = new Entry<>(hash, key, value, e); //这保证了新插入的元素总是在链表的头 //元素个数+1 size++; } 通过以上代码能够得知,当发生哈希冲突并且size大于阈值的时候并且对应的key对应的table桶的首地址元素不为null的情况下:需要进行数组扩容 扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。 扩容操作 扩容操作通过resize操作实现: //按新的容量扩容Hash表 void resize(int newCapacity) { Entry[] oldTable = table;//老的数据 int oldCapacity = oldTable.length;//获取老的容量值 //老的容量值已经到了最大容量值 if (oldCapacity == MAXIMUM_CAPACITY) { //修改扩容阀值 threshold = Integer.MAX_VALUE; return; } //新的结构 Entry[] newTable = new Entry[newCapacity]; //将老的表中的数据拷贝到新的结构中 transfer(newTable, initHashSeedAsNeeded(newCapacity)); //修改HashMap的底层数组 table = newTable; //修改阀值 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } 如果数组进行扩容,数组长度发生变化,而存储位置 index = h&(length-1),index也可能会发生变化,需要重新计算index,我们先来看看transfer这个方法。 //将老的表中的数据拷贝到新的结构中 void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length;//容量 for (Entry<K,V> e : table) { //遍历所有桶 while(null != e) { //遍历桶中所有元素(是一个链表) Entry<K,V> next = e.next; //如果是重新Hash,则需要重新计算hash值. if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } //定位Hash桶 int i = indexFor(e.hash, newCapacity); //元素连接到桶中,这里相当于单链表的插入,总是插入在最前面 e.next = newTable[i]; //newTable[i]的值总是最新插入的值 newTable[i] = e; e = next;//继续下一个元素 } } } 这个方法将老数组中的数据逐个链表地遍历,重新计算后放入新的扩容后的数组中,我们的数组索引位置的计算是通过对key值的hashcode进行hash扰乱运算后,再通过和 length-1进行位运算得到最终数组索引位置。 注意:HashMap数组元素长度的设计 通过源码可以发现,hashMap的数组长度一定保持2的次幂,这样做有什么好处呢? //根据Hash值和Hash表的大小选择合适的Hash桶 static int indexFor(int h, int length) { return h & (length-1); } 如果length为2的次幂,其二进制表示就是100….0000;则length-1 转化为二进制必定是0111….11的形式,在于h的二进制与操作效率会非常的快,而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,再于h与操作。 最后一位都为0,所以0001,0011,0101,1001,1011,0111,1101这几个位置永远都不会存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。 get操作 //获取key值为key的元素值 public V get(Object key) { if (key == null)//如果Key值为空,则获取对应的值,这里也可以看到,HashMap允许null的key,其内部针对null的key有特殊的逻辑 return getForNullKey(); Entry<K,V> entry = getEntry(key);//获取实体 return null == entry ? null : entry.getValue();//判断是否为空,不为空,则获取对应的值 } //获取key为null的实体 private V getForNullKey() { if (size == 0) {//如果元素个数为0,则直接返回null return null; } //key为null的元素存储在table的第0个位置 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null)//判断是否为null return e.value;//返回其值 } return null; } get方法通过key值返回对应value,如果key为null,直接去table[0]处检索。我们再看一下getEntry这个方法: //获取键值为key的元素 final Entry<K,V> getEntry(Object key) { if (size == 0) {//元素个数为0 return null;//直接返回null } int hash = (key == null) ? 0 : hash(key);//获取key的Hash值 for (Entry<K,V> e = table[indexFor(hash, table.length)];//根据key和表的长度,定位到Hash桶 e != null; e = e.next) {//进行遍历 Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))//判断Hash值和对应的key,合适则返回值 return e; } return null; } get方法的实现相对简单,key(hashcode)–>hash–>indexFor–>最终索引位置,找到对应位置table[i],再查看是否有链表,遍历链表,通过key的equals方法比对查找对应的记录。 在定位到数组位置之后然后遍历链表的时候,e.hash == hash这个判断没必要,仅通过equals判断就可以。 HashMap中的hashcode怎么生成 调用对象key的hashCode方法,再对这个hashcode方法进行一些右移以及异或运算(使的hashCode的高位和低位都参与到运算中);通过右移和异或运算可以使hashMap的散列化更强,提高hashMap的get方法的效率 为什么使用HashCode HashCode的存在主要是为了查找的快捷性,HashCode是用来在散列存储结构中确定对象的存储地址的 ( 用hashcode来代表对象在hash表中的位置 ) , hashCode存在的重要的原因之一就是在HashMap(HashSet其实就是HashMap)中使用(其实Object类的hashCode方法注释已经说明了)。 equals方法和hashcode的关系 若重写了equals(Object obj)方法,则有必要重写hashCode()方法 若两个对象equals(Object obj)返回true,则hashCode()有必要也返回相同的int数 若两个对象equals(Object obj)返回false,则hashCode()不一定返回不同的int数 若两个对象hashCode()返回相同int数,则equals(Object obj)不一定返回true 若两个对象hashCode()返回不同int数,则equals(Object obj)一定返回false 同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题 总结 HashMap之所以速度快,因为他使用的是散列表,根据key的hashcode值生成数组下标(通过内存地址直接查找,不需要判断,但是需要多出很多内存,相当于以空间换时间)

优秀的个人博客,低调大师

Jweb-boot v1.0.3 发布,通用的 Java Web 微服务极速开发基础框架

Jweb-boot 是基于JFinal+Jboot+undertow进行开发增强而来的极速开发基础框架,无它! 愿景 瑞士军刀 成为web开发的一把瑞士军刀。统一框架自由组合,以适应不同的系统结构。让不管是单体系统还是分布式微服务平台,配置调一调就可以快速组建完成! 看的懂,改的动 必竟程序员都有洁癖,普遍认为别人写的代码都是垃圾,不改一改太难用了,至少我是这么认为的。希望本人写的代码大家能看的懂,改的动! 特点 在Jboot的基础上开发,所有继承了Jboot+JFinal的全部特点,并在此基础上进行增强。 Jboot特点 基于JFinal的 MVC + ORM 快速开发。 基于 ShardingSphere + Seata 分布式事务 和 分库分表。 基于 Dubbo 或 Motan 的 RPC 实现 基于 Sentinel 的分布式限流和降级 基于 Apollo 和 Nacos 的分布式配置中心 基于 EhCache 和 Redis 的分布式二级缓存 Jweb-boot特点 提供jweb-discovery模块,支撑网关服务代理,提供各服务间直接发现与调用 提供jweb-security安全框架,借鉴于shiro,简化用户认证权限授权系统开发。 增强Jboot网关功能,适配jweb-discovery服务发现功能 增强Nacos分布式配置功能 增强jboot资源监控,资源查找功能 附录: Jweb极速开发官网:jweb.cc

优秀的个人博客,低调大师

java项目如何部署服务器-----如何传输文件到阿里云服务器(三)

步骤1:ftp做什么用 接下来就要在Linux上安装ftp服务器了。 ftp的全称是 File Transfer Protocol : 文件传输协议。 顾名思义,ftp就是用来我们把本地的文件传输到服务器上,或者从服务器上下载文件用的。 接下来的章节就会讲解如何在linux 上安装ftp服务器 步骤2:安装服务器相关内容 Linux上的ftp服务器有各种型号,我们会使用vsftpd,全称是:very secure FTP daemon 非常安全的ftp后台程序。。。 为了使得这个服务器可以使用还要做许多配合工作,比如创建用户,配置端口,开放端口等等活计,后面会一一展开 部署到Linux系列教材 (二)- FTP - 安装 步骤1:介绍vsftpd vsftpd 全称是:very secure FTP daemon 非常安全的ftp后台程序,及ftp 服务端 步骤2:安装命令 yum install vsftpd -y 执行之后,最后看到Complete! 就表示安装成功了 步骤3:查看服务启动状态 执行命令: systemctl start vsftpd.servicesystemctl status vsftpd.service 第一步是用来启动vsftpd服务,不一定需要,因为安装命令结束后就会自动启动,但是有时候不会自动启动,所以还是加上吧 可以看到最后 Started vsftpd ftp daemon 已经启动成功了注:在以前版本的Centos 是使用service vsftpd status来进行查看的,但是Centos 7 之后的版本,就已经改用命令systemctl 来查看了。 在购买ECS服务器-镜像部分,选择的是Centos 7.4 64位 步骤4:关闭和启动 一般说来关闭,启动,查看状态几个功能会如图所示一起执行,这样便于查看重启是否成功 systemctl stop vsftpd.servicesystemctl start vsftpd.servicesystemctl status vsftpd.service 步骤5:检查端口 ftp服务启动的是21端口,使用如下命令应该观察到21端口正在处于监听状态,这也从侧面反应了ftp服务器启动成功了 netstat -anp|grep21 部署到Linux系列教材 (三)- FTP - ftp用户 步骤1:用户概念 要连接上 vsftpd 服务器需要为Linux创建专门的用户。 接下来就会讲解如何创建这个用户 步骤2:用户目录 在Linux中,不同用户是有不同目录访问权限的,所以首先创建一个目录,作为这个ftp用户所拥有的目录。 mkdir -p /home/wwwroot/ftptest 步骤3:创建用户 执行如下命令,创建用户ftptest,并且指定其目录为用户目录中创建的/home/wwwroot/ftptest useradd -d /home/wwwroot/ftptest -g ftp -s /sbin/nologin ftptest -g ftp 表示该用户属于ftp分组 (ftp分组是内置的,本来就存在,不需要自己创建) -s /sbin/nologin 表示这个用户不能用来登录secureCRT这样的客户端。 这种不能登陆的用户又叫做虚拟用户 创建过程给出的警告信息是正常的,不用理会 步骤4:设置目录权限 chown -R ftptest /home/wwwroot/ftptestchmod -R775/home/wwwroot/ftptest 把目录/home/wwwroot/ftptest的拥有者设置为ftptest 使ftptest用户拥有这个目录的读写权限 步骤5:设置密码 为ftptest用户设置密码: passwd ftptest 密码修改成功后会如图所示出现成功提示。注:密码使用paw123###,后续连接ftp服务器的时候会用到 部署到Linux系列教材 (四)- FTP - 配置用户 步骤1:配置用户 通过上一步创建了ftp用户接下来就要在vsftpd服务器中配置该用户 步骤2:去掉匿名登陆 默认情况下vsftpd服务器是允许匿名登陆的,这样非常不安全,所以要把这个选项关闭掉。 首先通过vi命令打开ftp服务器配置文件: vi /etc/vsftpd/vsftpd.conf 然后把本来的 anonymous_enable=YES 修改为 anonymous_enable=NO 修改完成之后,保存退出。 对使用vi命令打开,编辑,保存不熟悉的同学请参考前面的教程:VI命令 步骤3:限制用户访问 接下来是限制用户访问,什么叫做限制用户访问呢?ftp用户教程中创建的ftptest用户所拥有的目录是 /home/wwwroot/ftptest,如果不做限制,那么使用ftptest登陆之后可以切换到其他敏感目录去,比如切换到/usr目录去,这样就存在巨大的安全隐患。 为了规避这个隐患,需要限制ftptest用户只能通过ftp访问到 /home/wwwroot/ftptest 目录以及子目录。 配置办法: 首先通过vi命令打开ftp服务器配置文件: vi /etc/vsftpd/vsftpd.conf 找到: #chroot_list_enable=YES# (defaultfollows)#chroot_list_file=/etc/vsftpd.chroot_list 并修改为: chroot_list_enable=YES# (defaultfollows)chroot_list_file=/etc/vsftpd/chroot_list chroot_list_enable=YES: 表示对用户访问进行限制 chroot_list_file=/etc/vsftpd/chroot_list 表示对chroot_list里面指定的用户进行限制 下一个步骤用户清单就会对chroot_list 这个文件进行编辑 修改完成之后,保存退出。 对使用vi命令打开,编辑,保存不熟悉的同学请参考前面的教程:VI命令 步骤4:用户清单 接着上一个步骤,在chroot_list中添加ftptest用户 首先通过vi命令打开chroot_list文件(此文件本来是空的): vi /etc/vsftpd/chroot_list 然后增加一行: ftptest 修改完成之后,保存退出。 对使用vi命令打开,编辑,保存不熟悉的同学请参考前面的教程:VI命令 步骤5:允许写权限 vsftpd服务器是这样的,一旦某个用户被限制访问了,那么默认情况下,该用户的写权限也被剥夺了。 这就导致ftp客户端连接上服务器之后无法上传文件。 这个时候,就需要打开此用户的写权限,请按照如下办法操作: 首先通过vi命令打开ftp服务器配置文件: vi /etc/vsftpd/vsftpd.conf 在最后面新加一行: allow_writeable_chroot=YES 修改完成之后,保存退出。 对使用vi命令打开,编辑,保存不熟悉的同学请参考前面的教程:VI命令 部署到Linux系列教材 (五)- FTP - 配置端口 步骤1:两种端口 vsftpd有两种端口,一个是21端口,用来监听客户端连接请求的。 这个一般说来是固定的,就一直使用21端口。 另一种是,一旦获取到请求之后,再专门用户服务端和客户端传输数据的端口。 本知识点就是用于指定第二种端口的获取范围 步骤2:配置端口 打开配置文件: vi /etc/vsftpd/vsftpd.conf 在最后添加: pasv_enable=YESpasv_min_port=30000pasv_max_port=30010 这表示使用被动模式,用于传输数据的端口分配从30000-30010之间。 在后续的Linux开放端口教程中也会做相应的配合工作。 部署到Linux系列教材 (六)- FTP - 开放端口 步骤1:端口概念 要访问Linux的端口,必须开通才行。 在购买ECS服务器-网络步骤里,只开通了22和3389端口。 22就是SecureCRT链接Linux服务器用的端口号。 3389 是阿里云控制台链接用的端口,这里没有用到。 只有这两个端口号是不够用的,为了能够链接vsftpd服务器还需要开放21端口和 30000-30010端口 步骤2:安全组 既然用的是阿里云,那么我们就借助阿里云的安全组操作来实现端口的开放效果。 登陆阿里云后,按照如下顺序选择:云服务器ECS->安全组->配置规则 步骤3:当前安全组规则一览 目前安全组规则是3个,分别是22,3389和ICMP协议。(ICMP是什么?我也不晓得。。。反正本教材没有直接用到~) 然后点下载区(点击进入)的添加安全组规则 步骤4:添加21端口 如图所示只需要修改两个: 端口范围: 21/21 表示从21开始,到21结束 授权对象: 0.0.0.0/0 表示所有的ip地址都可以访问该端口 步骤5:添加30000-30010端口 如图所示只需要修改两个: 端口范围: 30000/30010 表示从30000开始,到30010结束 授权对象: 0.0.0.0/0 表示所有的ip地址都可以访问该端口 步骤6:添加后安全组规则一览 如图所示,增加了两个新的规则 部署到Linux系列教材 (七)- FTP - ftp客户端 步骤1:上传 步骤2:ftprush ftp客户端有各种各样的版本,我个人习惯使用ftprush.下载区(点击进入)下载解压即用。 打开ftprush.exe之后看到如图所示的界面 步骤3:连接 菜单->站点->快速连接对话框 主机地址:输入服务器地址 用户名: ftptest 密码: paw123### 以上信息使用的是ftp用户教程中的账号密码 步骤4:连接成功界面 连接成功之后,就可以看到如图所示 左边是服务端:/home/wwwroot/ftptest里的内容,暂时什么都没有 右边是本地: 一些文件 步骤5:上传和下载 把右边的内容拖到左边,就是上传。。。。 把左边的内容拖到右边,就是下载。。。 如图所示把abc.txt 上传到了服务器 步骤6:到Linux下观察 进入Linux,然后输入命令 ls /home/wwwroot/ftptest/ 就可以观察到abc.txt 这个文件,确认上传成功啦

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

用户登录
用户注册