> 文档中心 > 浅析jvm组成部分和垃圾回收机制

浅析jvm组成部分和垃圾回收机制

目录

1.jvm的组成部分和作用

2.jvm的运行时数据区

2.1.方法区

2.1.1 方法区内存

2.1.2 方法区存储信息

2.1.3 常量池

2.2 堆

2.3 程序计数器

2.4 虚拟机栈

2.4.1相关概念

 2.4.2 问题:

2.4.3 栈内存溢出 java.lang.StackOverFlowError

2.5 本地方法栈

2.6 jvm中的五种引用

2.7 垃圾回收算法

2.7.1 标记清除算法

2.7.2 标记整理算法

2.7.3 标记复制算法

2.7.4 分代垃圾回收机制

2.8 垃圾回收器

2.8.1 串行垃圾回收器(Serial+SerialOld)

2.8.2 并行垃圾回收器 

2.8.3 响应时间优先垃圾回收器CMS

2.8.4 G1垃圾回收器


本文主要是自己最近学习jvm做的笔记,里面的内容参考了很多其他大佬的文章,自己主要是总结记录一下;

1.jvm的组成部分和作用

JVM主要由两个系统和两个组件组成。

两个系统指的是类装载器(Class Loader)和执行引擎(Execution Engine);

两个组件指的是运行时数据区(Runtime data area)和本地接口(Native interface)

类加载器的作用是加载类文件到内存,比如编写一个HelloWord.java程序,然后通过javac编译成class字节码文件,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

这里盗用一张比较全的jvm结构图:

2.jvm的运行时数据区

直接盗用一张图:

2.1.方法区

-XX 设置方法区的大小

2.1.1 方法区内存

方法区属于线程共享的内存区域,在jvm启动的时候被创建,jdk1.8之前,方法区的实现叫永久代,使用的是堆内存,容易导致内存溢出,比如spring或者mybatis框架大量使用动态代理加载类,就可能会导致方法区内存溢出;jdk1.8之后呢,是元空间,使用的系统内存,一般系统内存会比较大,不容易溢出;当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

2.1.2 方法区存储信息

方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

2.1.3 常量池

用于存放编译器生成的各种字面常量和符号引用;

1.6及之前,StringTable是放在永久代,也就是方法区,而永久代垃圾回收效率很低,到full GC的时候才会触发永久代的垃圾回收,而full GC会等到老年代的空间不足才会触发;触发时机比较晚,间接导致StringTable的回收效率并不高;我们程序一般会有大量的字符串常量,这样会占用较大内存,从而导致永久代的内存不足;

所以,1.6之后,StringTable直接放在堆里面,触发时机早,易回收;

2.2 堆

java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。

-Xmx,可设置堆内存大小;

如果堆内存不足会抛出异常:OutOfMemoryError:java heap space;

2.3 程序计数器

程序计数器就是记住下一条jvm指令的执行地址,每一条指令被执行的同时会把下一条指令的地址放到程序计数器;物理上通过寄存器实现;

特点:线程私有;

多个线程运行的时候,cpu会给线程分配时间片;线程切换过程中,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,所以每个线程都有自己私有的程序计数器;不会存在内存溢出;

2.4 虚拟机栈

2.4.1相关概念

Xss,可以指定栈大小;

数据结构:子弹夹,先进后出;

虚拟机栈,每个线程运行时需要的运行空间,每个线程需要一个栈,线程私有;

栈由多个栈帧组成,我们的代码都是由一个个方法组成,栈帧就是每个方法运行时需要的内存;

每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口返回地址等信息。

解析栈帧:

局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
返回地址:正常的话就是return 不正常的话就是抛出异常;

主方法也是一个栈帧;方法调用其他方法,那么其他方法也会有一个栈帧入栈;被调用的方法最先执行完,所以会先出栈,也就满足先入后出;

 2.4.2 问题:

1.垃圾回收是否涉及栈内存?

答:不会,因为栈内存主要是方法运行的栈帧,每次方法执行完,都会自动出栈被回收,不需要垃圾回收;

2.栈内存分配越大越好吗?

答:不是,-Xss,可以指定栈大小,一般采用操作系统都有默认的栈大小,内存越大,线程数越少,因为物理内存大小固定,每个线程的栈内存设定过大,线程就越少了;大了,可支持的方法的递归调用越大;

3.方法内的局部变量会有线程安全吗? 答:不会,应为局部变量是线程私有的,始终只有这个线程在操作这个变量;

2.4.3 栈内存溢出 java.lang.StackOverFlowError

1.栈帧过多导致栈内存溢出;不断调用方法入栈,没有出栈,那么就会溢出,比如方法的递归调用,没有设置一个正确的结束条件,就会导致栈内存溢出;

2.栈帧过大也会导致栈内存溢出,局部变量,方法参数等过大,一般不会出现;

2.5 本地方法栈

jvm调用本地方法时调用的内存空间;为本地方法分配的内存空间,都是线程私有的;

本地方法:不是由java代码编写的方法,c,c++编写的方法,需要和操作系统打交道的方法,很多,比如object类里面的clone方法,hashcode方法,都是native修饰,调用本地方法的实现,notify,wart等方法都是;

2.6 jvm中的五种引用

 强引用(StrongReference)

这个就不多说,我们写代码天天在用的就是强引用。如果一个对象拥有强引用,那么垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

软引用(SoftReference)

如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

弱引用(WeakReference)

如果一个对象只具有弱引用,那该类就是可有可无的对象,因为只要该对象被垃圾回收器扫描到了随时都会把它干掉。

虚引用(PhantomReference)

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。

终结器引用

我也不知道是啥;

也会被方法引用队列,用finallize线程去引用队列检查,调用finallize方法释放;

2.7 垃圾回收算法

2.7.1 标记清除算法

根据GC root引用链找,不在就回收,第一阶段:标记没有被引用的对象,第二阶段,清除;

优点:清除数度快,

缺点:会产生很多内存碎片;清除后不会对内存整理,如果来一个比较大的对象,比如数组,需要一块连续空间,所以不能存放,造成内存溢出;

2.7.2 标记整理算法

标记,整理,避免内存碎片问题,将可用内存移动,使其变得更加紧凑;

优点:解决了内存碎片问题;

缺点:由于涉及到对象移动,所以效率较低;

2.7.3 标记复制算法

将内存区划分为大小相等的两块区域,一块叫from,一块叫to;

to这块区域始终空闲着,里面不存任何对象;

首先还是标记,然后从from区域把存活的对象复制到to区域中,复制过程中完成碎片的整理;复制完成后,from区域就全是标记的垃圾;然后一次性清理;然后交换from和to的位置,原来的from变为to,to变成from,这样to又变成空闲的了;

优点:不会产生内存碎片;

缺点:复制算法占用双倍的内存空间;

 实际运用时结合多种算法;

2.7.4 分代垃圾回收机制

将堆内存划分为两块区域,新生代,老年代;代码中长时间需要使用的对象,存活时间长的对象,放入老年代中,而那些用完了就可以丢弃的对象,就放入新生代中;这样,根据不同对象不同生命周期的特点采用不同的垃圾回收算法;老年代垃圾回收很久才发生一次,新生代垃圾回收比较频繁;

新生代有划分为伊旬园(Edem)、幸存区from( From Survivor )、幸存区To ( To Survivor);

新生代垃圾回收: 

我们创建一个新的对象时候,默认会采用伊甸园的内存空间,慢慢的伊甸园的空间会被占满,在往里面放的时候,内存不足,会触发一次垃圾回收Minor GC(Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作,由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快),一般采用标记复制算法;

触发 Minor GC,标记伊甸园和幸存区from区域的对象,复制存活的对象到幸存区To区域,将存活对象的寿命+1,清理已经不在使用的对象,然后交换from和to的位置,这样from就是整理过的存活的对象,to就是空的内存空间;一般当新生代有对象的寿命达到15时,会将对象放到老年代,但是当新生代的整体空间不足,或者直接来一个大对象,大对象会直接方法老年代,新生代即使一些大的寿命没有达到15,也会被放入老年代;

老年代垃圾回收:

当老年代空间不足时,就会触发老年代的垃圾回收,一般采用的是标记清楚或者/标记整理算法;如果回收完还是空间不足,就会报内存溢出异常out of memory error java heap space;

关于垃圾回收时其他线程是否暂停,不同的垃圾回收器不一样;

2.8 垃圾回收器

2.8.1 串行垃圾回收器(Serial+SerialOld)

 显然,串行垃圾回收器在执行行垃圾回收时 ,所有的用户线程必须停止,而且只有一个线程执行回收工作;

串行垃圾回收器分为两个部分:Serial,工作在新生代,采用的垃圾回收算法是复制算法; 

serialold:工作在老年代,采用标记整理算法; 

安全点:因为垃圾回收有对象复制移动,也就是对象的地址发生变化,所以所有线程需要先达到一个安全点;由于是单线程,其他线程需要等待阻塞;

2.8.2 并行垃圾回收器 

吞吐量优先垃圾回收器(吞吐量指单位时间响应的请求数)

UseParallelGC,新生代的垃圾回收,采用复制算法;

UseParallelOldGC,老年代的垃圾回收,采用标记+整理算法;

 显然,并行垃圾回收器在执行垃圾回收时 ,所有的用户线程到达一个安全的后必须停止,由多个垃圾回收线程(默认情况下和cpu核数相关的)去执行垃圾回收;相比于串行垃圾回收,回收效率肯定更高了;

2.8.3 响应时间优先垃圾回收器CMS

UseConcMarkSweepGC 老年代的垃圾回收,基于标记清除算法的垃圾回收器;并发的,垃圾回收器执行的同时,其他用户线程同样可以执行;这样进一步减少了stop the world的时间;当然,某些阶段仍然要stop the world;

UseParNewGC 新生代的垃圾回收,复制算法;

如果并发失败,SerialOld补救,老年代的垃圾回收并发退化到serialold的单线程的垃圾回收器;

工作流程:当老年代发生内存不足,所有线程先到达安全点,cms垃圾回收器就开始工作,先执行初始标记的操作,这个时候需要stop the world,及所有用户线程阻塞,当初始标记结束后,用户线程就可以恢复运行,同时,我们的垃圾回收线程可以并发标记,将剩余的垃圾找出来,并发执行,并发标记后,需要重新标记,需要stop the world,因为并发标记同时,用户线程也在工作,可能产生一些新的对象,标记完了,用户线程可以恢复运行,这是,垃圾回收线程做一次并发的清理;整个过程,只有在初始标记和重新标记阶段会造成 stop the world,这样,响应时间会非常短;

并发线程数受两个参数影响。 ParallelGCThereads=n. 并行垃圾回收线程数,一般和cpu核数相同;

ConcGCThreads=threads 并发的线程数;一般设为并行的四分之一;本例用1个线程垃圾回收,其他线程给用户执行工作;

这种机制对cpu的占用不高;但是用户线程也在运行有可能产生新的垃圾,叫浮动垃圾,需要一些预留空间来存放,就是第三个参数,如果为80,表示老年代空间占用80%的时候执行垃圾回收,这样就会给浮动垃圾留一些预留空间;

cms垃圾回收问题:由于内存碎片太多,造成并发失败,退化为serialold,导致响应时间变长;

2.8.4 G1垃圾回收器

G1会把整个堆内存划分为大小相等的一个个区域region,每个区域都可以独立作为伊甸园E,幸存区S,和老年代O和Humongous;所谓的Humongous,就是一个对象的大小超过了某一个阈值一般是Region的1/2,那么它会被标记为Humongous。每个Region的大小可以通过参数

-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的进行回收大多数情况下都把Humongous Region作为老年代的一部分来进行看待,大对象回收不会采复制算法,代价太高,回收时优先考虑大对象;针对大对象区域的回收,如果没有引用了,在新生代垃圾回收的时候就会被回收;

盗用一张图展示G1垃圾回收器将堆内存划分不同类型的region:

 G1垃圾回收器的三个阶段:

三个阶段:

1.YoungCollection 新生代的垃圾回收同时进行初始标记,

2.YoungCollection+Concurrent Mark,新生代的垃圾收集,同时执行一些并发的标记;

3.MixedCollection,混合收集和最终标记;对新生代和老年代都会执行垃圾收集;

第一个阶段:新生代垃圾回收和初始标记

E代表伊甸园区,伊甸园会设定一个大小,当被占满,触发一次新生代的垃圾回收,会触发stop the world,只是这个时间非常短;

新生代的垃圾回收会采用复制算法将对象放入幸存区s,如图:

 每执行一次垃圾回收,存活对象的年龄就会加1,达到晋升条件的对象会晋升到老年代o;年龄不够的对象,也会复制到另一个幸存区,新生代的垃圾回收和其他垃圾回收器没有多大差别,如图;

 第二阶段:会并发标记(从根对象出发,顺着引用连标记有被引用的对象)

 当老年代占用到整个堆空间的45%时,会进行并发标记;o代表老年代所占区域;

第三阶段:混合收集

新生代回收:E表伊甸园区,会复制到幸存区s,s中不够年龄的也会复制到其他幸存区;一些符合晋升条件的对象会晋升到老年代区;

老年代区:经过之前的并发标记阶段,发现一些需要回收的对象,老年代也是采用复制算法,把还存活下来的对象复制到新的老年代区;由于G1根据最大暂停时间有选择的进行回收,有时堆内存空间太大,老年代垃圾回收时间比较长,就达不到MaxGCPauseMillis=ms的目标,为了达到这个目标,就会回收哪些回收价值比较高的老年代区域,所以只挑一部分区域垃圾回收;这样复制的区域少了,就可以达到暂停时间的目标;目的都是回收垃圾,整理内存,减少内存碎片;

最终标记这个阶段为了在之前并发标记过程中漏掉的一些对象再次标记,因为并发标记的同时,其他线程同时也在工作,可能产生新的垃圾或改变一些对象的引用,这样会对并发标记的结果产生影响;所以在混合收集的阶段,实现暂停,执行一个最终标记,最终标记完成了,就会执行拷贝存活,拷贝过程如上所述;

CMS和G1的最终标记的必要性

cms和G1都有并发标记阶段和最终标记阶段,

最终标记的目的就是解决并发标记的时候,一个对象原本被标记为回收对象,但是用户线程也在执行,用户线程又实用了这个对象,表示该对象又处于强引用状态,如果没有重新标记,就会将该对象给回收掉造成异常;

所以,如果一个对象的引用发生改变,比如原先标记为垃圾,后面又被引用,会将该对象放入一个队列中,当最终标记的时候,会stop the world,暂停用户线程,并对这个队列里面的对象判断是否需要回收;

参考:G1垃圾收集器详解

一篇文章掌握整个JVM,JVM超详细解析!!!