叫练

叫练 查看完整档案

南京编辑南京师范大学  |  计算机科学与技术 编辑技术部  |  技术leader 编辑 118.123.6.69:15906/game/ 编辑
编辑

持续技术输出

个人动态

叫练 发布了文章 · 4月1日

CMS前世今生

CMS一直是面试中的常考点,今天我们用通俗易懂的语言简单介绍下。

垃圾回收器为什么要分区分代?


image.png

如上图:JVM虚拟机将堆内存区域分代了,先生代是朝生夕死的区域,老年代是老不死的区域,不同的年代对象有不同特性,因此需要不同的垃圾收集器去处理。如下图,黑竖线左边的区域都是分代垃圾收集器,G1之后内存就不分代了。

image.png

单线程垃圾收集器:Serial + Serial Old


Serial(SY),Serial Old(SO)是单线程垃圾收集器组合,垃圾收集线程是单线程的,随着现代内存区域越来越大,SY+SO组合已经越来越少了。垃圾收集的单线程需要STW时间无疑越长。这种组合比较合适较早JDK版本。如下图,用户线程表示应用程序处理过程,垃圾收集线程表示垃圾线程清理垃圾过程,此阶段应用程序是需要等待垃圾线程STW的。

image.png

多线程垃圾收集器:PS+PO


前面我们说了,单线程垃圾收集器缺点就是当内存区域变大,收集效率会很低,那OK,摇身一变,如下图,多线程垃圾处理器。

image.png

值得注意的是:PS+PO组合是JDK1.7,JDK1.8默认垃圾收集器。通过java-XX:+PrintCommandLineFlags命令可以在Dos界面查看。如下图,该命令可以查看JVM初始化的默认参数。比如:-XX:InitialHeapSize表示初始化堆大小。

image.png

为啥蹦出来个CMS+ParNew


并行处理有了,CMS+ParNew又是干嘛的?其实PO关注是吞吐量,而CMS关注是缩短STW时间。而CMS处理流程更复杂,至于ParNew,其实约等于PS,如果你注意最上面一个图,你会发现PS年轻代无法和CMS组合。所以就多出来了一个ParNew。

介绍CMS阶段


CMS,全名称Concurrent Mark Sweep,中文释义并发标记清除,从名字上可以看出算法思想使用标记清除算法,下面我们看看CMS简化处理流程。

image.png

  • 初始标记。只标记GC root可达的第一个节点。会短暂的STW。
  • 并发标记。用户线程和垃圾线程同时进行。垃圾线程会继续向下寻找GCroot,不会有STW。但也会有两个问题。
  • 多标:之前不是垃圾,现在线程出栈引用断开了变成了垃圾。也称为浮动垃圾。
  • 错标:之前已经被标注是垃圾,但现在重新引用。
  • 重新标记。STW时间一般低于200毫秒。
  • 并发清除。并发清除时,因为用户线程和垃圾线程一起工作,如果CMS线程异常,可能会触发SO单线程执行。程序可能会特别缓慢。

劣势:碎片严重。

总结


主要简单介绍了分代垃圾回收器,特别介绍了cms执行过程,G1留下次再说吧。好了,文章有地方还写的不清晰希望亲们加以指正和点评,喜欢的请点赞加关注哦。点关注,不迷路,我是叫练,边叫边练,公众号叫练】,微信号【jiaolian123abc】。祝大家生活愉快。

查看原文

赞 0 收藏 0 评论 0

叫练 发布了文章 · 3月22日

ThreadLocal 慌不慌?

现在稍微大点的公司面试,可能会问到ThreadLocal源码实现,不过在介绍它之前,我们先介绍JVM中引用的概念。所谓这些概念就是我所说的基础了。引用强弱关系到内存垃圾回收时机,用好引用可以减轻内存压力。JVM引用一共分为4种,分别是强引用,软引用,弱引用和虚引用。

JVM引用


  • 强引用:

image.png

如上图:根引用list指向堆,一直向list添加512K的字节数组,程序几秒后会出现溢出,代码中list引用称为强引用。强引用内存一直不会被释放,直到内存溢出。

image.png

  • 软引用:

image.png

image.png

如上图代码:主函数中,软引用需要用SoftReference包装user对象,运行程序先配置初始化参数,-Xmx10M 设置堆空间为10m,设置user=null,user引用消失,主函数第一次调用GC时,控制台输出User对象信息,说明堆中User对象还存在,user对象并没有被回收,这是因为软引用softRef指向堆中User 对象,申请内存byte[] bytes = new byte[10249856];bytes 约6M,bytes引用对象会进入老年代,此时老年代会GC,此时控制台会输出null,说明此时userRef失去引用,堆中的user对象都被当成垃圾回收了。为什么要设置bytes 约6M,是因为想测试一个结论:当内存不足时,GC会回收软引用

特别注意:调试时,985设置bytes引用执行堆内存的大小,需要自己调试,才可能出现上述结果。

  • 弱引用

image.png

如上图代码:弱引用对象需要用WeakReference包装,GC后,弱引用对象被回收。总结一句:当GC时,不管内存够不够,弱引用会被回收。我们的ThreadLocal就是被WeakRefernece包装。

  • 虚引用:若有若无引用。适用于堆之外的内存。忽略了。

image.png

小结下:强引用,软引用,弱引用,虚引用的生命力是从高往低的。生命力越低越容易被回收,强引用则无法被回收,软引用比较适用于缓存的场景,软引用只有内存紧张时才会被回收,弱引用只要发生GC会被回收

ThreadLocal解析


ThreadLocal是线程安全的,因为它能让每个线程都拥有自己独享变量。它也可以让一个线程拥有多个变量。底层使用hash表实现的数组,说白了就是一个HashMap,其中key是ThreadLocal,value就是值。使用起来很方便。

image.png

如上图代码:创建一个ThreadLocal,当调用set时,主线程就拥有了自己的私有变量“叫练”了,通过get就可以取出来。但这里有个问题,源码中ThreadLocal是被WeakReference包装的。为什么要这么做!这样做的目的是为了节约内存,下面我们详细了解下!

image.png

我问自己下面几个问题:

  1. ThreadLocal会自动回收吗?

不会,当线程结束,ThreadLocal才有可能回收,注意是有可能,因为还有其他的线程引用了当前ThreadLocal。

  1. ThreadLocal设置为弱引用的目的是什么?

防止内存泄漏,为了回收内存。

  1. 为什么不将整个Entry设置成弱引用?

因为Entry中的value可能是一个对象,而这个对象可能被其他线程引用,一旦设置Entry为WeakReference,可能导致其他线程空指针。

  1. 正确使用ThreadLocal姿势?

每次使用完ThreadLocal之后,需要调用remove方法,清除当前线程的threadLocal。

image.png

总结


学习不是一蹴而就的,大家看如果你不去了解JVM引用,你就无法搞清楚ThreadLocal源码。好了,文章有地方还写的不清晰希望亲们加以指正和点评,喜欢的请点赞加关注哦。点关注,不迷路,我是叫练,边叫边练,公众号叫练】,微信号【jiaolian123abc】

查看原文

赞 0 收藏 0 评论 0

叫练 发布了文章 · 3月18日

图解垃圾算法,No,捡垃圾算法

对象生与死


今天不是给大家介绍对象的,给大家介绍下垃圾,因为垃圾会霸占内存,需清理之,今天我们聊聊JVM用什么方式回收垃圾的!先上图吧,我们看看对象的生命周期。

image.png

先解释几个名词:

  • 新生代:快速生长,存放年纪比较小的对象。
  • 老生代:存放年纪比较大的对象。
  • Surviror:回收新生代内存后容纳其余存活的对象,分为From区和to内存区。

新生的对象都在eden区,当eden区满时容纳不了大的对象,会触发GC,如果对象还活着,小对象进入From区或者是to区,这两块区域有一块是空的,假设现在装对象是from区,那么,当GC后from区所有对象会复制到to区,并且清空from区域,存活对象年纪会增大一岁,当对象到达一定年纪之后,就会进入老年代了。如果对象比较大,Surviror装不下,会直接进入老年代,如果老年代也装不下,会报错:堆内存溢出

简单介绍垃圾生命周期后,我们看看垃圾清理算法。

引用计数法


引用计数法怎么判断一个对象是垃圾?就看是否有引用指向该对象。引用计数法表示如果一个对象有1个引用计数器就是1,2个引用计算器是2,如果没有引用,计数器为0,也就是垃圾对象。缺点是对象相互引用,对象无法回收。画图举个简单案例。

image.png

如上图代码:teacher持自身引用同时持有student的引用,计数器为2,student持自身引用同时持有teacher的引用,计数器为2,这叫相互引用,最终导致teacher或者student对象都无法被回收。所以现在垃圾回收器一般不采用引用计数法。

标记-清除法


标记-清除分两步,GC线程先标记可达对象,当发生GC时,清除不可达对象。缺点是回收后内存碎片。image.png

如上图,我们知道分配内存都是连续的,垃圾对象回收后,内存很不规则,不利于内存使用效率。垃圾对象是不可达的?那什么叫可达对象呢,什么叫不可达对象呢?

  • 可达对象:从根引用搜索,能达到的对象叫可达对象,如绿色存活对象叫可达对象。如果从根引用搜索,
  • 不可达对象:不能达到的对象叫不可达对象。如黄色部分,就是垃圾对象,特别注意:此黄非彼黄。
  • 引用:也叫GC root,存放在栈中,指向堆的引用,一般用参数或者局部变量表示。

理论性的东西还是比较难理解,我们画图表示下。

image.png

假设:new Object()对应的堆地址是0xJL。

Object object = new Object(); 栈object引用指向new Object()对应的0xJL地址,new Object()对象可达,其中object就叫做根对象

object = null; 告诉gc线程,没有引用指向0xJL了,那这块内存就有可能被标记为垃圾对象。

复制算法


复制算法需要将一块空白的内存一分为二,GC后,将可达对象全部移动到另一块内存。新生代对象朝生夕死,GC后将活着的对象移动到另一块空内存,并将当前使用的内存清空。每次GC,循环往复。

image.png

如上图:新生代采用复制算法,GC回收前使用from区域,GC后使用to内存区域。

标记整理法


标记整理算基于标记清除算法做了一定优化,gc线程首先从根节点开始标记可达对象,并将可达对象压缩到内存顶部,最后清除边界区域,老年代对象生命周期长,比较适用于标记整理算法。

image.png

如上图:当老年代满了,会触发Full GC,将内存压缩整理。

总结


画图解释了几种GC算法的含义和缺点,希望对你有帮助,喜欢的请点赞加关注哦。点关注,不迷路,我是【叫练公众号,微信号【jiaolian123abc】边叫边练。

查看原文

赞 0 收藏 0 评论 0

叫练 发布了文章 · 3月15日

叫练手把手教你读JVM之GC信息

案例


众所周知,GC主要回收的是堆内存,堆内存中包含年轻代和老年代,年轻代分为Eden和Surivor,如下图所示。我们用案例分析下堆的GC信息【版本:HotSpot JDK1.8】。

image.png

/**
 * @author :jiaolian
 * @date :Created in 2021-03-15 15:02
 * @description:新生代内存测试
 * @modified By:
 * 公众号:叫练
 */
public class NewGenTest {
    public static void main(String[] args) {
        //每次在Eden申请1M空间
        byte[] bytes = null;
        for (int i=0; i<5; i++) {
            bytes = new byte[1024*1024];
        }
    }
}

案例很简单,for循环运行5次,每次在Eden申请1M空间,假设我们分配堆内存空间是20m,并打印GC详细信息,配置过程:-XX:+PrintGCDetails -Xmx20m。

  • -XX:+PrintGCDetails:打印GC详细信息。
  • -Xmx20m:分配最大堆内存空间是20m。

GC详细分析


运行程序,IDEA控制台打印结果如下:

  1. 第一句话:年轻代GC了一次,因为第五次循环,Eden满了,年轻代内存约6M(6144K),Eden回收前约5M(5057K),回收后是489K,年轻代内存回收前约是5M,回收后约是2M(1783K),总堆内存大小约是20M(19968K),GC耗时0.0016002 secs。
  2. 第二句话:年轻代总内存约6M,使用约5M。
  3. 第三句话:eden空间总内存约5M(5632K),使用率是94%。
  4. 第四句话:from是512K,使用率是95%。
  5. 第五句话:老年代总空间约14M(13824K),使用了1294K,这个使用空间是因为程序在Eden申请1M空间,判断空间不够,就申请from或者to空间,发现只有512K,就触发monitor GC,将1M内存申请在老年代。
  6. 第六句话:元空间内存。

image.png

  • -Xms 初始堆大小,不够时,会自动扩展,所以一般Xms空间和Xmx最大堆空间设置成一样的。

上面程序不变,设置JVM参数,-XX:+PrintGCDetails -Xmx20m -Xms5m,运行程序,部分结果如下图所示。

image.png

如上图所示:初始化堆大小是5M,新生代内存一共发生了4次GC,从上图可以分析,Eden只有1M多内存可以被申请,所以第二次for循环申请1M空间就触发了GC,数据就被丢进老年代,连续3次后,GC堆的空间由5M变为了6M,说明初始化堆空间不够使,可以自动扩展堆内存。

当然我们还可以通过-Xmn 设置年轻代大小。下面我们看看年轻代中,Eden和from/to区域怎么划分。

上面程序不变,设置JVM参数,-XX:+PrintGCDetails -Xmx20m-Xmn10m-XX:SurvivorRatio=2,运行程序,部分结果如下图所示。

  • -Xmn10m:设置新生代内存为10m
  • -XX:SurvivorRatio=2:设置新生代中eden和Survivor比例是2:1

image.png

我们设置新生代是10M,这里显示新生代大小是7.5M(7680K),实际上from/to是有一块空间是每次GC做交换的区域(方便垃圾回收),所以实际上7680K=5120+2560。5120/2560=2,也就是新生代和Survivor空间比例。

另外还有:-XX:NewRatio:设置老年代和新生代比例,一般是1/3)。比如设置-XX:NewRatio=2-XX:+PrintGCDetails -Xmx30m

那么老年代空间是20M,新生代空间是10M。

总结


最后总结下:

  • -XX:+PrintGCDetails:打印GC详细信息。
  • -Xmx20m:分配最大堆内存空间是20m。
  • -Xmn10m:设置新生代内存为10m
  • -XX:SurvivorRatio=2:设置新生代中eden和Survivor比例。
  • -XX:NewRatio:设置老年代和新生代比例,一般是1/3)。

今天学习了JVM之GC信息参数配置,写的不全同时还有许多需要修正的地方,希望亲们加以指正和点评,喜欢的请点赞加关注哦。点关注,不迷路,我是【叫练公众号,微信号【jiaolian123abc】边叫边练。

查看原文

赞 0 收藏 0 评论 0

叫练 发布了文章 · 3月12日

原来我还有网络天赋

问题


如下图,之前公司有10多台服务器,都设置成了静态IP,因为现在更换成了类似IP为192.168.1.X 的1网段,看着下面的服务器,修改IP简单,但想想服务器里面还有许多配置需要随着IP一起修改加测试,想想头大还是算了。咋办?也不能耽误大家工作太久啊,于是在原有的路由器上增加虚拟网段(60段),分配IP来减轻原来服务器修改IP工作!

image.png

公司没有网络管理员,于是我硬着头发干了!说实话,没有抱怨,我是朝学习态度去的。

tempimage1615539002415.gif

思考


梳理下公司网络拓扑图,大致简图如下:

image.png

  • 价格:我们公司是独享的IP,单独拉网线,速度会好一点,因为比共享IP成本要高,所以套餐单价会高点,像我们公司,移动100M一年价格是1200,我家是移动100M,一个月是10块钱,一年是120,你对比下就知道了。
  • 外网接入:一般手机信号都是通过基站来发送和接收的,公司的宽带也是一样的,找附近最近的基站接入到办公大楼,光猫盒负责和基站信号对接,这个光猫盒是办理宽带时运营商送的,猫上面有一根网线直接连核心路由器的WAN口,所有的客户端,如电脑,手机,Wifi路由,服务器都是通过该核心路由器转发数据。
  • 局域网接入:公司在装修时就会把网线埋在地上或者顺着墙体,到公司的各个办公室和办公场地,都是通过核心路由器/交换机分发的。大家可能用的最多的就是无线路由器了,手机一般连接无线路由器WIFI,公司WiFi有多种方式可以接入外网,比如中继,好理解就是一个路由器连接另外一个路由器通过密码或者网线的方式,为什么要中继,因为Wifi信号是有距离的,隔的太远就连不上信号了,还有最常见的就是网线直接连路由器的WAN口,然后可以通过无线信号或者Lan口连接终端设备了,如果有多个路由器需要通过网线连接,一般是这个路由器的LAN口连接另外一个路由器的WAN口。这样就形成了一个巨大的环形网络了。我所在的公司就是这样的。

公司用的核心路由器是H3C,登上去看了下知道网络设置是在LAN这块配置,但不知道无从下手,于是查了写资料了解。通俗解释如下。

image.png

  • WAN:负责连接外部网络,比如光猫的网口一定要连接核心路由器的WAN口。
  • LAN:负责连接内部网络(局域网),所有的客户端都是通过LAN口交换数据。
  • VLAN:虚拟局域网,比如现在网关是192.168.1.1,现在可以新增IP,192.168.60.1,这个虚拟的IP直接执行192.168.1.1,我理解就是一个地址映射。
  • DHCP:简单理解:分配IP地址的协议。具体分什么IP呢?比如60.100-60.200这段IP,我现在需要使用,那现在新增地址池,并且地址池需要指向具体LAN口。这点非常重要,否则不能实现1网段和60网段互联。

image.png

我画个草图,方便大家理解。

image.png

我最后配置出来效果大致如上图,如果大家想在网络上分不同网段管理机器,通过LAN口来映射VLAN虚拟IP,为什么要虚拟IP?因为大家知道动态IP需要网关,打个比方192.168.60.100的IP地址,网关你不能设置成192.168.60.1。大功告成。

分享


今天当了一次网管,给大家分享了一些网络概念,整理出来希望能对你有帮助,喜欢的请点赞加关注哦。点关注,不迷路,我是【叫练公众号,微信号【jiaolian123abc】边叫边练。

查看原文

赞 0 收藏 0 评论 0

叫练 发布了文章 · 3月11日

最简单的JVM内存结构图

JVM内存结构图


image.png

大家好,好几天没有更新了,今天的内容有点多,我们详细介绍下JVM内部结构图,还是和之前一样,案例先行,方便大家理解记忆。

/**
 * @author :jiaolian
 * @date :Created in 2021-03-10 21:28
 * @description:helloworld测试jvm内存区域
 * @modified By:
 * 公众号:叫练
 */
public class HelloWorldTest {

    public static void main(String[] args) {
        //新建HelloWorldTest对象;
        HelloWorldTest helloWorldTest = new HelloWorldTest();
        //新建2个线程调用sayHello
        for (int i=0; i<2; i++) {
            new Thread(()->helloWorldTest.sayHello("world")).start();
        }
    }

    /**
     * 对某人说hello
     * @param who
     */
    public void sayHello(String who) {
        System.out.println(Thread.currentThread().getName()+"hello!"+who);
    }
} 

如上代码:在主线程中for循环新建2个线程调用sayHello,最后两个线程分别对世界问好!这段代码比较好理解,就不贴输出结果了。我们编写并运行了这段代码,我们主要看看这段代码在JVM中是怎么运作的。

首先,我们编写一个HelloWorldTest.Java文件,经过javac编译会转化成字节码HelloWorldTest.class,为什么要转化成字节码呢?因为Java虚拟机能识别!最后由类加载子系统ClassLoader将字节码装载到内存。每块内存各有自己的作用,最后由执行引擎来执行字节码。下面我们重点介绍下各块内存发挥的作用!

image.png

方法区


方法区主要装一些静态信息,比如:类元数据,常量池,方法信息,类变量等。如上代码HelloWorldTest.class是类元数据,sayHello,main都是方法信息等都是放在方法区存储的。方法区中还需要注意两点:

  1. 如果方法区太大,超过设置,会报OutOfMemoryError:PermGen space错误。gclib工具可以动态生成类测试该错误。
  2. 在JDK1.7以前,方法区叫永久代,而1.8之后叫元空间。原因是JDK1.8为了释放管理压力,把运行时常量池交给堆去管理。


-


堆中主要存放实例对象。你可以这么理解,只要看到用关键字new的对象,数据都放在堆中。如上代码HelloWorldTest helloWorldTest = new HelloWorldTest();helloWorldTest是HelloWorldTest对象的引用,指向new出来的HelloWorldTest对象实例,helloWorldTest引用是放在栈中的,也叫局部变量方法内申明的对象类型或普通类型),我们简单画图来表示下堆,栈,方法区关系。当JVM执行了HelloWorldTest helloWorldTest = new HelloWorldTest();这句话,JVM内存结构看起来是这样的。如果指向对象引用消失,对象会被GC回收。

image.png

在堆内存中,内存需要划分成两块区域,新生代老年代。如下图所示。

  1. 新生代:在堆内存中,新生代又分为三块,eden(伊甸园创建新生命,对应new对象),from,to,这三块内存区域都属于新生代,默认比例是8:1:1,每次new对象都会先存储到eden中,如果eden区域内存满了,会触发monitor gc回收该区域,还未回收的对象会放入from或者to,from,to内存其中一块是空的,方便对象在内存中整理标记,每GC一次,from,to两块空间对象每移动一次,还未回收的对象年纪也会增加1,到达一定年纪(默认是15岁),就会进入老年代了。
  2. 老年代:当老年代满了,会触发Full GC回收,如果系统太大,Full GC都回收不了,程序会出现类似java.lang.OutOfMemoryError: Java heap space,我们可以通过配置JVM参数:如-Xmx32m 设置最大堆内存为32M。

对堆分块原因是方便JVM自动处理垃圾回收堆内存是GC回收的主要区域

image.png


-


栈内存空间相对于堆空间比较小,也属于线程私有,栈中主要是一堆栈帧,是先进后出的,理解起来栈帧对应就是一个方法,方法中包含局部变量,方法参数,还有方法出口,访问常量指针,和异常信息表,其中异常信息表和常量指针信息我们在方法体中可能看不出来,但通过工具Jclasslib工具类在反编译class文件可以体现出来,异常信息表可以处理当程序执行报错,会跳转到具体哪行代码执行,JVM中就是通过异常表反馈的。我们还是结合例子和图来详细分析下。当程序运行时,JVM中栈可能如下图呈现状态。

image.png    

一个线程可能对应多个栈帧,栈帧都是从上往下压入,先进后出,如下图所示,在方法A中调用方法B,在方法B中调用C,在方法C中调用方法D,主线程对应栈帧的压栈情况,出栈顺序是D->C->B->A,最终程序结束。另外还需注意:操作数栈的意思是存储局部变量计算的中间结果,比如在方法A中定义int x = 1;在JVM中会将局部变量入操作数栈用来之后的计算。栈也是有空间大小的,如果栈太大,超过栈深度,会类似报错,java.lang.OutOfMemoryError: Java stack space,最常见的例子就是递归了。你会写demo测试递归例子吗?

image.png

程序计数器


程序计数器也是线程独享的,多线程执行程序依赖于CPU分配时间片执行,画个简单的图,看看多线程怎么利用CPU时间片的。如下图,线程0和线程1分配cpu时间片交替执行程序,假设此时线程0先获取到了时间片,时间片用完后CPU会将时间片再分配给线程1,线程1执行完毕后,此时,时间片又回到线程0来执行,那么问题来了,线程0上次执行到哪儿了呢?具体是代码的多少行了呢,该行代码有没有执行完毕?此时程序计数器就发挥作用了,程序计数器保存了线程的执行现场,方便下次恢复运行。这也是为什么程序计数器是线程独享的原因。

image.png

本地方法栈


本地方法栈就不过多介绍了,和栈结构一样,是一块独立的区域,只是对应的是native方法。

直接内存


直接内存独立于JVM内存之外的内存,可以直接和NIO接口交互,NIO接口会频繁操作内存,如果放在JVM管理,无疑会增加JVM开销,所以单独将这块提出来,而且直接内存操作数据相比较JVM更快,显而易见提升了程序性能。

内存分配性能优化-逃逸分析


我们之前说过,只要是看到关键字new,对象分配肯定在堆上,下面我们来看一个案例。

/**
 * @author :jiaolian
 * @date :Created in 2021-03-10 16:10
 * @description:逃逸分析测试
 * @modified By:
 * 公众号:叫练
 */
public class EscapeTest {

    //private static Object object;
    public static void alloc() {
        //一个对象相当于16k大小,非逃逸对象
        //object = new Object();
        Object object = new Object();
    }

    public static void main(String[] args) throws InterruptedException {
        //亿次内存
        long begin=System.currentTimeMillis();
        for (int i=0; i<10000000; i++) {
            alloc();
        }
        long end=System.currentTimeMillis();
        System.out.println("time:"+(end-begin));
    }
}

如上代码,我们在主函数里面通过for循环1亿次来new Object,一个object为16k,大致估算下有GB数据了,此时我们手动配置JVM参数,-XX:+PrintGC -Xmx10M -XX:+DoEscapeAnalysis;设置打印GC信息,默认最大的堆内存是10M。

  1. -XX:+PrintGC。表示控制台打印GC信息。
  2. -Xmx10M。设置最大的堆内存为10M。
  3. -XX:+DoEscapeAnalysis。 开启逃逸分析(默认开启)。

执行程序,打印结果如下图所示。一共进行了3次GC,你可能有疑问10M堆内存需要容纳GB数据冲击,怎么也需要N次GC,为什么只有3次GC?如果设置-XX:-DoEscapeAnalysis关闭逃逸分析,GC可能会出现上千次。运行时间也从3毫秒增至1000毫秒以上。说明了非逃逸对象没有新建的堆上,而是建在栈上了。这样做的好处:从程序GC执行次数和执行时间上来看,程序运行效率提高了。

image.png

  • 原因分析:

观察我们上述案例代码中alloc()方法,方法中Object object = new Object();object是一个局部变量,每次新建后到下一次循环再新建,上一次新建的对象就会出栈,object引用指向的对象就会失效,失效的对象就会被GC回收了。开启逃逸分析后,new Object()创建的对象就不在堆上分配空间了,而放到了栈上。这就是JVM通过逃逸分析对内存的优化。思考下,如果将private static Object object;注释放开,object还会是非逃逸对象吗?

注意:逃逸对象不能在栈上分配空间!

相信到这里你已经对逃逸分析应该有一个比较清晰的认识了。

总结


好了,写的有点累了,写的不全同时还有许多需要修正的地方,希望亲们加以指正和点评,喜欢的请点赞加关注哦。点关注,不迷路,我是【叫练公众号,微信号【jiaolian123abc】边叫边练。

image.png

查看原文

赞 0 收藏 0 评论 0

叫练 发布了文章 · 3月4日

Semaphore实战

简介


Semaphore信号量计数器。和CountDownLatch,CyclicBarrier类似,是多线程协作的工具类,相对于join,wait,notify方法使用起来简单高效。下面我们主要看看它的用法吧!

实战


  • 限流。限制线程的并发数。

比如在一个系统中同时只能保证5个用户同时在线。

import java.util.concurrent.Semaphore;

/**
 * @author :jiaolian
 * @date :Created in 2021-03-04 11:13
 * @description:Semaphore限流
 * @modified By:
 * 公众号:叫练
 */
public class LimitCurrnet {
    public static void main(String[] args) throws InterruptedException {
        //定义20个线程,每次最多只能执行5个线程;
        Semaphore semaphore = new Semaphore(5);
        for (int i=0; i<20; i++) {
            new Thread(()->{
                try {
                    //获取凭证
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"登录成功");
                    Thread.sleep(2000);
                    //释放凭证
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName()+"用户退出");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

如上代码所示:我们定义了20个用户同时访问系统,Semaphore参数是5,表示同时只能有5个用户可以获取凭证,其他用户必须等待直到有在线用户退出。调用semaphore.acquire()表示获取凭证,此时凭证数会减一,调用semaphore.release()表示释放凭证,凭证数会加一,如果系统中有等待的用户,操作此方法会通知等待的一个用户获取凭证成功,执行登录操作。最后打印部分结果如下:证明系统最多能保持5个用户同时在线。

image.png

注意:上面举出的这个案例,出个思考题:线程池是否可以实现呢?

  • 模拟CyclicBarrier,CountDownLatch重用!

Semaphore可以轻松实现CountDownLatch计数器,CyclicBarrier回环屏障,还记得CountDownLatch用法么?它是个计数器,可以帮我们统计线程执行时间,常用来测试多线程高并发执行接口效率,我们下面用Semaphore模拟多线程主线程等待子线程执行完毕再返回。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
 * @author :jiaolian
 * @date :Created in 2021-03-01 21:04
 * @description:信号量测试
 * @modified By:
 * 公众号:叫练
 */
public class SemaphoreTest {

    //定义线程数量;
    private static final int THREAD_COUNT = 2;
    //初始化信号量为0,默认是非公平锁
    private static Semaphore semaphore = new Semaphore(0,false);
    private static ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);

    public static void main(String[] args) throws InterruptedException {
        for (int i=0; i<THREAD_COUNT; i++) {
            executorService.submit(()->{
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"执行");
                semaphore.release();
            });
        }
        //获取2个信号量
        semaphore.acquire(2);
        System.out.println("主线程执行完毕");
        executorService.shutdown();
    }
}

如上代码所示:我们定义了Semaphore初始化信号量为0,默认是非公平锁,在主线程中用线程池提交2个线程,主线程调用semaphore.acquire(2)表示需要获取两个信号量,但此时初始化信号量为0,此时AQS中的state会是0-2=-2,state值小于0,所以主线程执行这句话会阻塞将其加入AQS同步队列,线程池两个线程等待2秒后会调用semaphore.release()释放2个信号量,此时AQS中的state会自增到0,会通知主线程退出等待继续往下执行。执行结果如下图所示。

image.png

有没有发现Semaphore用法可以模拟CountDownLatch,另外Semaphore通过调用acquire,release方法,还可以实现CyclicBarrier功能!我们不举例了。

实现原理


  • 相同点:本质上都是计数器,底层是依赖AQS操作state实现。
  • 异同点:CountDownLatch是共享锁实现,CyclicBarrier是独占锁实现,CountDownLatch通过调用countDown递减计数器只能使用一次,而CyclicBarrier通过调用await递减计数器可以达到“回环”重复的效果。Semaphore也是共享锁实现,通过调用release计数器是递增的,通过设置信号量可以实现CyclicBarrier,CountDownLatch功能。

总结


今天我们介绍了Semaphore,整理出来希望能对你有帮助,写的比不全,同时还有许多需要修正的地方,希望亲们加以指正和点评,喜欢的请点赞加关注哦。点关注,不迷路,我是【叫练公众号,微信号【jiaolian123abc】边叫边练。

查看原文

赞 0 收藏 0 评论 0

叫练 发布了文章 · 3月3日

||运算你真的了解吗?

或运算介绍


或运算:只要有一个条件为true,即为true。

image.png

通过如上逻辑关系图,还有另外一层隐含的意思:

如果A条件是true,B条件不执行!

如果A条件是false,B条件要执行!

下面我们来看一个案例:如果A条件是true,B条件不执行!

/**
 * @author :jiaolian
 * @date :Created in 2021-03-02 11:32
 * @description:或条件判断
 * @modified By:
 * 公众号:叫练
 */
public class ElseTest {
    public static void main(String[] args) {
        int x = 1;
        String ss = "叫练";
        boolean f = false;
        //或条件判断
        if (x == 1 || (f = (ss=="叫练")) ) {
            //A条件是true,B条件是不执行,所以打印f=false
            System.out.println(f);
        }
    }
}

如上代码:A条件是true,B条件是不执行,所以打印f=false。

总结


这是我在平时学习中发现基础盲点,整理出来希望能对你有帮助,简单记录下,如有问题,希望亲们加以指正和点评,喜欢的请点赞加关注哦。点关注,不迷路,我是叫练【公众号】,微信号【jiaolian123abc】边叫边练。

查看原文

赞 0 收藏 0 评论 0

叫练 发布了文章 · 3月1日

图解CyclicBarrier运动员接力赛

图解游戏规则


大家都知道运动员短跑接力赛,今天我们并不是讲接力赛,我们讲“接力协作赛”,需要我们重新定义下游戏规则:如下图所示

image.png

现在有运动员A,B,先定义游戏规则:赛道目前是300米,每个运动员在跑完第一个100米时,需要等待其他运动员跑完第一个100米,比如运动员A先跑完100米,而此时运动员B只跑了95米,那运动员A必须要等待运动员B跑完剩余的5米,然后再一起接着跑第2个100米,第三个100米,规则也和第1个100米类同,最后我们可以得出一个结论,两个运动员跑完300米赛道,最长需要花多少时间。【本案例纯属虚构,为了讲清楚CyclicBarrier】。下面我们用代码模拟执行。

案例说明


import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author :jiaolian
 * @date :Created in 2021-03-01 14:56
 * @description:回环屏障测试--接力赛
 * @modified By:
 * 公众号:叫练
 */
public class CyclicBarrierTest {

    private static final int THREAD_COUNT = 2;
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2,()->{
        System.out.println(Thread.currentThread().getName()+"冲破屏障");
    });
    private static ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);

    public static void main(String[] args) {
        Runnable myTask = new MyTask();
        //初始化两个运动员
        for (int i=0 ;i<THREAD_COUNT; i++) {
            executorService.submit(myTask);
        }
    }

    private static class MyTask implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName()+"第1个100米");
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getName()+"第2个100米");
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getName()+"第3个100米");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

如上代码:线程池模拟执行两个运动员,每个运动员执行完每个100米必须等待另一个运动员,执行结果和我们设想一致,如下图所示。其中pool-1-thread-1,pool-1-thread-2分别表示运动员A,运动员B。CyclicBarrier初始化参数中有一个Runnable是用来冲破屏障回调的函数。

image.png

比较CountDownLatch


CyclicBarrier中文释义“回环屏障”,每个线程调用await,计数器会减1,如果此时计数器不为0,线程会阻塞,如果计数器为0说明需要冲破屏障,会唤醒之前被阻塞的线程,并会重置计数器。源码实现中用到了独占锁和条件队列控制线程的进队和出队,CountDownLatch用到的是共享锁,虽然实现不一样,底层都是AQS,相对于CountDownLatch来说,CyclicBarrier是它的补充,功能更强大。

总结


今天我们介绍了CyclicBarrier,整理出来希望能对你有帮助,写的比不全,同时还有许多需要修正的地方,希望亲们加以指正和点评,喜欢的请点赞加关注哦。点关注,不迷路,我是【叫练公众号,微信号【jiaolian123abc】边叫边练。

查看原文

赞 0 收藏 0 评论 0

叫练 发布了文章 · 2月28日

join为啥会阻塞主线程?

join使用


上篇我们介绍了CountDownLatch,顺便说到了Thread中的join方法!

import java.util.concurrent.TimeUnit;

/**
 * @author :jiaolian
 * @date :Created in 2021-02-28 21:43
 * @description:join测试
 * @modified By:
 * 公众号:叫练
 */
public class JoinTest {

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":想先执行");
        },"线程A");
        //开启一个线程A
        threadA.start();
        //主线程会持有子线程的锁,子线程还没开始主线程就阻塞了,等待子线程结束后通知;
        threadA.join();
        System.out.println(Thread.currentThread().getName()+ "线程执行");
    }
}

如上代码所示:在JoinTest开启一个线程A,threadA调用join()方法,主线程会等待threadA执行完毕!也就是两秒后,主线程执行最后一句话,运行结果如下图所示!

image.png

我们深入源码,join方法底层其实就是一个wait方法,但现在问题是:明明调用者是线程A,可阻塞的是mian线程,不应该阻塞的是threadA吗?

证明问题:明明调用者是线程A,可阻塞的是mian线程


我们参照Thread中join源码,将上面的代码改造如下:

import java.util.concurrent.TimeUnit;

/**
 * @author :jiaolian
 * @date :Created in 2021-02-28 21:43
 * @description:join测试
 * @modified By:
 * 公众号:叫练
 */
public class JoinCodeTest {

    public static void main(String[] args) throws InterruptedException {

        MyThread threadA = new MyThread("线程A");
        //开启一个线程A
        threadA.start();
        //主线程会持有子线程的锁,子线程还没开始主线程就阻塞了,等待子线程结束后通知;
        threadA.join2(0);
        System.out.println(Thread.currentThread().getName()+ "线程执行");
    }

    private static class MyThread extends Thread {

        public MyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":想先执行");
        }

        //复制Thread源码中的join方法测试阻塞的是线程A还是main线程?
        public final synchronized void join2(long millis)
                throws InterruptedException {
            long base = System.currentTimeMillis();
            long now = 0;

            if (millis < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }

            if (millis == 0) {
                while (isAlive()) {
                    //虽然调用者是线程A,但真正执行阻塞的是main线程!
                    System.out.println(Thread.currentThread().getName()+"会阻塞");
                    wait(0);
                }
            } else {
                while (isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0) {
                        break;
                    }
                    wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }
        }
    }
}

如上代码所示:MyThread继承Thread,并复制了join源码,将join修改成join2,并在join2方法中增加了一个输出语句,System.out.println(Thread.currentThread().getName()+"会阻塞")用来测试阻塞的是线程A还是main线程,所以在JoinCodeTest的main方法中ThreadA是调用join2方法,

结果发现进入join2方法的线程是main线程。运行结果如下图所示!

image.png

这里可以把join理解成一个普通方法!真正阻塞的不是调用者线程,而是当前正在执行的线程。

总结


今天我们介绍了join方法,特别是将源码中代码copy出来证明测试,相信整理出来希望能对你有帮助,写的比不全,同时还有许多需要修正的地方,希望亲们加以指正和点评,喜欢的请点赞加关注哦。点关注,不迷路,我是【叫练公众号,微信号【jiaolian123abc】边叫边练。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 2 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-07-22
个人主页被 1.3k 人浏览