JVM相关

JVM

类装载器

作用:负责加载class文件(java文件运行产生的),类装载器会将class文件的字节码内容加载到内存中,并将这些内容转换成方法区的一个类模板.类装载器只负责class文件的加载,是否可以运行需要由执行引擎决定.

ps:在加载之前首先会判断class文件是否能被加载到内存中,判断标准就是根据文件开头的特定表示(cafe babe)

类装载器种类

虚拟机自带的加载器

  1. 启动类加载器(Bootstrap):这个类加载器是用C++进行编写的是虚拟机的一部分,用来加载 Java 的核心类(JVM自身需要的类),由于涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,比如Object等类都是使用此加载器,在JVM启动后将这些类进行载入,用此加载器加载的多是java刚开始建立时考虑到运行时所必须的一些类,保证java能够正常运行.

  2. 扩展类加载器(Extension):它负责加载JRE的扩展目录lib/ext 目录下的classes或.jar中是否有指定的类别并加载,父类加载器为null.

  3. 系统类加载器(System):它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

用户自定义的加载器:Java.lang.ClassLoader的子类,用户可以定制类的加载方式.

双亲委派机制:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给自己的父类加载器去执行,如果父类加载器存在父类则进一步向上进行委托,依次递归,最终到达顶层启动类加载器。 如果父类加载器可以完成类加载任务,就成功返回。反之,子加载器才会尝试自己去加载,这就是双亲委派模式。

沙箱安全机制:沙箱安全机制是由基于双亲委派机制上 采取的一种JVM的自我保护机制,假设你要写一个java.lang.String 的类,由于双亲委派机制的原理,此请求会先交给Bootstrap试图进行加载,但是Bootstrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏.

image-20210607085956755

Java本地接口

native:Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能,使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,例如Socket通信、web service(微服务,restful接口)。

本地方法栈:他的具体做法是Native Method Stack中登记为native方法,在Execution Engine执行的时候加载本地方法库。

程序计数器(PC寄存器)

定义:记录方法之间的调用和执行情况,每个线程都有一个私有的程序计数器,是一个指针,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

  1. 如果执行的是一个native方法那么这个计数器是空的。

  2. 用来完成分支、循环、跳转、异常处理、线程恢复等基础功能,不会发生内存溢出(OutOfMemory = OOM)错误

方法区:供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时候的常量池、字段和方法数据、构造函数和普通方法的字节码内容。

定义:栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,随着线程结束而结束(内存释放),栈内不存在垃圾回收问题,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。

栈中存储的东西

  1. 本地变量:输入、输出参数以及方法内的变量

  2. 栈操作:记录出栈、入栈的操作

  3. 栈帧(Java方法)数据:包括类文件、方法等等

    ps:方法执行的时候会创建一个栈帧

定义:一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件之后,要把类、方法、变量放到堆内存中,保存所有引用类型的真实信息,方便执行器执行。

组成

逻辑上:新生+养老+永久(java7)或者元空间(java8)

物理上:新生+养老

image-20210608095328012

堆的基础路线

image-20210608100251810

image-20210608112111434

具体过程:

From:幸存者0区 To:幸存者1区

  1. 启动GC,存活的对象年龄+1

    当Eden区满时会触发第一次GC,把Eden区内还存活的对象拷贝到From区,存活的对象年龄增加1,当Eden区再次触发GC时,会扫描Eden和From区,对这两个区进行GC,这两个区内存活的对象被复制到To区,同时把存活的对象年龄增加1(当年龄达到了老年的标准则可进入老年代区,默认为15)。

  2. 清空Eden、From

    清空Eden和From中的对象,即复制之后有交换,谁空谁是To

  3. From和To交换

    From和To交换,原来的To成为下一次GC的From区,部分对象会在From和To区域中来回复制,交换15次后就可以进入老年代。

堆参数调优

image-20210613172918267

堆的结构

  1. 逻辑:新生区+老年区+永久区(1.7)或(元空间)

  2. 物理:新生区+老年区

堆参数调优

  1. -Xms:JVM堆所占内存的初始化大小,默认为物理内存的1/64。

  2. -Xmx:JVM堆可以用的最大内存,默认为物理内存的1/4。

  3. -Xmn:新生区的大小(一般不调)。

OOM(OutOfMemoryError):存放的数据超出了堆空间设置的最大数据。

 

面试题补充

垃圾回收会出现在永久代吗?

永久代用于存放静态文件,如Java类、方法等,垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。

ps: Full GC:清理整个堆空间—包括年轻代和老年代和永久代,因为Full GC是清理整个堆空间所以Full GC执行速度非常慢,在Java开发中最好保证少触发Full GC.

如何判断对象是否可以被回收?

引用计数法:每个对象都有一个计数属性,当新增一个引用时属性值加1,释放一个引用时值减1,值为0时可以被回收,但是此方法无法解决循环引用的问题.(循环引用问题:即对象A引用对象B,对象B引用对象A,此时A和B的值均不为0,但是这两个对象没有其它引用)

可达性分析法:该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

四种引用?

强引用:在GC时永远不会被回收.当内存空间不足时,系统宁愿抛出OOM错误也不会随意回收强引用的对象来解决内存不足的问题.

软引用:在GC时,如果内存不足那么就会被引用.只要垃圾回收器没有回收它,该对象就可以被程序使用.

弱引用:无论内存是否充足,只要进行GC就会被回收.

虚引用:如果一个对象仅有一个虚引用那么就和没有引用一样,在任何情况下都能被回收.

垃圾回收算法?

标记-清除算法

标记-清除算法:收集器从根节点开始遍历,然后在对象的Header中记录为可达对象,标记完成后然后对堆内存从头到尾进行线性的遍历,如果发现某个对象的Header中没有标记为可达对象那么就将其回收.(这里的清除并不是真正的置空,而是把需要清除的对象地址保存到空闲地址列表里,需要此空间时直接将此部分数据覆盖即可)

缺点:

  1. 效率不高

  2. 在进行GC时需要将整个应用程序停止,用户体验感差

  3. 清除后的空闲内存是不连续的,会产生内存碎片,另外还需要维护一个空闲列表

复制算法

复制算法:为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

优点:

  1. 没有标记和清除过程,实现简单且运行高效

  2. 复制过去后存在连续的空间里,不会出现"碎片"问题

缺点:

  1. 需要两倍的内存空间

  2. 不仅仅是内存之间的复制,而且还要对栈之间的引用关系进行维护

ps:当内存中大部分都是存活对象时使用复制算法的效率很低,因为在这个过程中不仅需要对大量对象进行复制,还要修改对应引用,而且清除出的内存很小,所以复制算法适用于存活对象比较少垃圾对象比较多的情况下,在新生区中死亡的对象率很高,所以此算法在新生区内较适用,老年代中大多数对象都是存活的所以不适用此算法.

标记-压缩算法

标记-压缩算法:根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

优点:

  1. 消除了标记-清除算法中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要维持一个内存的起始地址即可

  2. 消除了复制算法中,需要双倍内存的代价

缺点:

  1. 效率低

  2. 移动对象时如果对象被其它对象所引用那么还需要调整引用地址

  3. 移动过程中需要暂停用户的应用程序

image-20210615113642998

分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

垃圾回收器?

垃圾回收器的分类方法

  1. 按照工作模式可以分为并发式垃圾回收器和独占式垃圾回收器

    • 并发式垃圾回收器与应用线程交替工作,能够减少应用程序的停顿时间,让用户感觉更流畅

    • 独占式垃圾回收器一旦运行,那么就会停止应用程序中的用户线程,直到垃圾回收过程完全结束为止,用户操作的过程中可能会感到卡顿.

  2. 按照碎片处理方式可以分为压缩式垃圾回收器和非压缩式垃圾回收器

    • 压缩式垃圾回收器在回收完成后会对存活对象进行压缩整理,消除回收后的碎片

    • 非压缩式的则不会对空间进行整理,存在大量的空间碎片

  3. 按工作的内存区间可以分为年轻代垃圾回收器和老年代垃圾回收器

GC评估指标

image-20210615212707762

这些指标中吞吐量、暂停时间和内存占用是比较重要的三个指标,并且这三者不能同时兼得,随着硬件的发展内存占用的容忍度增加,目前矛盾较大的两个指标是吞吐量和暂停时间.

7种经典的垃圾收集器

image-20210615215108369

垃圾回收器与分代之间的关系

image-20210615215459944

垃圾收集器的组合关系

image-20210615220056633

  • 两个收集器间有连线,表明它们可以搭配使用: Serial/Serial Old、Serial/CMS、 ParNew/Serial Old、ParNew/CMS、 Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;

  • 其中Serial Old作为CMS 出现"Concurrent Mode Failure"失败的后备预案。

  • (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、 ParNew+Serial Old这两个组合声明为废弃(JEP 173) ,并在JDK 9中完全取消了这些组合的支持(JEP214),即:移除。

  • (绿色虚线)JDK 14中:弃用Parallel Scavenge和SerialOld GC组合(JEP366 )

  • (青色虚线)JDK 14中:删除CMS垃圾回收器 (JEP 363)

JVM Server和Client模式区别

Client:使用的是一个代号为C1的轻量级编译器,启动速度较快.

Server:模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,启动速度相对慢,但是服务起来之后,性能更高.

Serial回收器:串行回收(单线程/复制算法)

image-20210616125957071

  1. 开启收集器的参数:-XX:+UseSerialGC,在新生区开启此回收器之后,老年区默认开启Serial Old收集器

  2. Serial 是一个单线程的收集器, 它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。

  3. Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程(STW机制),但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。

ParNew垃圾回收器(多线程/复制算法):Serial的多线程版,Java10已经废弃了

  1. 开启收集器的参数-XX:+UseParNewGC,打开该开关后,使用ParNew(年轻代)+Serial Old(老年代)组合进行GC。另外,ParNew是CMS收集器的默认年轻代收集器。

  2. ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样, ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。

  3. ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。

  4. ParNew 虽然是除了多线程外和Serial 收集器几乎完全一样,但是ParNew垃圾收集器是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器

Parallel Scavenge 收集器(多线程/复制算法):吞吐量优先的收集器

image-20210616132523074

  1. 开启收集器的参数:-XX:+UserParallelGC或-XX:+UseParallelOldGC,选择一个参数将一组收集器打开.

  2. Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,是一个多线程的垃圾收集器

  3. 它重点关注的是程序达到一个可控制的吞吐量(Thoughput, CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。

  4. 自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别,可以根据当前系统的情况动态的调整参数提供最合适的停顿时间和最大吞吐量。

SerialOld收集器(单线程/标记整理算法)

Serial Old作为老年代的垃圾收集器,同样具有串行回收和"Stop-the-world"机制,但是回收算法采用标记-压缩算法. 这个收集器也主要是运行在 Client 默认的java 虚拟机默认的年老代垃圾收集器。在 Server 模式下,主要有两个用途:

  1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。

  2. 作为年老代中使用 CMS 收集器的后备垃圾收集方案。

Parallel Old 收集器(多线程/标记整理算法)

Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量, Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器, 如果系统对吞吐量要求比较高,可以优先考虑新生代Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略

CMS 收集器(多线程/标记清除算法):低延迟,高响应速度

image-20210616113600606

ps:只有在全黑色箭头部分进行短暂停顿,延迟低,黑灰色箭头共存的地方表示用户进程和收集进程可以同时执行

  1. 开启收集器的参数:-XX:+UseConcMarkSweepGC,开启ParNew收集器也会自动打开,新生代和老年代使用ParNew/CMS的组合,此时Serial收集器将作为CMS收集器错误时的备用

  2. Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间, 和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂。整个过程分为以下 4 个阶段:

    • 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

    • 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。

    • 重新标记:二次确认的过程,之前标记国的对象可能又被重新启用了,在正式清理前需要做一次修正,仍然需要暂停所有的工作线程。

    • 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作, 所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

优点:并发收集停顿少

缺点:

  1. 并发执行对CPU资源占用多,CMS必须要在老年代堆内存用完之前完成垃圾回收,否则会失败,那么会启用备用机制以STW的方式进行GC,造成较大的停顿.

  2. 标记清除算法造成大量碎片

 

G1收集器(多线程/标记整理算法)

Eden、Survivor、Tenured等内存区域不在是连续的,变成了一个个大小一样的region,每个区域都可能随着G1的运行在不同代之间切换

Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器, G1 收集器两个最突出的改进是: 1.基于标记-整理算法,不产生内存碎片。 2.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间, 优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率

-XX:+PrintCommandLineFlags:查看此程序使用的JVM参数

-XX:+UseSerialGC:查看是否使用此GC

-XX:ParallelGCThreads:限制线程数量,默认开启和CPU相同的线程数

收集器总结

单CPU或小内存,单机程序:SerialGC

多CPU,需要最大吞吐量,比如后台计算型应用:ParallelGC+ParallelOldGC

多CPU,追求低停顿时间,需要快速响应互联网应用:ParNewGC+CMS

image-20210617201817800

性能调优?(工具和参数)

image-20210618092536582

相关指令

  • jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。

  • jstat:JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

  • jmap:JVM Memory Map命令用于生成heap dump文件

  • jhat:JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看

  • jstack:用于生成java虚拟机当前时刻的线程快照。

  • jinfo:JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数

调优工具

  • jconsole:在JDK中自带的Java监控和管理控制台,用于对JVM中内存/线程和类等进行监控,是一个基于JMX的GUI监控工具

  • Jvisualvm是一个功能强大的多合一故障诊断和性能监控的可视化工具,继承了多个JDK命令行工具,使用Visual VM可用于显示虚拟机进程及进程的配置和环境信息,监视应用程序的GPU,GC,堆,方法区及线程的信息等,在JDK 6 Update 7以后,Visual VM便作为JDK的一部分发布

  • MAT(Memory Analyzer Tool)一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗

  • GChisto:一款专业分析gc日志的工具

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值