HashMap 技术问题点解答

Hash的理解

hash的基本概念就是把任意长度的输入通过一个hash算法之后,映射成固定长度的输出

那你这里提到了任意长度的输入转化成固定长度的输出,会不会有问题?

肯定会有问题的。在程序中(可能)碰到两个value值经过hash算法之后,算出同样的hash值,也就是会发生hash冲突

那hash冲突可以避免么?

理论上是没有办法避免的,就类比“抽屉原理”,比如说一共有10个苹果,但是咱一共有9个抽屉,最终一定会有一个抽屉里的数量是大于1的,所以hash冲突没有办法避免,只能尽量避免。

你那认为稍微好一些的hash算法,它考虑的点,应该是哪些呢?

首先这个hash算法,它一定效率得高,要做到长文本也能高效计算出hash值嘛,这二点就是hash值不能让它逆推出原文吧;两次输入,只要有一点不同,它也得保证这个hash值是不同的。其次,就是尽可能的要分散吧,因为,在table中slot中slot大总会发都处于空闲状的,要尽可能降低hash冲突。

HashMap中存储数据的结构,长什么样啊?

JDK1.7 是 数组 + 链表;
JDK1.8是 数组 + 链表 + 红黑树,每个数据单元都是一个Node结构,Node结构中有key字段、有value字段、还有next字段、还有hash字段。next字段就是发生hash冲突的时候,当前桶位中node与冲突的node连成一个链表要用的字段。

那如果创建HashMap的时候,你没有去指定这个hashmap中的这个散列表数组长度,那初始长度是多少啊?

初始长度默认是16

那这个散列表,new HashMap() 的时候就创建了,还是说在什么时候创建的?

散列表是懒加载机制,只有第一次put数据的时候,它才创建的

默认的负载因子是多少并且这个负载因子有啥用?

默认是…默认负载因子0.75,就是75%,负载因子它的作用就是计算扩容阈值用的,比如使用无参构造方法创建的hashmap对象,它默认情况下扩容阈值就 16*0.75 = 12

链表它转化为这个红黑树需在达到什么条件?

链表转红黑树,主要是有两个指标,其中一个就是链表长度达到8,这有个指标就是当前散列表数组长度它已经达到64。否则的话,就算slot内部链表长度到了8,它也不会链转树,它仅仅会发生一次resize,散列表扩容。

Node对象内部有一个hash字段,对吧?然后这个hash字段的值是key对象的hashcode() 返回值么?

不是的

你那说一下,这个hash值是怎么得到的呢?

这个hash值是key.hashcode二次加工得到的。
加工原则是:key的hashcode 高16位 ^ 低16位,得到的一个新值。

hashCode值为什么需要高16位 ^ 低16位

主要为了优化hash算法,因为hashmap内部散列表,它大多数场景下,它不会特别大。也就是说这个[table.length - 1] 得到的这个二进制数,实际有效位很有限,一般都在(低)16位以内,这样的话,key的hash值高16位就等于完全浪费了,没起到作用。所以,node的hash字段才采用了 高16位 异或 低16位 这种方式来搞它。

那你说下,hashmap Put写数据的具体流程吧,尽可能的详细点去说,好吧

主要为4种情况:
前面这个,寻址算法是一样的,都是根据key的hashcode 经过 高低位 异或 之后的值,然后再 按位与 & (table.length -1),得到一个槽位下标,然后根据这个槽内状况,状况不同,情况也不同,大概就是4种状态,
第一种是slot == null,直接占用slot就可以了,然后把当前put方法传进来的key和value包状成一个Node 对象,放到这个slot中就可以了
第二种是slot != null 并且 它引用的node 还没有链化;需要对比一下,node的key 与当前put 对象的key 是否完全相等;如果完全相等的话,这个操作就是replace操作,就是替换操作,把那个新的value替换当前slot -> node.value 就可以了;否则的话,这次put操作就是一个正儿八经的hash冲突了,slot->node 后面追加一个node就可以了,采用尾插法。
第三种就是slot 内的node已经链化了;这种情况和第二种情况处理很相似,首先也是迭代查找node,看看链表上的元素的key,与当前传来的key是不是完全一致。如果一致的话,还是repleace操作,替换当前node.value,否则的话就是我们迭代到链表尾节点也没有匹配到完全一致的node,把put数据包装成node追加到链表尾部;这块还没完,还需要再检查一下当前链表长度,有没有达到树化阈值,如果达到阈值的话,就调用一个树化方法,树化操作都在这个方法里完成
第四种就是冲突很严重的情况下,就是那个链已经转化成红黑树了

红黑树写入操作的第一部分吧 ,就是说找到它的父节点的流程吧

说清楚红黑树,首先说清楚TreeNode,TreeNode它就是继承Node结构,在Node基础上加了几个字段,分别指向父节点parent,然后指向左子节点left,还有指向右子节点的right,然后还有表示颜色red/black,这个就是TreeNode的基本结构
红黑树的插入操作:首先是找到一个合适的插入点,就是找到插入节点的父节点,然后这个红黑树它又满足二叉树的所有排序特性…(满足二叉排序树的所有特性),这个找父节点的操作和二叉树是完全一致的。二叉查找树,左子节点小于当前节点,右子节点大于当前节点,然后每一次向下查找一层就可以排除掉一半的数据,插入效率在log(N)
查找的过程也是分情况的,第一种情况就是一直向下探测,直到查询到左子树或者右子树为null,说明整个树中,它没有发现node.key与当前put key 一致的这个TreeNode。此时探测节点就是插入父节点所在了;然后是当前插入节点插入到父节点的左子树或者右子树,就完事了,然后,根据这个插入节点的hash值和父节点的hash值大小决定左右的,插入会打码平衡,还需要一个红黑树的平衡算法。
第二种情况就是根节点向下探测过程中,发现这个TreeNode.key 与当前 put.key 完全一致。说明它也是一次repleace操作。

红黑树那几个原则,你还记得么?

  • 1、红链接均为左链接
  • 2、没有任何一个结点同时和两条红链接相连
  • 3、该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同

左旋和右旋,你知道?

jdk8 HashMap为什么要引入红黑树呢?

其实主要就是解决hash冲突导致链化严重的问题,如果链表过长,查找时间复杂度为O(n),效率变慢。本身散列表最理想的查询效率为O(1),但是链化特别严重,就会导致查询退化为O(n)。严重影响查询性能了,为了解决这个问题,JDK1.8它才引入的红黑树。红黑树其实就是一颗特殊的二叉排序树,这个时间复杂度是log(N)

那为什么链化之后性能就变低了呀?

因为链表它毕竟不是数组,它从内存角度来看,它没有连续着。如果我们要往后查询的话,要查询的数据它在链表末尾,那只能从链表一个节点一个节点Next跳跃过去,非常耗费性能。

再聊聊hashmap的扩容机制吧?你说一下,什么情况下会触发这个扩容呢?

在写数据之后会触发扩容,可能会触发扩容。hashmap结构内,我记得有个记录当前数据量的字段,这个数据量字段达到扩容阈值的话,下一个写入的对象是在列表才会触发扩容

扩容后会扩容多大呢?这块算法是咋样的呢?

因为table 数组长度必须是2的次方数嘛,扩容其实,每次都是按照上一次的tableSize位移运算得到的。就是做一次左移1位运算,假设当前tableSize是16的话,16 << 1 == 32

我先打断下,这里为什么要采用位移运算呢?咋不直接tableSize乘以2呢?

主要是因为性能,因为cpu毕竟它不支持乘法运算,所有乘法运算它最终都是在指令层面转化为加法实现的。效率很低,如果用位运算的话对cpu来说就非常简洁高效

创建新的扩容数组,老数组中的这个数据怎么迁移呢?

迁移其实就是,每个桶位推进迁移,就是一个桶位一个桶位的处理;主要还是看当前处理桶位的数据状态吧

跟我聊下ConcurrentHashMap

首先它的数据结构在JDK1.7 版本底层是个分片数组
在这里插入图片描述

为了保证线程安全它有个Segment分片锁,这个Segment继承于ReentrantLock,来保证它的线程安全的,它每次只能一段加速来保证它的并发度。
在JDK1.8版本,它改成了与HashMap一样的数据结构,
在这里插入图片描述
数组 + 单链表 或者 红黑树的数据结构,在1.8它逐渐放弃这种Segment分片锁机制,而使用Synchronized和CAS来操作。因为在1.6版本的时候JVM对Synchronized的优化非常大。现在也是用这种方法保证它的线程安全。

刚刚你提到1.7版之前Segement分段锁,那你知道它如果要找到具体的值,它会经过几次哈希吗?或者它是如何去找到一个具体数值的?

刚说1.8版本之后它是CAS加Synchronized,那你说下CAS是什么?CAS相当于一个轻量级的加锁的过程

在这里插入图片描述
CAS 相当于一个轻量级的加锁的过程,比如说要修改一个变量,但在并发量不是特别大的情况下,锁竞争不激烈,你要修改东西,先查,再写之前,它会再查一次,比较之前的结果有没有区别,若有区别说明这个修改是不安全的,如果没有区别,这时它可以安全的去修改。而不是直接加锁的那种形式。在低并发的情况性能会好一点的。
CAS: Compare And Swap。比较并替换

CAS 优缺点?

优点:在低并发时,性能会更好,吞吐量也会更高
缺点:
引发ABA问题;之前读和再过段时间读中间会被三人修改过,但是它又给改回来了,这个问题通过添加一个时间戳或版本号来解决
只能对一个变量进行原子操作,多个变量则不行;
并发高时,CPU轮询,对CPU性消耗还是比较大的。
当高并发的时候,建议直接使用锁、Synchronized之类的。

刚你提到了 Synchronized,再跟我聊下它

在这里插入图片描述Synchronized可以用在比如同步代码块,还有它同步代码块时是可以指定任意的锁。对象作为锁,当它在应用于方法上的时候,它的锁定就是锁this。如果是用静态方法则锁定是它的class对象。
关于Synchronized 在JDK1.6 版本升级还是蛮大的。可以从无锁状态、偏向锁、轻量级锁(自旋锁),最后到重量级锁。偏向锁,它就偏向于获得第一个锁的线程,
在这里插入图片描述
然后,它会将线程拉到这个锁对象头当中,当其它线程来的时候,它可能就会立刻结束这个偏状态。进而跑到一个轻量级锁。轻量级锁也是在低并发情况下,来消除锁的源于在这里插入图片描述
它主要是在虚拟机栈中开辟一个空间,叫Lock Record,将锁对象写入 Mark Word。再将尝试将另一个Lock Record的指针,使用CAS去修改锁对象头的那个区域来完成一个加锁的过程,它也是普遍应用于一个低并发的情况,再往上如果锁竞争非常激烈那就会立刻膨胀为一个重量级锁。
在这里插入图片描述
重量级锁用的是一个互斥锁的过程,像它的加锁过程,锁它的主要实现原理是我先说下同步代码块会比较好,会在你的代码块前后加上两个指令,一个是mointerenter, 另外一个是mointerexit,一个线程来时,它发现这个对象图中它的锁标志位是无锁,应该是01的状态,它会尝试给一个互斥锁对象,锁对象的时候会跟另外一个对象关联,就是监视器锁monitor,它会在moniter的一个锁定器加1,并且将moniter的指针写入到一个对象头中表示,并且修改它的锁对象标志位为10,就是它重量级锁的一个标志位。那以此完成换锁的过程。并且它的这个过程是锁可重入的,因为它不会每次出去不需要再加锁还要再释放锁。它每次进来后获取这个锁,让锁记录加1即可。它加锁完成之后,当其它线程来的时候,它会检查到这个锁对象头中monitor监视器锁上计数器不为0,它会在monitor监视状态下等待去竞争这个锁。如果之前结束了操作。它就退出开始释放这锁。并且逐步的将加上的锁定释放几次。将计数器清零来完成对锁的一个释放。让其它线程继续去竞争这个锁。这是它重量级锁同步代码块的一个原理。

接下来说下同步方法吧

它不是这种指令,而是一个ACC_SYNCHRONIZED标记位,相当于一个flag,当JVM检测到这样一个flag,它自动去走一个同步方法调用的策略,这个原理是比较简单的。

何时用它、何时用ReentrantLock,有考虑么?

它们俩对比的话,区别还是蛮大的。
从JVM层面上Synchronized是JVM的一个关键字
ReentrantLock其实是一个类,需要手动去编码
像Synchronized使用时候是比较简单的,我直接同步代码块或者直接同步方法。我也不需要关心锁的释放。但是ReentrantLock,我需要手动去Lock,然后配合try finally代码块一定要去把它的锁给释放。
另外ReentrantLock相比Synchronized有几个高级特性:
它提供了一个如果有一个线程长期等待不到一个锁的时候,为了防止死锁,你可以手动的去调用一个lockInterruptibly方法尝试让它去释放这个锁。释放自己的资源不去等待。ReentrantLock提供了一个可以构建公平锁的方式,它的构造函数有一个但是不推荐使用。它会让ReentrantLock等级下降。此外,它提供一个condition你可以指定去唤醒绑定到condition身上的线程。来实现选择性通知的一个机制。这是一个它们之间的区别,关于它的选择性,如果你不需要我说的ReentrantLock以上三种特性,我还是推荐还是一定要使用Synchronized,因为相比来说Synchronized的话,它是JVM层面的关键字,在优化后它会非常方便了解当前的锁被哪些线程所持有。这些状态的话不是ReentrantLock能相比的。还是使用Synchronized比较好些。

Synchronized锁升级不可逆的?

比如滴滴、饿了么,在早上的时候所有的代码全部升级为重量级的锁了,中午或者说是平时我们打车的时候,哪怕是已经过了高峰了,但是你的锁一直是一个重量级的锁。所以在并发情况下,其实很多时候锁升级后,不可逆。如果确保你的QPS很稳定的情况下,或者是很低的情况下,其实还是要根据你的场景去做选择的

ReentrantLock比如像它里面是公平锁、非公平锁是怎么实现的?

在这里插入图片描述

那像它的底层AQS,可能你也不是很了解对吧?

那JUC下面你还用过哪些包?

CountDownLatch、CyclicBarrier、还有Semaphore

volatile知道么?

volatile 是JVM提供最轻量级的一个关键字
说到volatile,首先要说到我们计算机的模型。CPU和内存之间的线程效率是差好多数量级的。但为了保证它们之间的计算,不影响CPU的计算,然后中间有好多LLV那种缓存,我们线程在这个缓存中去工作,首先它取数据会从主内存取到工作内存中。在工作内存中计算完之后再传回主内存,这时候就有一个问题,多个线程之间的可见性。是如何保证的,在计算机层面上是有好多协议,在JVM它为了解决这些比较复杂的东西,它提供了像JMM的这种模型
在这里插入图片描述
被volatile修饰的变量,它就可以保证这个变量在所有线程间的可见性,在这个线程使用这个变量前,在修改这个变量之后,它可以立刻刷到主内存。它在使用时会立刻从主内存中取出来刷新那个值。volatile它是不能保证变量的原子性,像自增自减这种操作它是不能保证的。
俗称:总线嗅探机制
使用方式: volatile + 修饰变量
保证多线程可见性;
禁止指令重排序;
但它不保证原子性;
在这里插入图片描述

那原子性我们怎么去保证呢?

使用JUC下面的Atomic原子类,比如AtomicInteger;底层采用CAS轻量级锁
加锁,比如ReentrantLock、Synchronized;重量级锁

你知道不断地去总线嗅探会有什么问题吗?

会导致它的总线占用的资源就很大,就是总线风暴

多线程的线程池了解吗?

了解

那你跟我说一下线程池的运行机制

在这里插入图片描述
线程池它是有一个核心线程数,当你的线程运行的时候,如果你没有设置成预启动加载,线程数为0,当你提交一个新任务的时候,它会首先是建立一个核心线程;
在这里插入图片描述
它之前的有没有执行完那么你会一直建核心线程。
当达到最大核心线程数时,如果还都在忙,那么就会放到BlockingQueue里面作为节点,如果BlockingQueue队列也放满了,而且核心线程都在忙。那就会去建立新线程,它叫做非核心线程。若一直创建,数量达到非核心线程数max access。就会触发一个拒绝策略,JDK内置了四种拒绝策略。
第一种是AbortPolicy,直接抛出异常来解决
第二种是DiscardPolicy,悄无声息丢弃你的这个任务
第三种是DiscardOldestPolicy,丢弃你最早的未执行的任务
最后一种是CallerRunsPolicy,谁调用我的这个线程去执行你的这个任务
它这种方式,是会影响你的新任务提交速度。
关于使用的队列,它是阻塞队列。JDK提供了两种,xxxx,它是不保存任务的那种,JDK提供的new catch线程池是使用的这种队列。
第二种的话就是一个有界队列,ArrayBlockingQueue,你指定数量,如果超了,它可能会OOM的。
第三种是无界队列LinkedBlockingQueue。它可能因超出上下文而OOM的
关于线程池,还有几个比较重要的参数,线程构造,在创建一个线程池的时候。你要提供一个线程的threadFactory,一定要指定它名称,这是很重要的一点。你也可以设置它为守护线程,当你BM关闭的时候,可以让线程跟着它一块消亡。

像ArrayBlockingQueue,LinkedBlockingQueue这些队列的底层,你有去看过么?

AQS

跟我讲一个你平时的SQL调优思路么?

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

首先,有条最基本的,表要有主键。
因为有主键的表MySQL会创建聚族索引,聚族索引的好处是它的主键和数据行是在一行的。你在explain查询语句的时候会发现,它的type级别是const,这是很高的一种级别。然后当有主键的时候,如果一条SQL语句很慢,可以去查看它是否建立了相应的索引。
建立索引要尽量选择where条件后面的字段或者是group by 或者是 join连接的字段,作为你的索引列,这些索引列要排序,要符合最左匹配原则。另外你选的时候要根据它们的索引选择器,你的非重复的数据行和重复的数据行中间的排列,大的放左小的放右这样的形式。这种多列的索引列,你要去建立联合索引而非单个索引,这是索引的选择。
另外,你在SQL书写的时候,不要将索引列放在一个表达式中,或者说你用了一些反向判断,比如not null,不等于、not in等这种关键词。它会让你索引失效。
还有一点,如果你的数据查询非常频繁,你可以考虑给它使用覆盖索引,覆盖索引是可以直接在索引中查询到数据的,相对来说还是很快的。
如果你的索引没有失效,你也可以考虑是不是MySQL因为一些其它的原因造成的。因为MySQL的底层,它有一个随机采样,它会根据你的索引基数。但不可能全都给你标记上,但它会根据随机采样来计算你索引基数,如果它采错了,它就认为虽然你的索引选择性比较大,但它认为你的索引选择性比较小。就可能不走你的索引,可以通过force index强制让它走索引然后看它行不行,你需要刷新下它的信息,要用analyze TABLES,看它有没有再重新组队。关于数据库调优现在能想到的就这些。

在创建索引时,创建唯一索引还是普通索引好点?

聚族索引,它是确切来说应该是一种数据结构。它是将它的主键和它的数据行放在一块的。
聚族索引是通向真实数据行的有利途径。
而非聚族索引就是我们说的普通索引,它的索引存放在叶子节点和它的主键。还有它的索引列,在查询的时候,其实是通过它的索引列查询到它的主键。再通过主键去回表查询。还是通过一个聚族索引查的。相对来说是慢一点
还是看你选择吧,比如你要是对主键进行查询的话,那么你只要建立主键。那直接聚族索引就完事了,如果那些不同字段,你想去查的话,它的查询速度可能比较慢。你需要创建一个非聚族索引来加快它的查询速度

  • 因为唯一索引,它会去确保列的唯一,所以会多一次判断的过程。但它判断过程是开销是很小的,真正的开销的话是在它的buffer区的

我们聊下MVCC和事务隔离级别的关系吧

mvcc能解决部分幻读,不能完全解决幻读问题
事务它本身主要还是靠 MVCC进行保证的。
首先,InnoDB的特性就是它支持事务,支持事务为了保证事务的并发度,它提供了一个隔离级别
它有四种隔离级别
一种是Read Uncommitted,结果什么都不能保证
一种是Read Committed 它为了解决事务之间的脏读,但它对于你的不可重复读还有幻读,它者是不能保证的。
下一隔离级别是MySQL默认是Repeatable Read,这个隔离级别可以保证不发生脏读,还有不可重复读,但它是不能保证幻读的
最后一个是Serializable,串行化的。所有的都是串行来执行的。它是没有事务的,它是最高的安全性级别
如果需要解决幻读,需要手动去操作。因为我们知道select语句,它是没有不加锁的过程,但是你可以在后面加上,当你是索引范围查询的时候,在后面加上for update锁住你未出现的那个行。保证你的事务中不读到其它事务中的提交数据行。另外一点for update 是锁住了未出现的那行。它用的是一个行级锁。
关于事务另外一个问题是丢失更新。丢失更新,我们会需要手动去处理一下。
两种方式:一种是乐观锁,另一个是悲观锁的形式,当乐观锁的时候,你可以使用类似于之前并发的CAS,可以加一个版本号,是先读,读完之后你再改,改的时候往回写再读一下,跟之前的版本号进行比较。如果一致的话则安全写回
还有一种情况,你可以加个悲观锁。可以在等人查询的时候,带上一个for update 给那行上个锁,来防止丢失更新
四种隔离级别,有两种是完全靠MVCC多版本并发控制去保证的。

JVM内存模型

JVM内存模型可以分为两大类,一个是它线程私有区:线程栈、本地方法栈、程序计数器;一个是线程共享区:堆、方法区(jdk7叫方法区,jdk8叫元空间)

程序计数器,是占用内存比较小的地方,但是唯一一块不会OOM的区域。这里会告诉你的执行的代码归于哪一行。
虚拟机栈主要使用的是栈帧。一个方法的调用就是从入栈到出栈的过程。

方法区(元空间)存放在直接内存,它其实只是JVM的一个规范,但是它在1.7版本的时候,它的实现在HotSpot虚拟机中叫做永久代,存放的是一些常量池,常量、还有类的元数据信息。
1.8版本的时候,因为一些原因,它转移到了一个集结内存中,它叫做元空间。这存放的是类的元数据信息,这里的方法区也会是OOM的。

还有一个区域是堆,它里面存放是java里产生的对象、对象的实例。它也是GC重点回收的一个区域。并且它也是会产生OOM的。

最后要关注的是运行常量池,它是在1.8时将运行时常量池转移到堆中。因为之前它是在元空间里面,后来搬移到堆里面了。堆里面存放的是常量池被加载之后,运行时常量池和静态变量都存储到了堆中。

关于直接内存,它现在存放的是JVM的元空间,元空间放在里面的时候它有一个好处。oom的机率相对之前变小很多了。

你刚刚多次提到OOM,你知道一个OOM的排查思路和过程么?

可以通过jps/jmap/jstack/jvisualm工具查看

你怎么判断那个对象是否可达呢?

两种方式:
引用计数:但它的缺点是可能会产生循环依赖
GCRoot 根可达性分分算法:从GCRoot根结点作为起来,在链上是否可以找到,如果找不到判断可回收了。
可以作为GCRoot的对象有:
虚拟机栈中引用的一些对象;像是方法区中的静态变量所引用的对象;还有常量所引用的对象;还有方法区中所引用的一些对象;

那它现在有哪些垃圾回收算法和垃圾回收器?

垃圾回收算法有四个

  • 标记-清除:比如说将判定为死亡的对象然后依次抹掉,会产生很多内存碎片,并且当它对象是比较大的时候,它的标记效率是比较低的
  • 复制算法:就是粗暴地将我们的堆划分成两块,在GC的时候将一些活动对象直接复制到另一半,它的缺点是内存的使用大小减半,好处是不会产生内存碎片
  • 标记-整理:也是每次使用一块区域,将一些还存活的对象,往另一端移动、复制然后就可以找出剩下的一块区域。
  • 分代垃圾回收

说一下垃圾回收器吧

像JDK默认使用Serial Parallel
CMS是可以和用户线程并发操作的,GC线程与用户线程并行执行的一款垃圾回收
它的特点就是低延迟,整个过程主要用是来回收老年代的,它的算法使用的是标记清除;可能还混合一下标记整理,它会容忍一定的垃圾碎片,当达到这个阈值时,触发一次标记整理来清理一下。它的过程有四个阶段:初始标记+并发标记+重新标记+筛选回收
但它在初始标记和重新标记的时候,它不是线程并行,它会短暂的产生Stop The World,且这个时间是很短的,它的并发标记和清理这两个阶段是耗时比较长的。但它是和用户线程一起并发执行的。所以说它可以最短的实现低延迟

可以说下他们CMS、G1两个Stop the world的区别么?

G1 将内存分为很多块大小相等的区域Region,

比如我的CPU突段飙高了,我应该怎么去排查呢?

cpu飙高,一般是我们线程在不停地运行程序。
如果你的服务部署到Linux上,你需要跑命令去查看下,你可以看下当前CPU百分比的进程ID
然后你可以用jstack命令,加上进程id来查看。

你平时是怎么学习新技术的?

新技术有人写博客,去做引导或者更潮流些会有人做些视频。看博客、视频会用之后,我就会找一些专业的书籍,因为书籍它的语言是经过千锤百炼的,表达的更精确一些。去多些一些书籍巩固下,了解它的底层。到时候再回过头来看,感觉会不一样。你可能更深入的去了解一些源码之类的。

最近的话,看什么书呀

最近的话是看过的是JVM

差不多了,你有什么想问我的么?

你们大厂里面的开发是怎样的一个模式

猜你喜欢

转载自blog.csdn.net/weixin_32265569/article/details/108481327