JVM相关知识 (每日更新)

JVM

一、初识JVM

  1. 平时写好的Java项目(.java文件)是怎么运行起来的
    将写好的代码编译成 .class 为后缀的字节码文件
    运行,就启动了一个JVM进程,JVM负责运行 “.class” 这些文件
    通过类加载器找到主方法(main),再根据主方法中用到的类,找到对应的类并执行
    在这里插入图片描述

  2. JVM什么情况会加载一个类
    一个类从加载到使用,经历的过程如下:
    加载-》验证-》准备-》解析-》初始化-》使用-》卸载
    JVM 在 代码中用到某个类的时候 会加载一个类

  3. 验证、准备和初始化过程
    验证:验证加载进来的".class"文件中的内容,是否符合指定的规范
    准备:给相关的类分配一定的内存空间,在给静态变量分配内存,并初始化。 在准备过程中,碰到静态变量初始化时,并不是按照代码进行初始化,而是根据类型赋一个默认值 如:int a=0;
    解析: 将符号引用替换为直接引用的过程
    初始化:真正对静态变量赋值,是在初始化的过程(按照代码进行赋值)

  4. 类初始化的规则:
    触发类的加载到初始化的全过程,把类实例化 或者 包含"main()"方法的主类,必须立马初始化
    初始化一个类的时候,发现他的父类还没有初始化,必须先初始化他的父类

  5. Java 中有哪些类加载器
    启动类加载器(Bootstrap ClassLoader):主要负责加载我们在机器上安装的Java目录下的核心类
    Java安装目录(JDK)下有一个 lib目录,包含Java最核心的一些类库,支撑Java系统的运行
    扩展类加载器(Extension ClassLoader):Java安装目录(JRE)下有一个 lib\ext 目录中的类
    应用程序类加载器(Application ClassLoader):负责加载 系统环境变量中配置的"ClassPath"中指定路径的类,通俗点就是 负责加载写好的Java代码,加载到内存中
    自定义类加载:根据自己的需求去加载类
    双亲委派机制:JVM的类加载器是有亲子层级结构的
    类加载器 -》扩展类加载器-》应用程序类加载器-》自定义类加载器
    先去往该自己的上一层级去加载,加载不到再去找父级下的子级去加载
    在这里插入图片描述

  6. 如何对“.class”文件处理保证不被人拿到以后反编译获取公司源代码?
    可以采用小工具对字节码加密或者做混淆等处理,对于加密的类,可以考虑自定义的类加载器来解密

  7. Tomcat类加载体系:
    启动类加载器 -》扩展类加载器-》应用程序类加载器-》自定义类加载器(Common类加载器、Catalina类加载器、Shared类加载器),用来加载Tomcat自己的核心基础类库。会为每个部署在里面的Web应用提供一个 webApp类加载器,负责加载web应用的类
    jsp类加载器,为每个JSP都准备了一个JSP类加载器
    在这里插入图片描述

  8. JVM内存区域
    程序计数器:记录执行到哪一个代码指令了
    栈:在执行方法时,为每一个方法创建一个栈帧放入栈中,包含方法的局部变量
    堆:创建的对象都在堆中

  9. 创建的对象,在内存中会占用多少空间
    对象自己本身的信息
    对象的实例变量作为数据占用的空间

  10. 类加载的全流程
    在这里插入图片描述
    在这里插入图片描述

二、JVM内存模型

  1. JVM堆内存 分代模型:年轻代、老年代、永久代
    年轻代:创建和使用完之后立马就要回收的对象放在里面
    在这里插入图片描述
    ReplicaManager 是放在年轻代中的

老年代:创建之后需要一直长期存在的对象放在里面
在这里插入图片描述
永久代(之前叫 方法区):存放一些类信息

  1. 可以思考下面这段代码,区分分别哪些放在 堆内存的年轻代,老年代,永久代,栈
    在这里插入图片描述
    实例化ReplicaFetcher对象,放入堆内存的老年代 -》main() 会进入 主线程,先执行 main(),进入栈,-》在继续执行 loadReplicasFromDisk(),在放入栈中,-》实例化 ReplicaManager对象,放在堆内存的年轻代
    在这里插入图片描述

当执行完 loadReplicasFromDisk(),ReplicaManager对象会被回收,且 loadReplicasFromDisk 出栈
在这里插入图片描述

  1. 方法区里面的类会不会被回收?
    满足以下三个条件就会被回收:
    首先该类的所有实例对象都已经从Java堆内存里被回收
    其次加载这个类的ClassLoader已经被回收
    对该类的Class对象没有任何引用

  2. 对象是如何分配内存的?(好奇不,好奇就接着看,不好奇就 say bye bye)
    ◆ 对象优先分配在新生代(年轻代)
    ◆ 新生代如果占用的内存满了,会触发 Minor GC(Young GC)回收掉没有人引用的垃圾对象
    ◆ 如果有对象躲过了15次垃圾回收,就会进入老年代
    ◆ 如果老年代的内存也满了,也会触发垃圾回收,把里面没人引用的垃圾对象清理掉

  3. 每个线程执行方法的时候,那些方法对应的栈帧出栈了,里面的局部变量需要垃圾回收吗?
    JVM里垃圾回收针对的是 新生代,老年代,还有方法区(永久代),不会针对方法的栈帧
    方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉

  4. JVM内存相关的几个参数:
    -Xms:Java堆内存的大小
    -Xmx:Java堆内存的最大大小
    -Xmn:Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小
    -XX:PermSize:永久代大小,jdk1.8之后叫 -XX:MetaspaceSize
    -XX:MaxPermSize:永久代最大大小,jdk1.8之后叫 -XX:MaxMetaspaceSize
    -Xss:每个线程的栈内存大小
    在这里插入图片描述

在IDEA中可以这样设置:

-Xms512M -Xmx512M -Xmn256M -Xss1M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M

在这里插入图片描述
可以通过 visualvm 工具查看
在这里插入图片描述
线上部署系统若通过 "java -jar"的方式,可以这样设置:

java -Xms512M -Xmx512M -Xmn256M -Xss1M -XX:PermSize=128M -XX:MaxPermSize=128M -jar App.jar
  1. 如何合理设置永久代大小?
    一般设置几百MB就够用了(具体情况具体分析)
    主要用来存放一些类的信息

  2. 如何合理设置栈内存大小?
    一般默认就是比如512KB~1MB
    每个线程自己的栈空间,用来存放线程执行方法期间的各种布局变量的

  3. 如果Java程序没有设定以上参数,默认值是多少 (针对JDK1.8)
    -Xms:当前机器最大内存的 1/64
    -Xmx:当前机器最大内存的1/4
    -Xss:设置单个线程栈的大小,一般默认为512k。

对于堆的初始值和最大值,可以通过以下命令查看:

在Windows里:
java -XX:+PrintFlagsFinal -version | findstr /i "HeapSize PermSize ThreadStackSize"
在Linux里:
java -XX:+PrintFlagsFinal -version | grep -iE 'HeapSize|PermSize|ThreadStackSize'

三、JVM垃圾回收

  1. 什么时候会触发垃圾回收
    当 新生代里的对象越来越多,快满时,会触发垃圾回收,将新生代里没有人引用的对象给回收掉
  2. 哪些变量引用的对象是不能被回收的
    JVM 使用了 可达性分析算法,来判断哪些对象是可以被回收的,哪些对象是不能被回收的。意思是 每个对象,都分析一下有谁在引用他,然后一层一层往上去判断,看是否有一个 GC Roots。
    只要你的对象被局部变量、类的静态变量给引用,就不会回收这些对象。
  3. Java中对象不同的引用类型
    引用类型有:
    强引用: 一个变量引用一个对象,只要是强引用的类型,进行垃圾回收的时候不会去回收这个对象
    在这里插入图片描述
    软引用:
    在这里插入图片描述
    一个实力对象被一个软引用类型的对象给包裹起来了 正常情况下,垃圾回收是不会回收软引用对象的,但是在执行垃圾回收后,发现内存空间还是不够存放新的对象,内存都快溢出了,就会将软引用对象回收掉
    弱引用:
    在这里插入图片描述
    和 软引用类似,发生垃圾回收,就会将这个对象回收掉
    虚引用: 后续补充
4. 垃圾回收策略
复制算法(新生代):

原理:
复制算法是针对新生代的。新生代中的内存会分为两块内存区域,只是用其中一块内存,待那块内存快满的时候,就把里面的存活对象一次性转移到另外一块内存区域,保证没有内存碎片,接着一次性回收原来那块内存区域的垃圾对象,再次空出来一片内存区域
缺点:
从一开始就只有一半内存可以用,对内存的使用效率太低
优化:
分为1个Eden区,2个Survivor区,其中Eden占 80%内存空间,每一块Survivor占10%的内存空间

标记整理算法(老年代):

原理:
标记出来老年代当前存活的对象,让存活的对象在内存里进行移动,存活的对象紧凑在一起,避免垃圾回收过后出现过多的碎片,在一次性把垃圾对象都回收掉
缺点:
老年代的垃圾回收算法速度至少比新生代的垃圾回收算法的速度慢 10倍
如果频繁出现老年代的Full GC垃圾回收,会导致系统性能严重被影响,出现频繁卡顿的现象

  1. 新生代中的对象,什么情况下会进入老年代
    第一种:
    新生代中的对象,经历过 15 次垃圾回收,会进入老年代
    具体的可以通过JVM参数 "-XX:MaxTenuringThreshold"来设置,默认是 15次
    第二种:
    动态对象年龄判断。当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,此时,这批对象会进入老年代
    第三种:
    大对象直接进入老年代
    JVM中有一个参数,就是 “:XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如 "1048576"字节,就是1MB。意思就是 创建的对象超过此数值,会直接放入老年代,根本都不会经过新生代
    第四种:
    当新生代触发MinorGC之后,发现剩余的存活对象太多,没办法放入另一块Survivor区,就会将这些对象放入老年代中
  2. 老年代空间分配担保规则
    首先,在执行任何一次Minor GC之前,JVM会检查一下老年代可用的内存空间是否大于新生代所有对象的总大小。
    在这里插入图片描述
    当老年代的内存大小大于新生代所有对象,就可以对新生代进行 Minor GC了,这样即使GC之后所有的对象都存活,新生代中的Survivor区放不下,可以转移到老年代去

当老年代的内存大小小于新生代所有对象占用的空间时,可以根据 “-XX:-HandlePromotionFailure"的参数进行设置,判断老年代的大小,是否大于之前每一次Minor GC 后进入老年代的对象的平均大小
在这里插入图片描述
当”-XX:-HandlePromotionFailure"参数没有设置,就会触发 Full GC,对老年代进行垃圾回收,腾出来一点内存空间,在执行Minor GC。

7. 进行Minor GC 有以下几种可能

a、Minor GC后,剩余的存活对象的大小 < Survivor区的大小的,此时存活对象进入 Survivor区域
b、Minor GC后,剩余的存活对象的大小 > Survivor区的大小的 且 < 老年代可用内存大小,此时存活对象进入老年代即可
c、Minor GC后,剩余的存活对象的大小 > Survivor区的大小的 且 > 老年代可用内存大小,就会触发 Full GC,进行 Full GC的同时,也会对新生代进行垃圾回收。当Full GC过后,还是没有足够的空间存放Minor GC过后的剩余的存活对象,就会导致 内存溢出即 OOM

8. 老年代触发垃圾回收的时机

a、在Minor GC之前进行,因为在 MInor GC之后进入老年代的对象太多,老年代存放不下,就需要触发Full GC,在带着执行 Minor GC
b、老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC
c、新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足
d、如果老年代可用内存大于历次新生代GC后进入老年代的对象平均大小,但是老年代已经使用的内存空间超过了这个参数(“-XX:CMSInitiatingOccupancyFaction”)指定的比例,也会自动触发Full GC

  1. 当后台在执行垃圾回收时,还会创建新的对象吗?
    不会,当在进行垃圾回收时,不允许创建对象。所以就是 Stop the World,我们的代码不在运行

  2. Stop the World造成的系统停顿
    假设我们执行 Minor GC需要花费100ms,也就是我们的系统在这100ms内,不能处理任何请求,当用户在这100ms内发出的请求会出现短暂的卡顿

四、新生代的垃圾回收器

  1. 新生代垃圾回收器 ParNew
    新生代的ParNew 垃圾回收器是线程垃圾回收机制,在执行垃圾回收时,会将系统程序的工作线程池全部停掉,禁止程序继续运行创建新的对象,在用多个垃圾回收线程进行垃圾回收,提高性能
    新生代的Serial垃圾回收器是线程垃圾回收机制
  2. 启动系统的时候如果指定使用ParNew垃圾回收器,使用什么参数?
    使用 “-XX:+UseParNewGC”,这样在JVM启动之后对新生代进行垃圾回收,就是用的PerNew垃圾回收器
  3. ParNew垃圾回收器默认情况下的线程数量
    指定使用ParNew垃圾回收器后,默认给自己设置的垃圾回收线程的数量跟CPU的核数是一样的,并且是并行处理的。如:线上机器使用的是4核CPU/8核CPU,ParNew的垃圾回收线程数是 4个线程、8个线程,依次类推
    在这里插入图片描述
    需要手动调节ParNew的垃圾回收线程数量,使用 "-XX:ParallelGCThreads"参数,也可以设置线程的数量 一般情况下不建议随意设置此参数
  4. 什么情况下使用PerNew,什么情况下使用 Serial
    启动系统的时候是可以区分 服务器模式和客户端模式的,比如启动系统的时候加入 "-server"就是服务器模式,加入 "-client"就是客户端模式
    服务器模式和客户端模式的区别是什么?
    服务器模式通常运行我们的网站系统、电商系统、业务系统、App后台系统等大型系统,一般都是多核CPU,此时新生代进行垃圾回收,使用 ParNew会更好,因为是多线程并行垃圾回收,充分利用多核CPU资源,提升性能
    在这里插入图片描述

客户端模式 比如 微信客户端或者其他客户端,都是运行在Windows个人操作系统上,这种系统大多都是单核CPU,如果继续选用ParNew进行垃圾回收,就会导致一个CPU运行多个线程,增重了性能开销,还会频繁的进行上下文切换,影响效率
在这里插入图片描述
相比之下,选用Serial垃圾回收器,效率可能更高一些
在这里插入图片描述

五、老年代垃圾回收器 CMS

  1. 如果Stop the World然后垃圾回收会如何
    先“Stop the World”,然后再采用“标记-清理”算法去回收垃圾,会停止一切工作线程,然后在去执行“标记-清理”算法,会导致系统卡死时间过长,很多响应无法处理。
    CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理的
  2. CMS如何实现系统一边工作的同时进行垃圾回收?
    CMS在执行一次垃圾回收的过程一共分为4个阶段:
    a、 初始标记
    b.、并发标记
    c.、重新标记
    d、 并发清理

    总结:
    只有初始标记和重新标记是需要“Stop the World”的,速度非常快
    最耗时的是并发标记和并发清理,就是对老年代对相关进行GC Roots追踪,标记出来到底哪些可以回收,然后就是对各种垃圾对象从内存里清理掉
  3. 并发回收垃圾导致CPU资源紧张
    CMS的垃圾回收线程是比较耗费CPU资源的。
    CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4 如:普通的2核4G的机器,垃圾回收线程的数量为: (2+3)/4 =1
    CMS这个并发垃圾回收的机制:第一个问题就是会消耗CPU资源。
  4. Concurrent Mode Failure问题
    浮动垃圾: 在并发清理阶段,CMS只回收之前标记好的垃圾对象,在此阶段,系统一直在运行,可能有对象进入老年代,同时变为垃圾对象(也就是用红圈圈起来的地方) ,此次垃圾回收不会回收这些对象
    在这里插入图片描述
    为了保证在CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代,一般会预留一些空间。CMS垃圾回收的触发时机,其中一个是 当老年代内存占用达到一定比例,就会执行GC。
    可以通过此参数设置 “-XX:CMSInitiatingOccupancyFaction”,用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK1.6里面默认的值是 92%。
    如果CMS垃圾回收期间,系统程序要放入老年代的对象 > 可用内存空间 ,怎么办?
    会发生Concurrent Mode Failure,也就是 并发垃圾回收失败了,此时会用 "Serial Old"垃圾回收器替代CMS,直接强制将系统程序 “Stop the World”,重新进行 GC Root追踪,标记出全部的垃圾对象,不允许创建新对象,一次性把所有的垃圾对象都回收掉,之后在回复系统线程。
    CMS这个并发垃圾回收的机制:第二个问题就是会产生 浮动垃圾
  5. 内存碎片问题
    老年代 CMS 采用 "标记-清理"算法,每次都是标记出来垃圾对象,然后一次性回收掉,会产生大量的内存碎片,碎片过多会触发 Full GC。
    CMS 并不是完全用 "标记-清理"算法,因为太多的碎片实际上会导致更频繁的 Full GC
    参数 “-XX:+UseCMSCompactAtFullCollection” 默认是打开的,意思是 在 Full GC 之后要再次进行 “Stop the World”,停止工作线程,然后进行碎片整理(将存活的对象挪到一起,空出来大片连续内存空间,避免内存碎片)
    在这里插入图片描述

参数是“-XX:CMSFullGCsBeforeCompaction”,默认是0,意思是执行多少次Full GC之后在进行一次内存碎片整理的工作。
在这里插入图片描述

6. ★ 为什么老年代的Full GC 比 新生代的Minor GC慢很多倍,一般在10倍以上?

新生代
因为直接从GC Roots出发就追踪到哪些对象是活的,并将存活对象放入Survivor中,一次性直接回收Eden和之前使用的Survivor了
老年代:
a、在并发标记阶段,需要追踪所有存活对象,追踪的过程比较慢
b、在并发清理阶段,并不是一次性回收一大片内存,而是找到零零散散在各个地方的垃圾对象,速度比较慢。
c、清理完成后,还需要进行一次内存碎片整理,此过程需要"Stop the World"
d、万一并发清理期间,剩余内存空间不足以存放要进入老年代的对象了,引发了“Concurrent Mode Failure”问题,那更是麻烦,还得
立马用“Serial Old”垃圾回收器,“Stop the World”之后慢慢重新来一遍回收的过程,这更是耗时了。

7. ★ 年轻代垃圾回收参数如何优化

在于需要对JVM有限的内存资源进行合理的分配和优化,包括对垃圾回收进行合理的优化,减少JVM的GC次数,尽量避免Full GC
a、尽量避免在 Minor GC后的对象都留在Survivor里,尽量减少进入老年代
b、需要根据具体的业务场景进行设置 新生代对象躲过多少次垃圾回收进入老年代 “-XX:MaxTenuringThreshold”,默认值是 15次。若是需要长期存活的核心业务逻辑组件(@Controller),此时就可以减少这个次数。
c、多大的对象可以直接进入老年代,可以根据自己的系统中有没有创建大对象来决定。一般设置 1MB就可以。具体参数 “-XX:PretenureSizeThreshold=1MB”
d、指定垃圾回收器 新生代使用ParNew("-XX:+UseParNewGC"),老年代使用 CMS("-XX:+ConMarkSweepGC")
ParNew垃圾回收器的核心参数:新生代内存大小,Eden和Survivor的比例,根据系统模型,合理设置 “-XX:MaxTenuringThreshold”,让存活的对象抓紧进入老年代

六、G1 垃圾回收器

  1. G1 垃圾回收器设计思想: 将内存拆分为很多个小的Region,然后新生代和老年代各自对应一些Region,回收的时候尽可能挑选停顿时间最短以及回收对象最多的Region,尽量保证达到我们制定的垃圾回收系统停顿时间。
2. G1 的内存模型和分配规则

内存模型:会将堆内存 / 2048 来决定每个 Region(区域)有多大 例如:4096 / 2048 =2MB
新生代 : 默认新生代对堆内存的占比是 5% ,也就是 200MB,大概是100个 Region,可以通过
“- XX:G1NewSizePercent"来设置新生代初始占比,一般不需要重新设置。 新生代的占比不会超过60% 可以通过 “-XX:G1MaxNewSizePercent"来设置最高占比。
新生代中依然存在 Eden 和 Survivor 的概念,且占比还是 Eden 占 80%,survivor 占 20%
进行垃圾回收的时候跟之前一样,使用的是复制算法,唯一不同的是 G1是可以设定目标GC停顿时间,可以通过 “-XX:MaxGCPauseMills"来设定,默认是 200ms
老年代:占堆内存的40%
新生代中的对象进入老年代的条件: 对象在新生代躲过了很多次的垃圾回收 ;动态年龄判断规则 -》发生新生代GC之后,存活的对象超过 survivor的50%
大对象在 G!回收器中,并不会进入老年代,而是直接进入专门存放大对象的Region中。 因为在G1中,新生代和老年代的Region是不断变化的,当一个大对象特别大时,会横跨几个Region。
3. G1垃圾回收
触发新生代和老年代混合垃圾回收的参数,”-XX:InitiatingHeapOccupancyPercent” ,默认值是 45%。
也就是说老年代占据堆内存的45%的Region的时候,大概是1000个Region的时候,会触发一个混合回收
4. G1垃圾回收的过程
初始标记:会触发Stop the World,简单标记一下GC Roots直接能引用的对象
并发标记(比较耗时):此阶段允许系统程序运行,同时进行GC Roots追踪
最终标记:会触发Stop the World,会根据并发阶段记录的对象进行标记
混合回收(Mixed GC):老年代对堆内存占比达到45%的时候,触发的是 混合回收,此时的回收,不仅仅是回收老年代,还有新生代和大对象
在这里插入图片描述
5. G1垃圾回收器的参数
设置最后一个阶段执行混合回收的次数:”-XX:G1MixedGCCountTarget",默认是 8次
混合回收时,对Region的回收都是基于 复制算法进行的,在进行回收过程会不断空出来新的Region,一旦空闲出来的Region数量达到堆内存的5%,就会立即停止 混合回收。"-XX:G1HeapWastePercent",默认值是 5%
“-XX:G1MixeedGCLiveThresholdPrecent”,默认值是85%,确定回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收
6. 回收失败时的Full GC
在进行Mixed回收的时候,不管是新生代还是老年代,都需要将存活对象拷贝到别的Region里,万一出现拷贝过程中发现没有空闲Region可以承载自己的存活对象,就会触发一次失败。一旦失败,就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出Region。

愿你前程似锦 归来仍是少年!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值