JVM虚拟机及GC机制算法

  1. JVM基本结构
    **类加载子系统:加载二进制流的class文件,会经历加载,校验,准备,解析,初始化五步,校验是为了判断字节码是否合理合法。准备阶段是当一个类验证无误后需要给该类分配内存空间并将静态变量赋予默认值,构造相关数据结构,如方法表,final修饰的常量也在准备阶段初始化。解析阶段是将符号引用转化为实际的变量或方法的内存地址。初始化阶段是将已经完成解析的类装载到系统,初始化阶段重要的工作是执行类的初始化方法,但是是线程安全的,所以当执行类的初始化时容易发生死锁且很难排查。
    **方法区:JDK1.7及之前版本叫做永久区,存放类的字段,方法,常量池等,不过JDK1.7已经将常量池移除到了堆中。方法区大小限制了系统可以保存多少个类。JDK1.8及之后叫做元空间,元空间是一块堆外直接内存,即不受虚拟机影响,和本地内存同步,不声明大小的话就和系统可用内存一样大。(方法区存放的是类信息,而堆存放的是运行时动态分配的对象信息,原始类只有一个,而堆中的类的实例对象可以有无数个)
    **堆:JDK1.6及之前堆存放类的实例对象,在JDK1.7时方法区中的常量池就移除到了堆中。堆空间是线程共享的。
    **直接内存:JDK1.4提供了NIO类,基于通道和缓冲区的方式,可直接操作本地内存,在本地内存存放数据,在Java堆存放该数据对象的引用,减少了本地内存到Java堆的复制,提高了效率,同时在Java堆GC时,本地内存也会进行垃圾回收。但与此同时本地内存的GC要等到Java堆执行GC,所以容易出现本地内存满了而Java堆还没满造成的内存异常。
    **Java栈:存放对象的引用,每创建一个线程都会创建一个线程私有的Java栈,栈中保存着局部变量,方法参数,同时保存Java方法调用,返回值信息等。
    **本地方法栈:Java虚拟机允许Java直接调用本地方法。
    **PC寄存器:指向当前正在执行的指令,保存当前执行进度
    **执行引擎:执行虚拟机字节码

  2. 垃圾回收机制:
    **引用计数法:每一个对象被引用的时候该对象的引用计数器都会加一,当计数器为0说明该对象已经不被使用,就可以被GC。但是无法解决循环引用问题,如两个对象A,B,对象内部互相引用对方的引用,那么就无法回收。
    **标记清除法:首先执行可达性分析(从根对象出发,最终可以到达的对象叫做可达对象),对于可达性的对象说明还在使用,标记为存活对象,而不可达的未被标记,将在下一步清除阶段进行GC,但容易产生内存碎片(空闲的连续空间小于要申请的内存空间,造成小的内存空间未被利用)。
    **标记压缩法:将依然先进行标记存活对象,然后将存活对象压缩到内存的一端,再清除存活对象边界以外的空间,即不会产生内存碎片也不会需要两块相同内存空间。
    **复制算法:将存活对象复制到内存另一半空间中,执行清除时直接清除另一半内存空间,没有内存碎片产生,但是内存利用率只有50%。
    **分代算法:即新生代串行垃圾回收器:使用了优化后的复制算法,将新生代分为eden区,survivor区,survivor区又分为from区和to区,垃圾回收时,eden区的存活对象会复制到from或to区,然后直接回收其中eden和to或from区,此时经历过几次GC都没有回收的对象和大对象都会进入老年代(此时就保证了内存连续性,内存碎片就少了,也保证了内存利用率),对于新生代多扫描,默认新生代的对象用完就会成为垃圾回收掉,对于老年代的对象默认其为长生命周期,减少扫描次数,从而减少复制次数。(对新生代使用复制算法。对老年代使用标记压缩或标记清除算法)
    **分区算法:将内存划分为多个区,每个区独立执行回收,效率很高。但是堆空间越大,GC的STW时间就越长。

3.引用强度:
**强引用:直接定义的引用,其他三种引用需要使用java.lang.ref包实现。强引用对象是可触及的,程序运行时如果内存不够了,系统宁可抛出OOM异常也不会回收强引用指向的对象。容易造成内存泄漏(垃圾对象未及时回收)
**软引用:GC未必会回收软引用指向对象,但是内存不够了,就要回收软引用指向对象。一般用于缓存,只要空间够,缓存数据方便下次读取更快,但是空间不够就回收缓存空间为程序提供空间。
**弱引用:当进行GC时就会回收引用指向的对象,一般用于HashMap存储数据,当Key是强引用而,value是弱引用,只要Key回收掉了,该段数据也就回收了,如保存用户信息和session时,用户下线了,session也就没必要了。
**虚引用:虚引用和没有引用一样,如过要通过虚引用get方法获取强引用会失败,虚引用的作用就是跟踪垃圾回收过程。

4.String在虚拟机中的实现:
**首先要知道String的实现是基于char数组的,在JDK1.7中,对于每个subList方法截取的字符串,都会在原来字符串基础上创建新对象而只修改了offset(偏移量)和count(长度),假如源字符串没有回收,那很好,新的字符串对象也复用源字符串,常量池中只存在这一个源字符串而被两个对象共享,还节省了空间,但是源字符串被回收了,糟糕的地方来了,该新对象会创建一个和源字符串相同的字符串放入字符串常量池,然而该新对象字符串只是源串截取的一部分,很多都未使用到,但在内存中依然占有位置。此时就造成了内存泄漏。

  1. 锁和并发:
    **每个对象的对象头中都有一个数据区可以存放锁信息的区域,包含了锁对象,锁状态,锁的类型,锁的重入次数等。在Java虚拟机中,JDK1.6版本时,获取锁时会有一个锁升级过程,分别是偏向锁,轻量级锁,自旋锁,重量级锁。
    **偏向锁:当一个线程获取到锁的时候,会先将对象头中锁信息修改为偏向锁状态,此时耗费的资源是最小的,且用完不会释放锁,假如下次还是该线程执行,就不需要获取锁了,但如果是其他线程获取到了锁,该线程就要退出偏向模式,对于竞争激烈的情况,偏向锁很难保持,对性能起不到太大优化作用。
    **轻量级锁:如果偏向锁获取失败,就会申请轻量级锁,申请轻量级锁的时候,会尝试CAS操作(自旋)加锁,加锁失败就要升级为重量级锁了。
    **重量级锁:如果轻量级锁加锁失败,就要进行锁膨胀升级为重量级锁,而启用重量级锁会调用enter方法进入该锁,线程很可能在此时被操作系统挂起,那么此时的切换和调度成本就非常高了,所以在锁膨胀之前就有了自旋锁,自旋锁可以让线程未获得锁时不被挂起,执行一个循环尝试获取锁,若干次失败后才会使用重量级锁挂起线程进入锁。但是当锁竞争非常激烈时,自旋一定次数后仍然免不了挂起操作,此时就白白浪费了自旋等待时间。所以在JDK1.7中JVM对自旋锁自旋次数做出了调整。
    **锁消除:对无竞争的资源操作时,JVM会消除这些锁。如使用单线程使用线程安全容器会锁消除。
    **减少锁持有时间:在真正发生冲突的地方加锁,对于线程安全的代码就无须加锁了
    **减小锁粒度:如ConcurrentHashMap中put方法,当执行插入时,只需要对要插入部分所在段进行加锁就可以了。也就是分段锁
    **锁分离:如多线程对于链表的头插和尾插,两个区域互不影响,那么就可以定义putFirst锁和putLast锁,分别保证其对应区域的线程安全。
    **锁的粗化:虽然上面提到要精准定位到线程不安全的代码,但是如果在一个线程中刚释放了锁,而仅仅相隔几行(这几行为线程安全)代码又要加刚刚释放的锁,此时直接将这几行线程安全代码和下面使用相同锁的代码放入刚刚的代码中,因为锁的释放和加锁也是需要时间的,这种锁优化需要视具体情况而定。

6.无锁解决线程安全冲突问题
**CAS算法:属于Unsafe类,即Compare And Swap比较和替换,包含了三个参数(V,A,B),V是内存值,A是旧的内存值,N是要更新的新值,如果在更新时发现V和A不相等,就说明其他线程已经操作过了,那么更新A值等待下次对比相等时就可以执行更新操作,也就实现了线程安全,但是只适用于基本数据类型,且当大量的自旋操作等待时,性能也会比较差。具体实现类是java.util.concurrent.atomic。但是存在ABA问题,当比较内存值与预期值结果是相等的时候,可能已经有其他线程修改了该内存值,只不过经过其他线程又改回了预期值,此时修改的话并不是线程安全的,所以又增加了版本号来确保线程安全。
CAS优化:正如ConcurrentHashMap一样,也可以使用分段的思想,校验内存值和新值的时候一部分值的和,提高了并行度。

7.Java内存模型:JMM
**为了屏蔽硬件和系统差异,让一套代码在不同设备上可以正常运行。
**JVM规定了线程操作主内存时先拷贝一份到自己的工作内存,然后线程的操作在线程的工作内存当中,但是什么时候同步到主内存中就由JMM实现了。

8.Happens-Before原则:当一个线程先A执行了一系列操作,如果这些操作是线程安全的,那么对后操作的B线程来说A的操作是可见的。具体有volatile,对于volatile修饰的变量,写操作一定发生于读操作之前,所以保证了volatile的内存可见性。

9.ReentrantLock基于AQS实现,具体含有acquir方法,lock方法就是基于acquir实现,当获取锁失败就线程进入等待队列,获取成功就直接执行。

10.类的加载器:启动类加载器:底层是C/C++实现,Java代码无法访问到。扩展类加载器,应用类加载器,自定义加载器
**当一个类被调用的时候会先从自定义加载器开始查找,而当一个类被加载时,会先从顶层即启动类加载器开始加载。保证了Java代码的安全性,这也就是双亲委派模式的基础,当用户想加载一个类会先请求双亲加载,也就是从启动类加载器开始,逐层往下去找到该类。

  1. 核心垃圾收集器G1:
    **G1收集器使用了分代算法,分区算法,在进行GC时经历4个阶段:新生代GC,并发标记周期,混合收集,如果需要可能会进行Full GC。
    **新生代GC:当eden区内存使用达到回收阈值,就会进行新生代GC,对于存活对象复制到survivor区,然后清空eden区,部分survivor区对象会获得晋升,进入老年代。
    **并发标记周期: 初始标记,该操作标记从根节点直接可达的对象,这个阶段伴随一次新生代GC,会产生全局停顿。 根区域扫描:因为初始标记伴随新生代GC,所以eden会被清空,此时会扫描survivor区直接可达对象。 并发标记:这是一个并发过程,目的是扫描并标记整个堆中的存活对象。可以被新生代GC打断。 重新标记:重新标记也会造成全局停顿,该操作是对并发标记的补充。 独占清理:将各个区域存活对象和GC回收比例进行排序,识别可供混合回收的区域。 并发清理阶段:识别清理完全空间区域,并发操作不会造成全局停顿
    **混合收集:在并发标记周期时对垃圾比率高的区域进行了标记,混合收集阶段会正常执行新生代GC,还会选取一些被标记的老年代进行垃圾回收,被清理的区域中存活对象还会被移动到其他区域,减少了内存碎片产生。
    **Full GC:一般由于老年代内存过小或老年代连续内存过小,元空间内存达到阈值而导致,Full GC会造成全局停顿,性能极低,如果JVM多次出现Full GC,那就要考虑增加老年代内存大小了。

  2. 造成STW的场景:G1的初始标记,重新标记,独占清理。Full GC操作。

  3. CMS执行步骤:
    **初始标记:会全局停顿,然后标记根对象
    **并发标记:标记所有对象
    **预清理:为正式清理做准备和检查以外,控制一次停顿的时间。
    **重新标记:会全局停顿,修正并发标记的数据
    **并发清理:清理垃圾
    **并发重置

**使用的垃圾回收算法:标记清理算法,内存碎片问题无法解决。由于使用标记清理算法,所以初始标记,并发标记,重新标记都是为了标记回收对象,并发清理才是真正的回收垃圾对象,并发重置是为了垃圾回收完成后重新初始化CMS数据结构和数据,为下次垃圾回收做好准备。

  1. JVM性能调优:
    **如果发现Full GC次数过多,大概率是因为老年代满了,直接增加老年代内存空间可减少Full GC次数。发生Full GC时会同时回收新生代和老年代
    **更大的堆内存意味着GC的频率会降低,但是每次GC的时间会增加
    **更小的堆内存意味着GC的频率会增加,但是每次GC的时间会降低
    **具体怎么设置大小要视具体情况而定
    **将初始堆大小和堆最大容量大小设置为相等,可以避免GC后调整堆容量带来的时间消耗

  2. JVM回收垃圾的时机:
    **当new的对象无法放入堆会触发GC,此时只回收新生代区域
    **当老年代已满或快满时触发Full GC

16.OOM异常抛出时机:
**当JVM98%的时间都在回收垃圾且每次回收内存小于总内存的2%就会抛出OOM

17.吞吐量:CPU运行时间/GC时间

  1. 为什么叫做重量级锁:当线程自选后仍未争取到锁,就要放入阻塞队列,而阻塞队列中线程唤醒操作是由操作系统完成,系统调用存在用户态和内核态的切换,所以开销很大,虽然自旋失败仍要进入重量级锁,但这些都是工程师们为提高性能而做出的补救措施。

  2. 在线程竞争资源不激烈的情况下,一般只能到轻量级锁,轻量级锁之间的竞争都会先经历CAS自旋操作,此时各个线程不会因为抢不到锁而进入阻塞,但一定次数之后还没获取到锁,就要升级为重量级锁,其他未获取到锁的线程就进入阻塞队列,一旦进入阻塞队列,就要经历阻塞和唤醒,以及系统在用户态和内核态之间的切换。

  3. CMS和G1的区别:
    **G1在JDK1.7开始使用,使用标记压缩算法,CMS在JDK1.6开始使用,使用标记清除算法
    **G1无内存碎片产生,CMS会产生内存碎片
    **G1回收新生代和老年代,CMS回收老年代
    **G1回收时一般针对新生代,回收时间可控,CMS一般全堆扫描回收,回收时间不可控
    **对象进入老年代时机也不同,CMS进入老年代时机比较早,G1需要更多次的扫描才能进入老年代

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值