JVM 架构解释 + 垃圾回收机制 详解(基于JDK8版本)

1. JVM 内存结构


JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。
在这里插入图片描述

程序计数器(PC寄存器):

由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,

因此,为了能够使得每个线程都在线程切换后能够恢复在切 换 之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,

否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,

因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

Java虚拟机栈(Java Virtual Machine Stacks):

Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)

指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)

方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。

在这里插入图片描述

本地方法栈:

与上面不同的是Java栈是给Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。

堆:

Java中的堆是用来存储对象本身的以及数组(数组引用是存放在Java栈中的)。堆是被所有线程共享的,在JVM中只有一个堆。

方法区:

与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

2. JVM 之 堆


堆空间结构图:
在这里插入图片描述
(注意:在JDK1.8版本废弃了永久代)


整个堆空间分成两半:

  • 一半:新生代(包括:伊甸园 + survivor(S0 + S1)幸存者)。
  • 另一半:老年代。

伊甸园:一开始加载或者创建新对象都存到伊甸园中。

S0和S1:存放GC清理后,幸存下来的对象。

老年代:经过几轮清理存下来的对象,就会放到老年代中。

3. JVM 之 垃圾回收器(GC, Garbage Collector)

3.1 垃圾回收器 分类 + 组合方式


垃圾回收(GC,Garbage Collection),垃圾回收器(GC, Garbage Collector)。

组合方式就是:年轻代和老年代的组合方式。

垃圾回收器几种组合方式:

  • 新生代(New Generation )、老生代(tenured generation)和永生代(Perm Generation,jdk8已经废弃)。
    在这里插入图片描述
其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案
(红色虚线)在jdk8时将这两个组合声明为废弃,并在jdk9中完全取消
(绿色虚线)在jdk14中废弃
(绿色虚线)jdk14中,删除CMS垃圾收集器

3.2 年轻代的 垃圾处理器


Serial GC:

  • 新生代的垃圾回收器,收集工作是单线程的,基于复制算法的。
  • 参数命令:-XX:+UseSerialGC。

Paralel Scavenge GC:

  • 新生代的垃圾回收器,并行收集的多线程收集器,采用的是复制算法。
  • 通过设置参数命令,达到可控制吞吐量(Thoughput ,CPU 用于运行用户代码的时间/CPU总消耗时间)ParallelScavenge 是可以保证新生代的吞吐量优先,但是不能保证整体吞吐量。。
  • 参数命令:-XX:+UseParallelGC。
    在这里插入图片描述

ParNew GC:(默认的新生地啊垃圾回收器)

  • 新生代的垃圾回收器,Serial 收集器的多线程版本,采用的也是复制算法。可通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。
  • 命令参数:-XX:+UseParNewGC。

3.3 老年代的 垃圾处理器


Serial Old GC:

  • 老年代的Serial 垃圾收集器,单线程串行的收集器,是基于标记-整理算法。
  • -XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。

Parallel Old GC:

  • 老年代的垃圾收集器,多线程并发的收集器,基于标记整理算法。
  • Parallel Old 是为了在老年代同样提供吞吐量优先的垃圾回收器。
  • 参数命令:-XX:+UseParallelOldGC。

CMS GC:

  • 老年代的垃圾收集器,多线程并发的收集器,基于标记清除算法。
  • 参数命令:-XX:+UseConcMarkSweepGC。

3.4 G1 GC 垃圾收集器(jdk9 默认)


G1 GC:

  • 青年代和老年代的垃圾收集器,多线程并发,并行的收集器,基于复制算法,标记-整理 算法。
  • 参数命令:-XX:+UseG1GC。

4. JVM 之 垃圾回收算法

4.1 标记-清除 算法(Mark-Sweep)


标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。
在这里插入图片描述
标记-清除 缺点:

  • 存在内存碎片,就像上面存活的对象分布不均的占据着,每个空间都有大小不一的内存间隙,如果下次存一个比较字节大一点的对象,就没有能够容纳它的了。
  • 效率低,标记和清除,需要扫描整个内存空间,逐个释放内存。

4.2 复制算法


Java堆中新生代的垃圾回收算法。

复制算法是标记-复制算法的简称,将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉。
在这里插入图片描述

特点:

  • 解决了内存碎片化的问题。
  • 空间利用率低,损失了部分系统内存,总要腾出一部分内存用于复制。

4.3 标记-压缩 算法(又名标记-整理 算法)


Java堆中老年代的垃圾回收算法。

标记-压缩 算法:就是先将标记清除的对象清除,之后再将其他存活的对象整理压缩到内存的一端,此时可以直接清除边界处的内存。
在这里插入图片描述

特点:

  • 不会有内存碎片,也提升了空间利用率。
  • 效率低,不仅要清除扫描标记的对象,还多了一步整理。

4.4 分代收集 算法


一般虚拟机可都是采用分代收集算法:

  • 就是上面几个算法的整合,在java堆中分为新生代和老年代,根据场景不同,让其分别采用不同的垃圾回收算法。

新生代一般采用:复制算法。

  • 新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

老年代一般采用:标记-整理算法。

  • 老年代中,因为对象存活率较高,采用标记-清理、标记-整理算法来进行回收。

5. JVM 之 垃圾回收机制过程


整个垃圾回收机制的过程:

  • Minor GC : 清理年轻代。
  • Major GC : 清理老年代。
  • Full GC : 清理整个堆空间,包括年轻代和永久代。
  • 所有GC都会停止应用所有线程。

新生成的对象首先放到年轻代Eden(伊甸园)区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。


jdk命令:jstat [-options参数] 进程号 [多少毫秒显示一次]

  • jstat命令:监控JVM中的类加载,内存,GC,JIT编辑等运行数据(堆中的一些情况)。
  • options参数:
Options 参数如下:
-gc:统计 jdk gc时 heap信息,以使用空间字节数表示
-gcutil:统计 gc时, heap情况,以使用空间的百分比表示
-class:统计 class loader行为信息
-compile:统计编译行为信息
-gccapacity:统计不同 generations(新生代,老年代,持久代)的 heap容量情况
-gccause:统计引起 gc的事件
-gcnew:统计 gc时,新生代的情况
-gcnewcapacity:统计 gc时,新生代 heap容量
-gcold:统计 gc时,老年代的情况
-gcoldcapacity:统计 gc时,老年代 heap容量
-gcpermcapacity:统计 gc时, permanent区 heap容量
  • 执行命令后,效果图:

在这里插入图片描述

S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
单位:KB

6. 设置堆 结构大小


演示一个堆溢出的效果:

package com.itholmes;

import java.util.ArrayList;

public class MyGC {
    
    
	
	byte[] b = new byte[1024*1024*2];//相当于一个对象2M大小
	
	public static void main(String[] args) throws Exception{
    
    
		
		ArrayList<Object> list = new ArrayList<>();
		
		//睡眠20s
		Thread.sleep(20000);
		
		for(int i = 0;;i++) {
    
    
			Thread.sleep(20);
			list.add(new MyGC());
			System.out.println(i);
			if(i % 20 == 0) {
    
    
				list = null;
				list = new ArrayList<>();
			}
		}
		
	}
}

我们在对象中,创建一个2兆大小的数组,这样一个对象大约就占2M。不断的向一个list中添加,并且到一定时刻清空list(设置list为null,触发垃圾回收机制)。


设置堆结构大小:

右键 -》 run as -》 run configurations如下图:
在这里插入图片描述
(在eclipse当中,设置命令参数;正常代码执行,也可以直接通过命令java -Xmx36m等等,这种方式来执行。)

-Xmx:最大堆大小。-Xmx36m就是最大堆36M。

-Xms:初始堆大小。-Xms16m就是最小堆16M

这样堆的大小就会在最大堆和最小堆之间来回变化,有一定的延展性。上面是我们写死了,最大最小都是36M方便测试。

-Xmn:年轻代大小。-Xmn16m就是年轻代16M

-XX:SurvivorRatio:eden(伊甸园) :(S0+S1) 的比例。S0和S1的大小相同。例如:年轻代大小16M,如果 -XX:SurvivorRatio=2 那么伊甸园:S0:S1 就是 8:4:4 的比例。


按照这个逻辑推算,那么上面的代码执行就会造成堆溢出的效果。

7. jconsole命令


jconsole 查看java进程的一个可视化工具。

我们新建连接,选择对应java进程后,就能看到相关信息:
在这里插入图片描述

与上面的jvm 参数命令配置,一个设置一个查看:
在这里插入图片描述

8. jps 命令


很多人认为这个命令是linux系统自带的,其实是jvm命令,查看当前java进程。

9. jvisualvm 命令


和jconsole命令差不多,比jconsole显示的命令更加详细一些,还可以远程连接。

  • 还可以远程监控一些进程的情况,默认jvisualvm不能监控远程的JVM进程,需要进行一些配置。
    在这里插入图片描述

对于tomcat想要远程监控,操作如下:在这里插入图片描述

配置bin目录下的catalina.sh/bat文件:
在这里插入图片描述
在这里插入图片描述

10. jinfo 命令


jinfo [ option ] pid

查看某个java进程的详细信息。
在这里插入图片描述

11. jmap 命令


jmap [options] pid

  • 没有指定参数,会显示java虚拟机进程的内存映像信息。
    在这里插入图片描述
  • -heap,显示Java堆的相关信息。
  • -histo,显示Java堆中对象的统计信息。
  • 还有其他命令。

12. jstack 命令


jstack [ option ] vmid

  • 使用jstack查看线程堆栈(部分结果)。

猜你喜欢

转载自blog.csdn.net/IT_Holmes/article/details/125433386