美文网首页
整理笔记(线程和垃圾回收器)

整理笔记(线程和垃圾回收器)

作者: Chen_yiy | 来源:发表于2019-11-04 21:01 被阅读0次

    1. 集合部分.

    集合有List,map,set

    • List有arrayList和LinkedList Array是在内存中连续的,因此在查询快,在插入和删除的时候,会需要对于数组内存的调整.因此在数据量比较大的- 时候,插入和删除是比较慢的.
    • LinkedList的则是用链表的形式存储数据,在内存中是不连续的,在插入和删除的时候,只需要在对应节点记录下个节点的内存地址就可以.
      map分为 hashMap和treeMap以及linkedHashMap hashTable等.
      hashMap的是以数组和链表的形式存储数据的.是以Key/Value的形式存储的数据的,允许Key和value 为null
    • treeMap内部有序的树作为对应的key/value,允许key为null
    • set则是无序的不可重复的集合,需要实现对应的hash和equals方法,在保证在对象的去重.

    2.线程的状态

    线程的状态分为:

    • 初始化状态: 新建的线程对象,但是还没有调用start方法
    • 可运行状态:调用了start方法,等待线程调度执行,这个时间被称为可运行
    • 运行状态:在cpu上执行,也就是运行状态.
    • 阻塞状态:线程因为某种原因放弃了执行的cpu时间,暂时停止,直到有机会在重新运行.
    • 阻塞的情况分为:
      • 同步阻塞:运行时的线程在获取对象的同步锁的时候,若同步锁被别的线程使用,JVM会把线程放到锁池中.
      • 等待阻塞:运行的线程执行wait方法,jvm会把该线程放到等待队列中
      • 其他阻塞:运行时的线程执行sleep方法或者join方法,或者方法发出了I/O请求,此时线程就会进入阻塞状态,当sleep的时间结束或者join等待结束就会重新进入运行时状态.
    • 等待状态:表示线程阻塞与锁
    • 超时等待状态:不同与等待状态, 当等待时间过了之后,就会自行返回
    • 终止状态:表示线程已经执行完毕了

    3.线程池的优缺点

    线程池的优点: 可以避免线程不断的创建和销毁来消耗cpu和时间.
    线程如果执行的是不耗时的操作,那么线程执行比较快,线程是不会占用过多的cpu核数,只有在线程池执行的方法比较耗时才会占用其他的cpu核数,因此创建线程池大小的时候,对于线程池的大小选择尤为重要.
    一般来说,线程池的大小是根据CPU的核数选择的大小

    如果是CPU密集型,应该选择 cpu核数+1
    如果是I/O密集型,应该选择CPU核数 * 2 + 1

    因此可以得到另外的一个计算公式:
    最佳线程数目 = ((线程等待时间+线程CPU时间)/ 线程CPU时间)

    因此的出来的结论是 CPU执行时间越久的需要的线程数可以越多,CPU执行的时间越快,需要的线程大小应该越小,可以避免线程间切换引起的耗时和占用的资源

    4.JVM的数据结构和垃圾回收算法

    JVM的内存数据结构中划分为5个区域

    • 程序计数器: 程序计数器是一块很小的内存区域,是线程私有的,是当前线程执行的行号指示器.

    • 堆:堆是线程共享的内存区域,对象的创建主要是在这个区域.

    • 本地方法栈:本地方法栈的和虚拟机栈执行的都是java字节码.

    • 虚拟机栈:每个执行的方法都会创建一个栈帧用于存储局部变量,操作栈,动态链接,方法出口等信息,每一个方法被调用的时候就是一个入栈出栈的过程.

      • 虚拟机栈在编译时就已经确认了在内存的大小.在运行时是不会改变大小的.
    • 方法区:方法区和堆一样,是所有线程共享的区域,存储 类加载的信息,常量,静态变量等

    • 运行时常量池,是方法区的一部分,用于存放编译时生成的字面量和符号引用.

    4.1 可达性分析算法

    由于对象的创建占用一定的内存的,当对象达到一定程度后,就必须要进行对象的回收.一般来说回收都是不在被程序引用的对象. 如何确认对象是否不被引用,主要是用根可达分析来计算对象是否被引用.

    根可达分析算法:

    1. 根据"GC ROOT"来将所有有引用的对象形成一个引用链,那么在这些链以外的对象就是不可达的对象,也就是可以进行回收的对象.
      GC ROOT:也就是根,那么使用什么样的对象可以作为根?
    2. 虚拟机栈中引用的对象,为什么是虚拟机栈中的对象,可以想想虚拟机栈是什么?(正在运行的方法 虚拟机栈用包含的方法栈,方法出口,以及局部变量)
    3. 方法区中类的静态属性作为引用对象 (方法区中存放的是类加载信息,静态变量,都是和类对象相关的)
    4. 方法区中常量引用的对象. (都是类中的对象)
    5. 本地方法栈中JNI(native方法)引用的对象.

    对象被宣布真正可以被回收是需要经过两个步骤

    1. 根据根可达分析后,没有在对应的引用链
    2. 看对象finalize方法,如果在对象的finaliz方法,重新建立引用,那么对象就无法被回收.

    程序计数器算法
    程序计数器: 对象在被创建后,当对象存在引用的时候,计数+1,对象引用使用完毕后,计数-1,当程序的计数为0时,就是可以被回收.
    缺点: 如果是ABA引用,则没法办进行对象的回收

    4.2垃圾回收算法

    垃圾回收算法主要分为以下几种:

    1. 引用计数器回收算法:
      • 同引用计数算法: 如果对象的引用计数为0,则标记可以回收,但是但是如果循环引用的话,则无法回收.
    2. 标记清理算法:
      • 在开始进行清理的时候,先进行标记需要清理的对象,然后在回收对象,但是由于只清理对象,那么空间碎片就比较多.在开辟比较大的对象的空间的时候,很容易造成空间不足.
    3. 标记整理算法:
      • 标记整理算法整体思路和标记清理一样,就是在清理完对象后,把存活的对象统一向内存的一段移动.整理了内存碎片
    4. 复制算法:
      • 复制算法:在内存中划分为大小相等的两块区域,在通过根搜索确定到对应的存活的对象时,将存活的对象复制到新的内存区域中,然后把原来区域中的对象清理.存在的问题是:内存利用率比较低. 会比较频繁的进行对象回收.
    5. 分代收集算法:
      • 分代收集算法: 认为对象的存活的时间都比较短,因此将整个jvm划分为: 将内存区域划分为2个subver 区域和一个eden 区域 . 在内存的占比为1:1:8

      • 新生代:
        新生代的对象的生命周期比较短,因此需要频繁的进行对象的回收:新生代采用复制算法. 对象的创建主要在eden区,回收时,先将存活的对象复制到subver0区,然后清空eden区域,如果subver0也放满了对象,则将存活的对象复制到subver1中,然后清空subver0和eden区域的对象.这样保持subver两个区域的其中一个为空.
        当subver1也无法存放的下对象的时候,就会将存活的对象直接放置到老年代.如果老年代也满了,就触发一次fullGC,也就是新生代和老年代同时进行对象回收.

        - **注意:fullGC(stop the word 也就是执行fullGc的时候,是停止其他线程的). 新生代发生的GC是minorGC, minorGC发生的频率比较频繁,不一定等eden区满了才触发.**
        
      • 老年代(Tenured):
        老年带的对象都是经历过N次垃圾回收而仍然存活的,因此老年代中存储的都是存活周期比较久的对象.
        老年代的内存区域也比新生代的对象大很多,大概是1:2,当老年代满了之后就会触发fullGC fullGC发生的频率比较低.

      • 永久代(Perm):
        永久代的存放的对象都是些不会经历回收的对象(静态文件). 如果说java类,方法等.存放的是比如hibernate的一些类,因此需要设置一个比较大的持久类区域存放.永久代区也称为方法区. 方法区的对象回收是不同于堆区的回收的.

      • 方法区的回收主要的内容有: 废弃常量,和无用的类. 对于废弃常量可以通过根可达分析算法判断回收.

        • 对于无用的类需要满足以下条件才会被回收.
          a.该类的实例已经全部被回收.也就是不存在该类的任何实例.
          b.加载类的ClassLoader已经被回收.
          c.该类的java.lang.Class对象已经没有在任何地方被引用,无法再任何地方通过反射的方式访问该类.
        • 什么时候会触发FULLGC?
          a.老年代满了
          b.永久代满了
          c.system.gc方法调用,但是不是立即调用.
          d.在新生代的对象进入老年代内存时,老年代剩余空间不足
        • 压测时,fullGC频繁,需要怎么排查?
          思路: 对象创建的频繁,但是fullGC,说明新生代晋升到老年代的数量多,那么是不是新生代的对对象被引用,一直不被释放?
          a.观察GC日志,看是否有内存泄漏,或者代码存在一直引用对象的地方.
          b.调整老年代,新生代的区域
          c.Dump内存,分析

    5.对象的内存布局

    对象在内存中分为: 对象头、实例数据、对齐填充

    5.1 对象头:[markword]

    • 对象头:携带的是对象在虚拟机中所特有的信息,对象的hashCode,对象的锁状态,是否是偏向锁的标志,对象的分代年龄,持有偏向锁的线程ID.

    • 如果在32位虚拟机,那么虚拟机的对象头中是这样分配的.
      25位的hashCode 、1位是否偏向锁(0非 1 是 ) 、 2位锁状态(01 无锁 01 偏向锁 01 轻量级锁 10 重量级锁 11 GC标记) 、 4位的分代年龄

    • 不同状态时对象头中包含的含义和数据.


      image.png

      如果对象是数组: 对象头中还存放这数组的长度.

    5.2 实例数据

    • 实例数据是对象各种属性的类型,不管是从父类继承的还是子类中定义的.

    5.3 对齐填充

    • 由于hotSpot规定对象的长度必须是8的整数倍,对象头刚好是整数倍,实例数据由于是对象的各种数据,如果不够8的倍数,则需要占位符填充对齐.

    5.4 四种锁变换的描述

    java中包含的四种锁,无锁,偏向锁,轻量级锁,重量级锁,以及其他的锁 自旋锁 自适应自旋锁的过程描述

    • a.从不存在竞争的时候就是无锁
    • b.当只有一个线程存在竞争锁的时候,线程会通过CAS的方式写入对象头中的偏向线程ID为自己的线程ID编号.

    在这个过程中,首先判断对象的markword(对象头)是否处于可偏向的状态.

    1. 如果为可偏向状态,则通过CAS的方式,把自己的线程ID写入到对象头里.如果CAS成功,则认为获取到锁,开始执行同步代码.
    2. 如果不可偏向则检查markword中的线程ID是否等于当前线程的线程ID.
    3. 如果相等,则获取到锁,如果不等,则需要撤销偏向锁,也就是锁要升级了(但是此时需要等待全局安全带[此时间点,没有线程在执行字节码]),因为有另外的线程在争夺锁.此时需要注意的是偏向锁的撤销.
      • 偏向锁的撤销(需要的到达全局安全点):
        • 两个时机触发偏向锁的撤销
          1. 在通过CAS写入线程ID时,如果写入失败,则任务有其他线程争夺.
          2. 如果线程的对象的对象头里的偏向线程ID不是自己,则需要升级锁.
        • 偏向锁的撤销是在获取锁的过程中,发现了竞争,直接将一个偏向对象 升级到 轻量级锁的状态.
          首先到达全局安全点. 通过markword(对象头)中已经存在的那个线程ID,找到对应的那个线程,然后在该线程的栈帧上补充上轻量级锁时,会创建保存锁记录的空间,将对象头的mardword复制到锁记录,然后获取偏向锁对象的markword更新为指向该锁记录的指针.至此,阻塞在安全点上的线程可以继续执行.
    • c.在偏向锁撤销的过程中,完成了轻量级锁的升级,但是此时对象可能会存在两种状态
      • 一种是变成轻量级锁的 无锁状态(没有被任何线程持有).
      • 一种是变成轻量级锁的 有锁状态(原来的偏向锁的线程又继续获取到了锁,继续在执行同步代码)
    • d.轻量级锁在加锁的时候,类似于偏量锁的撤销,此时通过对于对象头指向线程锁记录的指针修改来判断是否获取到锁,如果没有获取到锁,jvm会使用自旋锁,自旋锁通过重试去尝试抢夺锁,如果抢锁成功,则执行同步代码,如果抢锁失败,则进行锁升级,升级到重量级锁.
    • e.此时把锁的标志位改为10 在这个状态下,为抢到锁的线程都会被阻塞,指导持有锁的线程唤醒,然后去争夺锁.此时争夺的条件为CAS的去写入锁对象的对象头指向monitor对象的地址.

    5.5 盗图一个网上整理的对象头和锁之间转换的过程.

    image.png

    6.四种引用的作用和状态

    6.1强引用

    • 强引用就是比如说Object obj = new Object(); obj强引用一个对象,这个对象在内存中,如果只要还在被引用,那怕虚拟机发送内存泄漏也不会回收该对象.

    6.2软引用

    • 软引用的对象,当内存空间不足的时候,就会回收该引用所引用的对象.

    6.3虚引用

    • 虚引用的对象,如果只引用单独的对象,则不会有其他的效果,该回收的时候就会回收对象.一般都配合引用队列使用.

    6.4弱引用

    • 弱引用的对象,当发生GC的时候就会回收,无关内存是不是不足.

    7.GC收集器

    stop the world

    7.1 serial收集器

    • 特点是 单线程,执行效率高,但是在执行期间,必须要停止所有其他工作的线程,直到它结束.

    7.2 CMS 收集器

    CMS收集器是老年代的收集器,CMS采用标记清理方式.在执行的过程中,主要有4个步骤. 初始标记.并发标记,重新标记,并发清理

    • 初始标记:GC线程根据根可达快速的找出可达的对象.只能GC线程运行,会有短暂的STOP THE WORLD
    • 并发标记:在这个过程中,多个GC线程和用户线程同时运行,用来根据根可达的算法,计算对象是否存活.
    • 重新标记:在这个过程中,GC线程来矫正并发标记过程中被修改的对象.
    • 并发清理: 同时清理标记过的对象.
    • 缺点: 由于CMS采用的是标记清理算法,内存空间在清理后有较大的空间,可能存在在老年代无非开辟足够大的对象,而重新引起对Full GC
      由于并发修改对象,也可能会导致新一次的fullGC

    7.3 G1收集器

    • G1收集器是对整个堆进行对象操作的收集器. G1收集器将整个堆划分为多个region,当然年轻代和年老代区域也还是存在的.在划分成不同的region,那么也会存在年老代引用年轻代的对象,如果在此时回收对象的话,那么必然会引起对整个堆空间的扫描,为了解决这个问题,每个region中也会存有一份remember set 集合,用于存储那个region引用本region的对象.
      不考虑remember set的G1收集器在处理过程中分为 初始标记,并发标记,最终标记,筛选回收
      • 初始标记: GC线程快速的找到根可达分析可以到达的对象.
      • 并发标记: GC线程和用户线程同时运行,用于分析根可达分析对象的是否存活.
      • 最终标记: 用户修整并发标记因用户程序运行导致标记发送变化的那部分对象,且对象的变化时记录在remember set Log对象里,此时需要把 remember set log 和 remember set进行整合.
      • 筛选收回: G1收集器会计算各个region区域垃圾的大小,并且计算回收价值,来择优进行对象的回收.

    相关文章

      网友评论

          本文标题:整理笔记(线程和垃圾回收器)

          本文链接:https://www.haomeiwen.com/subject/nsqjbctx.html