决战圣地玛丽乔亚Day38---JVM相关

发布时间 2023-03-22 21:13:19作者: EmiXXXt

JVM的内存结构:

1.程序计数器:线程私有,保存执行指令地址。

2.java虚拟机栈(线程创建,并存方法调用的相关参数):

每个线程在创建时候都会被分配一个虚拟机栈。当线程调用方法时,会创建一个栈帧,入栈,方法执行完毕栈帧出栈。

栈帧会在调用方法的时候把存局部变量表,操作数栈,动态连接,方法出口等信息存进去,然后压入java虚拟机栈。

3.本地方法栈

其他变成语言的接口。

4.java堆

所有线程共享的一块区域,优化GC主要优化的部分。

用来存一些实例对象(数组/普通对象)。

我们new Entity()的时候会分配一块空间存这个Entity。

 

5.方法区

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

1)类信息:java程序运行,java虚拟机会把类的信息存在方法区。包括类的名称、父类的名称、类的修饰符、类的字段、方法等信息

2)  运行时常量池:方法区的一部分,存一些字面量和符号引用。其中包括字符串常量、类和接口的全限定名、字段和方法的名称和描述符等信息。

3)静态变量:由于静态变量是类级别的变量,因为这个是类的属性不属于对象的属性,所以存方法区。

4)即时编译后的代码

7.直接内存

与Java堆不同,直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。直接内存的分配不受Java堆大小的限制,但是会受到本机总内存大小以及处理器寻址空间的限制

 

常量池的作用?为什么是-128~127?

1.常量池把所有的常量存储在一起,避免了重复分配的问题,如果用到就要重新分配,太浪费内存空间。

2.使用频繁,用常量池避免查找和创建的消耗。

3.动态解析:符号引用是通过类名、方法名、字段名等符号来引用类、方法、字段等元素的。在程序运行时,Java虚拟机需要将这些符号引用解析成实际的内存地址,才能进行方法调用、字段访问等操作

这个过程称为符号引用的动态解析。常量池存符号引用进行动态解析,符合java语言的动态语言特性。

-128~127:

这个范围的数字使用频率高,java虚拟机把这个范围作为常量池缓存而不是每次都重新创建。这个范围是整数常数,像Boolean、String、Double等没有常量池缓存限制。

 

永久代1,.8为什么会被移出到堆外?

之前永久代在java堆内的特殊区域,存元数据,静态变量,常量池等等。但是由于永久代大小有限,经常出现内存溢出。

所以1.8之后把元数据、静态变量、常量池等存储到本地内存(Native Memory),称之为元空间。

元空间相比之前的永久代的好处是:

元空间的大小是动态的,根据策略进行动态调整防止OOM。

避免了java堆的内存碎片化问题,提高内存使用率。

减少了FULL GC,提高程序响应速度和性能。

 

栈和堆:

栈主要存方法调用的东西。

堆主要存对象、数组等动态分配的东西,和类息息相关。对象创建时,java虚拟机在堆分配一个连续的空间并返回引用来存对象,对象不再被引用的时候,会通过垃圾回收算法对其进行回收操作。

栈溢出的情况:

1.递归调用过深。递归自身或其他方法,栈帧被大量创建打满java栈的大小。

2.方法调用层数过多

3.局部变量过多,方法的局部变量过多打满java虚拟机栈大小

 

如何进行垃圾回收:

1).标记阶段:Java虚拟机从根对象(如线程、静态变量等)出发,递归遍历所有可达对象,将它们标记为“存活”。

2).清除阶段:Java虚拟机清除所有未被标记的对象,释放它们所占用的内存空间。

3).整理阶段:Java虚拟机将所有存活对象向一端移动,从而使剩余的内存空间变成一块连续的空间,便于后续的对象分配。

首先,标记阶段有一个问题,如何判断对象是否可达?

引用计数器法:引用计数法是一种简单的判断对象是否可达的方法。它的原理是在每个对象中记录一个引用计数器,每当有一个对象引用它时,引用计数器加1,每当一个对象取消对它的引用时,

引用计数器减1。当某个对象的引用计数器为0时,就可以判断它已经不再被引用,可以被回收了。但是,引用计数法无法解决循环引用的问题,即两个或多个对象相互引用,导致它们的引用计数器永远不为0,无法被回收。

可达性分析:可达性分析法是一种更加可靠的判断对象是否可达的方法。它的原理是从一组根对象出发,遍历所有对象,将所有可达的对象标记为“存活”,所有不可达的对象标记为“死亡”,

从而回收它们所占用的内存空间。在可达性分析法中,通过一系列算法和数据结构,可以高效地判断对象是否可达,避免循环引用的问题。

1,从一组根对象(如线程、静态变量等)出发,将它们作为起始点,遍历所有与之相连的对象,将它们标记为“存活”。(根搜索)

2.将所有可达的对象标记为“存活”,不可达的对象标记为“死亡”。(对象标记)

3.清除所有被标记为“死亡”的对象,回收它们所占用的内存空间。(清除死亡对象)

4.将所有存活的对象向一端移动,从而使剩余的内存空间变成一块连续的空间,便于后续的对象分配(压缩存活对象)

具体的实现方法:

标记-清除算法:

最简单的方法。

1.标记所有可达对象存活,不可达对象死亡。

2.清除死亡对象回收空间

优缺点:内存碎片,回收效率低

标记-复制算法:

1.标记所有可达对象存活,不可达对象死亡。

2.在复制阶段,将所有存活的对象复制到另一个内存区域中,保证存活对象之间的空间是连续的。在清除阶段,将原来的内存空间清空,回收它们所占用的内存空间

加大内存负担,用更多空间保证内存碎片问题。

 

三色标记法

白色:没有被标记过,一开始都是白色。(没有引用)
灰色:已经被标记过,但是该对象下的属性没有全被标记完(由引用,但是没有处理完引用关系)
黑色:已经标记过,并且该对象下的属性全部标记完毕。(有引用,不用清除)

1.从一组根对象出发,将它们标记为灰色,加入待处理队列

2.从待处理队列中取出一个灰色对象,将它标记为黑色,遍历所有与之相连的对象,将它们标记为灰色,加入待处理队列。

重复步骤2,直到待处理队列为空

3.清除所有被标记为白色的对象,回收它们所占用的内存空间。

结束后,白色里仍然没有标记的,是不可达,进行回收。

并发标记带来的问题:

1.并发修改导致标记结果不准确。 加锁/CAS等性能损失换原子性

2.漏标情况,应用程序动态产生新对象,漏标,内存泄漏。可以使用增量标记优化。

3.并发标记过程中,应用程序可能会持续地分配内存空间,导致垃圾回收器无法回收内存空间,从而导致内存空间的不足。为了解决这个问题,需要使用内存分配器的优化策略,如分代回收TLAB等,以提高内存分配的效率和减少内存空间的浪费

 


如果并发标记,标记期间对象间的引用可能发生变化,就会出现多标漏标情况。
多标:
黑色,灰色变成白色,但是之前标记成了黑色,变成了不会被发现的浮动垃圾,交给下次gc处理即可,影响不大。
漏标:
并发标记的过程中,白色断开引用成为垃圾,黑色引用白色对象。但是黑色已经被完全扫描过不会重新扫描,导致对象需要被引用但是又面临被回收。CMS和G1都对此进行了优化

CMS:

对于CMS垃圾回收器而言,它采用增量标记的方式来解决漏标问题。增量标记是指在垃圾回收过程中,将标记过程分解成多个阶段,每个阶段只标记一部分对象,然后让应用程序继续执行一段时间,再继续标记下一部分对象。这样可以避免垃圾回收线程一次性占用太多CPU资源,同时也减少了漏标的可能性。


G1:

即使用 remembered set 来解决漏标问题。remembered set 是一种数据结构,用于记录从年轻代到老年代的引用关系,当年轻代中的对象被回收时,G1垃圾回收器会扫描 remembered set 中的引用关系,从而标记所有与之相关的对象。这样可以避免漏标问题,并且也减少了垃圾回收的开销。