临时抱佛脚系列

1.JVM调优
对JVM内存的系统级调优主要目的是减少GC频率和Full GC的次数。
1)监控GC的状态,使用各种JVM工具,查看当前日志,并且分析当前堆内存快照和GC日志,根据实际情况看是否需要优化。
2)通过JMX的MBean或者Java的jmap生成当前Heap信息,并使用Visual VM或者Eclipse自带的Mat分析dump文件。
3)如果参数设置合理,没有超时日志,GC频率GC耗时都不高,则没有GC优化的必要,如果GC频率很高或者GC时间超过1秒,则必须优化。
4)调整GC类型和内存分配,使用1台和多台机器进行测试,进行性能对比,再做修改,最后通过不断的试验和试错,分析并找到最合适的参数。

2.JVM内存区域划分
1)程序计数器
每个线程都有自己的程序计数器,并且在任何时间一个线程都只有一个方法在执行。程序计数器会存储当前线程正在执行Java方法的JVM指令地址。
2)Java虚拟机栈
每个线程再创建时都会创建一个虚拟机栈,其内部保存着一个个的栈帧,对应着一次次的Java方法调用。JVM直接对Java栈的操作只有2个,就是进栈和出栈。栈帧中存储着局部变量表、操作数栈、动态链接、方法正常退出或异常退出的定义等。
3)堆
它是Java内存管理的核心区域,用来放置Java对象实例,几乎所有创建的Java对象实例都直接被分配到堆上,堆被所有的线程共享。
4)方法区
所有线程共享的一块内存区域,用于存储所谓的元数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。JDK 8中将永久代移除,同时增加了元数据区。
5)运行时常量池
这是方法区的一部分。Java的常量池可以存放各种常量信息。
6)本地方法栈
它和Java虚拟机栈十分相似,支持对本地方法的调用,也是每个线程都会创建一个。

3.G1 垃圾收集器
G1 GC这是一种兼顾吞吐量和停顿时间的GC实现,是JDK 9以后默认GC选项。G1可以直观的设定目标的停顿时间。G1 GC仍然存在年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个region。region之间是复制算法,但整体上可以看作是标记-整理算法。
JVM会尽量划分2048个大小相等的region,数值是在1M到32M字节之间的一个2的幂指数,G1可以根据堆大小进行自动调整,也可以手动调整。在G1实现中,一部分region是作为Eden,一部分作为Survivor、old region。
在新生代,G1采用的是并行的复制算法;在老年代使用的是mixed GC,会回收整个新生代,还会回收一部分的old region。
1)新生代GC
在分配一般对象(非巨型对象)时,当所有Eden region使用达到最大阈值并且无法申请足够内存时,会触发一次Young GC。每次Young GC会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。
2)mixed GC
越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器即mixed GC。除了回收部分Old region,还有回收整个Young region,这里需要注意的是回收部分老年代而不是全部老年代,可以选择对哪些old region进行收集,从而控制垃圾回收的耗时。
缺点:region的大小和大对象很难保证一致,这会导致空间的浪费。

4.HashMap的底层原理
HashMap实现Map接口,非线程安全的,区别于ConcurrentHashMap在于允许使用null值和null键,不保证映射的顺序,底层数据结构是数组+链表+红黑树。
put()方法:
1)根据key计算得到key.hash=(h=k.hashCode())^(h>>>16);
2)根据key.hash计算得到桶数组的索引index=key.hash&(table.length-1),这样就找到该key的存放位置了:1.如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null。2.如果该位置有数据且是红黑树,那么执行相应的插入/更新操作。3.如果该位置有数据且是链表,那么分为两种情况,一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样:如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null;如果该链表已经有这个节点了,那么找到该节点并更新新数据,然后返回老数据。
扩容时机?怎么扩容?
扩容时机:当链表长度大于8的时候并且数组的长度小于64时优先进行扩容;当元素的个数大于阈值时,进行扩容。
怎么扩容:1)首先进行异常情况判断,一是否需要进行初始化,二若当前容量<最大值则不扩容。2)然后根据新容量(原容量的2倍)新建数组,将旧数组上的数据(键值对)转移到新数组中,这里包括遍历旧数组中的每个元素,重新计算每个数据在数组中的存放位置,将旧数组上的每个数据逐个转移到新数组中,这里采用的是尾插法。3)新数组table引用到HashMap的table属性上。4)最后重新设置扩容阈值,此时哈希表=扩容2倍&转移了旧数据的新table。

5.ConcurrentHashMap的底层原理
数据结构采用voliate修饰的table数组+单向链表+红黑树的结构,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。
get()方法:
1.计算hash值,定位带该table索引位置,如果是首节点,匹配就返回。
2.如果遇到扩容的时候,会调用find()方法,查找该节点,匹配就返回。
3.以上情况都不符合的话,就往下遍历节点,匹配,匹配就返回,否则返回null。
put()方法:
1.如果没有初始化,就先调用initTable()方法来进行初始化。
2.如果没有hash冲突就直接CAS插入。
3.如果还在进行扩容操作就先进行扩容。
4.如果存在hash冲突,就加锁来保证线程安全。这里有两种情况:一是链表形式直接遍历到尾端插入;二是按照红黑树结构插入。
5.最后如果该链表的数量大于阈值8,就要先转换成红黑树结构。
6.如果添加成功就调用addCount()方法计算size,并检查是否需要扩容。
扩容时机?怎么扩容?
扩容时机:当链表长度大于2并且数组的长度小于64时,优先进行扩容;当数组元素个数大于阈值,会触发transfer()方法,重新调整节点的位置,进行扩容。
怎么扩容:
1)单线程扩容
它的大体思想就是遍历、复制的过程。首先根据运算得到需要遍历的次数i,然后利用tabAt()方法获得位置i的元素。如果这个位置是空,就在原table中的位置i放入forwardNode节点,这个也是触发并发扩容的关键点;如果这个位置是Node节点,如果它是一个链表的头节点,就构造一个反序链表,把它们分别放在nextTable的i和i+n的位置上;如果这个位置是TreeBin节点,也做一个反序处理,并且判断是否需要untreefi,把处理的结果分别放在nextTable的i和i+n的位置上。遍历过所有节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的0.75倍,完成扩容。
2)多线程扩容
如果遍历到的节点是forward节点,就向后继续遍历,再加上给节点上锁的机制,就完成了多线程的控制。多线程遍历节点,处理了一个节点,就把对应点的值set为forward,另一个线程看到forward,就向后遍历,这样交叉完成复制工作。
size()方法:
1)JDK 8推荐使用mappingCount()方法,因为这个方法的返回值是long类型,不会因为size方法是int类型限制最大值。
2)在没有并发的情况下,使用一个名为baseCount的voliate变量就足够了,当并发的时候,CAS修改baseCount失败后,就会创建一个CounterCell对象,通常对象的voliate value属性是1.在计算size的时候,会将baseCount和CounterCell数组中的元素的value累加,得到总值,但这个数字仍可能是不准确的。
3) 还有一个需要注意的地方就是这个counterCell类使用了@sun.misc.Contented注解标识,这个注解是防止伪共享的,是1.8新增的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值