JVM基础知识总结

此文章仅为个人以前通过网上的知识点和资源进行的整理整合,侵删

1.常见问题

  1. Java8 虚拟机和之前更新变化的区别
  2. 什么是OOM,什么是栈溢出
  3. JVM常用调优参数
  4. 内存快照如何抓取,怎么分析Dump文件
  5. 谈谈你对类加载器的认识

2. JVM位置

在这里插入图片描述

3. JVM结构

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

3.1 类加载器
名字引用在JVM栈里,具体属性数据在堆里

在这里插入图片描述

1.虚拟机自带加载器

2.启动类(根)加载器(主要负责j加载ava.lang.*)

3.扩展类加载器(主要负责加载jre/lib/ext目录下的一些扩展的jar)

4.应用程序加载器

4. 双亲委派机制(保证安全)

沙箱安全机制的扩展

在这里插入图片描述

AppClassLoader—>ExtClassLoader—>BOOTLoader

  1. 类加载器收到类加载的请求

  2. 将这个请求向上委托给父类加载器,一直向上委托,直到根加载器

  3. 加载器检查是否能加载这个类

    ​ 如果根加载器能加载就结束,然后使用当前加载器

    ​ 否则抛出异常,通知子加载器进行加载

    重复step3

为什么设置这种机制?

这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

5. Native

native:凡是带了native关键字的,说明java作用范围到达不了,会去调用底层c语言的库

进入本地方法栈,调用本地方法库:JNI

JNI作用:扩展Java的使用,融合不同的编程语言为java所用,最初c/c++(java开发初,c++横行)

在内存中专门开辟了本地方法栈(Native Method Stack),登记native方法

// java程序驱动打印机,Robot()类

// 调用其他接口,Socket,WebService

6. 方法区

在这里插入图片描述
static,final,Class,运行时常量池

7. 栈(Stack)

为什么main()方法最先执行,最后结束

栈:栈内存主管程序运行,生命周期和线程同步;线程结束,栈内存释放,不存在垃圾回收机制,一个线程一个栈

栈存什么?:8大基本类型 + 对象引用(地址)+ 实例的方法

在这里插入图片描述

栈+堆+方法区交互:

在这里插入图片描述

8. 堆(Heap)

一个JVM只有一个堆,堆内存的大小可以调节

堆内存三区域:新生代,老年代,永久代(元数据区1.8之后)

新生区:类诞生和成长,甚至死亡的地方;

  • 伊甸区(Eden):所有对象都在此区被new出来
  • 幸存区(Survivor)(0(From),1(To)):

永久代是对方法区的一种实现

  • 1.6之前:永久代,运行时常量池在方法区;
  • 1.7:永久代,常量池在堆(开始去永久代)
  • 1.8:元空间(Metaspace)取代永久代

另外还需要注意的是在HotSpot虚拟机中永久带和堆虽然相互隔离,但是他们的物理内存是连续的,即永久代使用JVM分配的内存。而且老年代和永久带的垃圾收集器进行了捆绑,因此无论谁满了都会触发永久带和老年的GC。

元空间在1.8中不在与堆是连续的物理内存,而是改为使用本地内存(Native memory)。元空间使用本地内存也就意味着只要本地内存足够,就不会出现OOM的错误。

在这里插入图片描述

9. GC(Garbage Collection)

主要作用域:新生代,永久代

OOM:堆内存溢出

https://blog.csdn.net/laomo_bible/article/details/83112622

9.1 GC的对象

需要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活常用的有两种办法:引用计数和可达性分析。

(1)引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

(2)可达性分析:(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

在Java语言中,GC Roots包括:

  • 虚拟机栈中引用的对象。

  • 方法区中类静态属性实体引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈中JNI引用的对象。

9.2 GC机制

GC又分为 minor GC 和 Full GC (也称为 Major GC )

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:

a.调用System.gc时,系统建议执行Full GC,但是不必然执行

b.老年代空间不足

c.方法区空间不足

d.通过Minor GC后进入老年代的平均大小大于老年代的可用内存

e.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

9.3 GC算法

GC常用算法有:标记-清除算法标记-压缩算法复制算法分代收集算法。

目前主流的JVM(HotSpot)采用的是分代收集算法。

9.3.1 标记-清除算法

为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

优点:
最大的优点是,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。此外,更重要的是,这个算法并不移动对象的位置。

缺点:
它的缺点就是效率比较低(递归与全堆对象遍历)。每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。没有移动对象,导致可能出现很多碎片空间无法利用的情况。

在这里插入图片描述

9.3.2 标记-压缩算法(标记-整理)

标记-压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。

优点:
该算法不会像标记-清除算法那样产生大量的碎片空间。

缺点:
如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
在这里插入图片描述

9.3.3 复制算法

该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。

注意:
这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。

优点

实现简单;不产生内存碎片

缺点
每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。
在这里插入图片描述

9.3.4 分代收集算法

现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。

具体过程:新生代(Young)分为Eden区,From区与To区
在这里插入图片描述

当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。一般来说这时候不是所有的
对象都没用了,所以就会把还能用的对象复制到From区。

在这里插入图片描述

这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区

在这里插入图片描述

再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区
在这里插入图片描述

经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。

在这里插入图片描述

老年代经过这么几次折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除(Full GC),也就是全量回收。如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。

9.4 GC回收器
9.4.1. Serial收集器(串行收集器)

串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收
-XX:+UseSerialGC

  • 新生代、老年代使用串行回收
  • 新生代复制算法
  • 老年代标记-压缩

在这里插入图片描述

9.4.2. 并行收集器

ParNew

-XX:+UseParNewGC(new代表新生代,所以适用于新生代)

  • 新生代并行
  • 老年代串行

Serial收集器新生代的并行版本
在新生代回收时使用复制算法
多线程,需要多核支持
-XX:ParallelGCThreads 限制线程数量

Parallel收集器

  • 新生代复制算法
  • 老年代标记-压缩
  • 更加关注吞吐量

-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行

-XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行

在这里插入图片描述

9.4.3. CMS收集器
  • Concurrent Mark Sweep 并发标记清除(应用程序线程和GC线程交替执行)
  • 使用标记-清除算法
  • 并发阶段会降低吞吐量(停顿时间减少,吞吐量降低)
  • 老年代收集器(新生代使用ParNew)
  • -XX:+UseConcMarkSweepGC

CMS运行过程比较复杂,着重实现了标记的过程,可分为

  1. 初始标记(会产生全局停顿)

根可以直接关联到的对象
速度快
2. 并发标记(和用户线程一起)

主要标记过程,标记全部对象
3. 重新标记 (会产生全局停顿)

由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
4. 并发清除(和用户线程一起)

在这里插入图片描述

-XX:MaxGCPauseMills

  • 最大停顿时间,单位毫秒
  • GC尽力保证回收时间不超过设定值

-XX:GCTimeRatio

  • 0-100的取值范围
  • 垃圾收集时间占总时间的比
  • 默认99,即最大允许1%时间做GC
9.5 堆内存调优
  • 1.尝试扩大堆内存,看结果
  • 2.分析结果,用分析工具
  1. -XX:+PrintGCDetails

  2. -Xmx2g:JVM最大的堆大小为2g,Xmx默认是物理内存的1/4但小于1G;

  3. -Xms2g:JVM启动初始化堆大小为2g,Xms的默认是物理内存的1/64但小于1G。

    将-Xms和-Xmx的值配置为一样,可以避免每次垃圾回收完成后对JVM堆大小进行重新的调整。

  4. -Xmn512M:堆中的新生代大小为512M

  5. -Xss128K:每个线程的堆栈大小为128K

  6. -XX:PermSize=128M:JVM持久代的初始化大小为128M

  7. -XX:MaxPermSize=128M:JVM持久代的最大大小为128M

  8. -XX:NewRatio=4:JVM堆的新生代和老年代的大小比例为1:4

  9. -XX:SurvivorRatio=4:新生代Surivor区(新生代有2个Surivor区)和Eden区的比例为2:4

  10. -XX:MaxTenuringThreshold=1:新生代的对象经过几次垃圾回收后(如果还存活),进入老年代。如果该参数设置为0,这表示新生代的对象在垃圾回收后,不进入survivor区,直接进入老年代

  11. -XX:+UseParallelGC -XX:ParallelGCThread=4

  12. -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

  13. -XX:+UseParallelGC:使用并行的垃圾收集器,但仅针对新生代有效,老年代仍然使用串行收集器

  14. -XX:ParallelGCThread=4:设置并行垃圾回收器的线程为4个,该设置最好与处理器的数目相同

  15. -XX:+UseParalleOldGC:配置老年代使用并行垃圾收集器,JDK1.6支持老年代使用并行收集器

  16. -XX:MaxGCPauseMillis=100:设置每次新生代每次收集器垃圾回收的最长时间,如果无法满足该时间,JVM会自动调整新生代区的大小,以满足该值

  17. -XX:+UseAdaptiveSizePolicy:设置此值后,JVM会自动调整新生代大小以及相应的surivor区的比例,以达到设置的最低响应时间或者收集频率等

  18. -XX:UseConcMarkSweepGC

  19. -XX:+UseParNewGC

  20. -XX:CMSFullGCsBeforeCompaction=5

  21. -XX:+UseCMSCompactAtFullCollection

  22. -XX:UseConcMarkSweepGC:设置JVM堆的老年代使用CMS并发收集器,设置该参数后,-XX:NewRatio参数失效,但-Xmn参数依然有效

  23. -XX:UseParNewGC:设置新生代使用并发收集器,在JDK1.5以上,JVM会根据系统自动设置

  24. -XX:CMSFullGCsBeforeCompaction=5:设置5才CMSGC后对堆空间进行压缩、整理

  25. -XX:+UseCMSCompactAtFullCollection:打开对老年代的压缩,可能会影响性能,但可以消除堆碎片

9.7 JMM (Java Memory Model)

在Java中,不同线程拥有各自的私有工作内存,当线程需要读取或修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到线程的工作内存变量副本中,当该线程修改其变量副本的值后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值

在这里插入图片描述

9.7.1 volatile

https://blog.csdn.net/vking_wang/article/details/8574376

https://blog.csdn.net/weixin_30342639/article/details/91356608

原理

  1. 规定线程每次修改变量副本后立刻同步到主内存中,用于保证其它线程可以看到自己对变量的修改
  2. 规定线程每次使用变量前,先从主内存中刷新最新的值到工作内存,用于保证能看见其它线程对变量修改的最新值
  3. 为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障防止指令重排序

注意:

  1. volatile只能保证基本类型变量的内存可见性,对于引用类型,无法保证引用所指向的实际对象内部数据的内存可见性。关于引用变量类型详见:Java的数据类型
  2. volilate只能保证共享对象的可见性,不能保证原子性:假设两个线程同时在做x++,在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量,所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是2。
9.7.2 volital内存语义与实现
  • 一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
  • 一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
是否能重排序第二个操作
第一个操作普通读/写volatile读volatile写
普通读/写NO
volatile读NONONO
volatile写NONO

当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volate读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

9.7.3 三大点
  1. 内存可见性(MESI缓存一致性协议)

    可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。

    状态描述
    M 修改(Modified)此时缓存行中的数据与主内存中的数据不一致,数据只存在于本工作内存中。其他线程从主内存中读取共享变量值的操作会被延迟执行,直到该缓存行将数据写回到主内存后
    E 独享(Exclusive)此时缓存行中的数据与主内存中的数据一致,数据只存在于本工作内存中。此时会监听其他线程读主内存中共享变量的操作,如果发生,该缓存行需要变成共享状态
    S 共享(Shared)此时缓存行中的数据与主内存中的数据一致,数据存在于很多工作内存中。此时会监听其他线程使该缓存行无效的请求,如果发生,该缓存行需要变成无效状态
    I 无效(Invalid)此时该缓存行无效
  2. 禁止指令重排序(内存屏障)

    屏障点描述
    每个volatile写的面插入一个store-store屏障禁止上面的普通写和下面的volatile写重排序
    每个volatile写的面插入一个store-load屏障禁止上面的volatile写与下面的volatile读/写重排序
    每个volatile读的面插入一个load-load屏障禁止上面的volatile读和下面的普通读重排序
    每个volatile读的面插入一个load-store屏障禁止上面的volatile读和下面的普通写重排序
  3. 不保证原子性

尽管volatile关键字可以保证内存可见性和有序性,但不能保证原子性。也就是说,对volatile修饰的变量进行的操作,不保证多线程安全。

如对i赋值:i++

  • 1.首先获取变量i的值
  • 2.将该变量的值+1
  • 3.将该变量的值写回到对应的主内存中
9.7.4 synchronized关键字

synchronized 可以保障原子性和可见性。因为 synchronized 无论是同步的方法还是同步的代码块,都会先把主内存的数据拷贝到工作内存中,同步代码块结束,会把工作内存中的数据更新到主内存中,这样主内存中的数据一定是最新的。更重要的是禁用了乱序重组以及保证了值对存储器的写入,这样就可以保证可见性。

解决的问题

现在可以多个线程对同一片存储空间进行访问,这时存储空间里面的数据叫做共享数据。线程并发给我们带来效率的同时,也带了一些数据安全性的问题,数据安全性是一个很严重的问题,多个线程同时访问同一片数据区,很有可能把里面的数据弄的混乱。 所以Java语言提供了专门机制以解决这种数据安全性问题,有效避免了同一个数据对象被多个线程同时访问,从而导致数据的错乱的问题。

synchronized关键字可以作为函数的修饰符(也就是常说的同步方法)

synchronized关键字可以作为函数内的语句(也就是常说的同步代码块)

在这里插入图片描述

synchronized它的作用域默认是当前对象,这时锁就是对象。防止多个线程同时访问这个对象的synchronized方法,如果一个对象有多个synchronized方法,只要一个线程访问了其中的某一个synchronized方法,其它线程就不能同时访问这个对象中其他任何一个synchronized方法了。不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。

在这里插入图片描述

锁对象:synchronized (this),中的this代表着当前对象。

锁class:可以防止多个线程同时访问这个类所创建的对象中的synchronized方法。它可以对这个类创建的所有对象实例起作用。锁class只需要将上面代码中的this,换成“类名.class”就行了

9.7.5 synchronized和voiatile的比较

a)volatile不需要加锁,比synchronized更轻便,不会阻塞线程

b)从内存可见性的角度来讲,volatile的读相当于加锁,volatile的写相当于解锁

c)synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值