【Java】JVM内存结构和垃圾回收

JVM(Java虚拟机)

JVM介绍

JVM —— Java程序运行环境(Java二进制字节码运行环境)

好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

JVM、JRE、JDK比较:

  • Jvm在倒数第二层 由他可以在(最后一层的)各种平台上运行

  • Jre大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库

  • Jdk还包括了一些Jre之外的东西 ,就是这些东西帮我们编译Java代码的, 还有就是监控Jvm的一些工具
    在这里插入图片描述

内存结构和垃圾回收

内存结构

在这里插入图片描述

1.程序计数器

程序计数器(寄存器)

作用:记住下一条JVM指令的执行地址。

特点:

  • 是线程私有的
  • 不会存在内存溢出
2.虚拟机栈

(可以类比C++函数调用堆栈详细过程:CPP-LearningNotes/03C++:函数调用堆栈详细过程.md at main · gzf66666/CPP-LearningNotes (github.com)

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧组成,对应着每次方法调用时所占的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

问题辨析:方法内的局部变量是否线程安全?

  • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
  • 如果是局部变量引用了对象, 并逃离方法的作用方法,需要考虑线程安全

栈内存诊断检测方法:

  • 用top定位哪个进程对CPU的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id
3.本地方法栈

存放的本地方法的空间。

本地方法如:clone、wait等,底层都是通过C++实现的。

4.堆

通过new关键字创建对象都会使用堆内存。

特点:

  • 它是线程共享的(程序计数器、虚拟机栈、本地方法栈都是线程特有的),堆中对象都需要考虑线程安全问题。
  • 有垃圾回收机制。(不用像C++中new完对象需要delete,JVM垃圾回收机制会自动释放内存)。

堆内存诊断监测工具:

  1. jps工具
    • 查看当前系统中有那些Java进程
  2. jmap工具
    • 查看堆内存占用情况
  3. jconsole工具
    • 图形界面的,多功能的监测工具,可以连续监测
5.方法区

特点:

  • 它也是线程共享的。
  • 存储类的结构相关信息(类的成员变量、常量池、方法数据、成员方法等)。
  • 方法区在虚拟机启动时创建,逻辑上是堆的一部分。

二进制字节码包括:类基本信息、常量池、类方法定义 、虚拟机指令

可以通过javap -v **.class命令查看**.class文件的基本信息

常量池:就是一张表,为JVM指令提供常量符号,通过常量符号找到要执行的类名、方法名等信息。

运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号表地址变为真实地址。

关于常量池的相关问题:String Table可以阅读博客:

深入理解String、StringBuffer和StringBuilder_有头发的代码匠的博客-CSDN博客

总结:String Table特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是String Builder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池

垃圾回收机制

垃圾回收机制简称GC。

GC主要用于Java堆的管理。Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。

1.堆的区域划分
1.1新生代、老年代、永久代(方法区)的区别:
  • 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。
  • 老年代就一个区域。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
  • 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小
  • 新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,默认的,Edem : From Survivor : To Survivor = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,From Survivor = To Survivor = 1/10 的新生代空间大小。
  • JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间
  • 永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。

为什么要这样分代?
可以根据各个年代的特点进行分区存储,便与回收,针对不同年代使用最适合的收集方法。

  1. 新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  2. 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。

数据会首先分配到Eden区当中,当Eden没有足够空间的时候就会触发JVM发起一次Minor GC。如果对象经过一次Minor GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代中了。

1.2Minor GC、Major GC、Full GC区别及触发条件

区别:

  • Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。

  • Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。

  • Full GC是清理整个堆空间,包括年轻代和老年代

触发条件:
触发添加基本原理相同,基本上都是区域内存不够用的时候触发。

Minor GC 触发条件一般为:

  • eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
  • ​新创建的对象大小 > Eden所剩空间

Major GC和Full GC 触发条件一般为:

  • 每次晋升到老年代的对象平均大小>老年代剩余空间
  • MinorGC后存活的对象超过了老年代剩余空间
  • 永久代空间不足
  • 执行System.gc() (手动回收)
  • CMS GC异常
  • 堆内存分配很大的对象
2.判断对象存活的方法
2.1引用计数法

(可以类比C++智能指针shared_ptr:CPP-LearningNotes/09C++:智能指针.md at main · gzf66666/CPP-LearningNotes (github.com)

每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,计数器为0就代表该对象死亡。

优点:引用计数算法的实现简单,判定效率也很高。
缺点:主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

public class Test {
	public Object object = null;
	public static void main(String[] args) {
		Test a = new Test();
		Test b = new Test();
		/**
		 * 循环引用,此时引用计数器法失效
		 */
		a.object = b;
		b.object = a;

		a = null;
		b = null;
	}
}

因为无法解决循环引用,所以通常不使用引用计数法。

2.2可达性分析法
  • 该种方法是从GC Roots开始向下搜索,搜索所走过的路径为引用链。当一个对象到GC Roots没用任何引用链时,则证明此对象是不可用的,表示可以回收。
    在这里插入图片描述
  • 上图上图中Object1、Object2、Object3、Object4、Object5到GC Roots是可达的,表示它们是有引用的对象,是存活的对象不可以进行回收。Object6、Object7、Object8虽然是互相关联的,但是它们到GC Roots是不可达的,所以他们是可以进行回收的对象。

这是目前主流的虚拟机都是采用的算法。

3.垃圾回收机制策略(GC算法)
3.1标记–清除算法

为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。
分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

优点:

  • 是可以解决循环引用的问题,必要时才回收(内存不足时)

缺点:

  • 回收时,应用需要挂起,也就是stop the world。 标记和清除的效率不高,尤其是要扫描的对象比较多的时候
  • 会造成内存碎片(会导致明明有内存空间,但是由于不连续,申请稍微大一些的对象无法做到)

应用场景:
该算法一般应用于老年代,因为老年代的对象生命周期比较长。

3.2标记–整理算法

标记-整理法是标记-清除法的一个改进版。该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。

优点:

  • 解决标记-清除算法出现的内存碎片问题=

缺点:

  • 压缩阶段,由于移动了可用对象,需要去更新引用。

应用场景:
该算法一般应用于老年代,因为老年代的对象生命周期比较长。

3.3复制算法

该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。

这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。

优点:

  • 在存活对象不多的情况下,性能高,能解决内存碎片和java垃圾回收算法之-标记清除中导致的引用更新问题。

缺点:

  • 会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,复制算法的性能会变得很差。

应用场景:
复制算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用复制算法进行拷贝时效率比较高。

JVM将新生代划分为Eden与2块Survivor Space(幸存者区) ,然后在Eden –>Survivor Space 与To Survivor之间实行复制算法。
不过JVM在应用复制算法时,并不是把内存按照1:1来划分的,这样太浪费内存空间了。一般的JVM都是8:1。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值