有关JVM的一些总结

本文详细介绍了JVM的内存区域,包括堆区、方法区、虚拟机栈、本地方法栈和程序计数器。堆区分为年轻代和老年代,年轻代通过初级GC进行垃圾回收,老年代则进行Full GC。此外,文章还讨论了类加载的五个过程以及对象的生命周期,以及几种常见的垃圾回收算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先说下类的记载
类加载的五个过程
加载:类加载器获二进制字节流,将静态存储结构转化为方法区的运行时数据结构,并生成此类的Class对象。

验证:验证文件格式、元数据、字节码、符号引用,确保Class的字节流中包含的信息符合当前虚拟机的要求。

准备:为类变量分配内存并设置其初始值,这些变量使用的内存都将在方法区中进行分配。

解析:将常量池内的符号引用替换为直接引用,包括类或接口的解析、字段解析、类方法解析、接口方法解析。

初始化:执行类中定义的Java程序代码(字节码)。

类的生命周期(7个):加载、验证、准备、解析、初始化、使用、卸载
在这里插入图片描述
1、堆区
堆区是JVM中最大一块内存区域,存储着各类生成的对象、数组等,JVM8中把运行时常量池、静态变量也移到堆区进行存储。堆区被细化可以分为年轻代、老年代,而年轻代又可分为Eden区、From Survivor、To Survivor三个区域,比例是8:1:1。一个对象从生成到结束将会有机会经历堆区的不同区域完成“使命”。根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
问题:什么是运行时常量池?
简单的来class文件在编译后除了存储一些类的版本、字段、方法、接口等元数据信息外,还有一部分信息是常量池,这个常量池我们称之为“静态常量池”,只是作为一种持久化数据存储在硬盘上,代表编译期生成的各种字面量和符号引用(最常见的就是字符串常量),那么这类信息被加载到内存中就会以运行时常量池的形式存在内存中,JDK7以前这类信息被存储在方法区,但是JDK7/JDK8都已经移到了堆区。这类数据变量的好处简单来说就是如果堆区中已经存在一个数据变量,即使再创建一个这样的变量,那么JVM将会直接指向已经创建好的数据,而不会再分配内存区域,这样一方面加快数据的创建,另一方面节省内存空间!但是实际上的机制要复杂一些,可以参考String类去理解!

2、方法区
方法区主要是存储类的元数据的,如虚拟机加载的类信息、编译后的代码等。JDK8之前方法区的实现是被称为一种“永久代”的区域,这部分区域使用JVM内存,但是JDK8的时候便移除了“永久代(Per Gen)”,转而使用“元空间(MetaSpace)”的实现,而且很大的不同就是元空间不在共用JVM内存,而是使用的系统内存。

元空间是一块与堆不相连的本地内存。原本存在永久代的数据,一部分移到了java堆里面,一部分移到了本地内存里面(即元空间)(文档中原句:Move part of the contents of the permanent generation in Hotspot to the Java heap and the remainder to native memory.) 。永久代中原来存储的字符串常量(池)、符号引用(这两个在jdk7普遍就已经将其放在堆上了)和类的静态变量现在存储在java堆中,其余的数据作为元数据存储在元空间中。

元数据是数据的数据或者叫做用来描述数据的数据或者叫做信息的信息。(比如原本方法区存储的类信息、即时编译器编译后的代码等),也可以把元数据简单的理解成,最小的数据单位。元数据可以为数据说明其元素或属性(名称、大小、数据类型、等),或其结构(长度、字段、数据列),或其相关数据(位于何处、如何联系、拥有者)。

3、虚拟机栈
我们通常所说的“方法入栈”、“栈区”其实指代的就是虚拟机栈。但是实际上这个并不准确,我们所说的“栈区”等称呼确切的说指代的是虚拟机栈中局部变量表的部分。
Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

4、本地方法栈
从英文中我们可以很容易的看出,其实这部分区域是专门为Native方法来实现的!由于java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节 ,一个Native Method就是一个java调用非java代码的接口。方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

5、程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。

对象回收判断
1、引用计数法:给每个对象添加一个引用计数器,每当有一个对象引用它,计数器增加1,引用失效时计数器减1。任何时刻计数器为0就判定位没有对象引用。实际的VM回收过程中,并没有按照引用计数法判断对象是否存活。
2、可达性分析算法:Java是根据该算法判定对象的回收。基本思路:每个对象的创建都是以GC Root作为对象的起点,每发生一次对象引用,就会往引用链中增加一个对象,根对象结束了生命周期,引用的对象就没有GC Root,当发生GC时,这些没用根对象的引用就会被回收。

GC

对于大多数Java应用来说,Java Heap(Java堆)是JVM管理的内存中较大的一块,而且Java Heap是被所有线程共享的一块内存区域,于虚拟机启动时创建。而Java堆的唯一目的就是存放对象实例。由于Java堆是垃圾收集器管理的主要区域,因此也被称为"GC堆"。再从内存回收的角度来看,由于现代收集器基本都采用分代收集算法,所以Java Heap还可以被细分为:新生代和老年代,新生代再分得细致一点就可以分为Eden空间和两个相同大小相同的Survivor空间,比列是8:1:1
来一张盗图 原文链接:https://blog.youkuaiyun.com/sted_zxz/article/details/72476673
在这里插入图片描述
Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
年轻代的特点是产生大量的死亡对象,并且要是产生连续可用的空间, 所以使用复制清除算法和并行收集器进行垃圾回收.对年轻代的垃圾回收称作初级回收 (minor gc)。

年轻代

初级回收将年轻代分为三个区域, 一个新生代 , 2个大小相同的复活代, 应用程序只能使用一个新生代和一个复活代, 当发生初级垃圾回收的时候,gc挂起程序, 然后将新生代和复活代中的存活对象复制到另外一个非活动的复活代中,然后一次性清除新生代和复活代,将原来的非复活代标记成为活动复活代。将在指定次数回收后仍然存在的对象移动到老年代中,初级回收后,得到一个空的可用的新生代。

新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。 当一个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。 当对象在 Eden 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳,则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域中,然后清理所使用过的 Eden 以及 Survivor 区域,并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。 但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。

老年代

Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。

现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。 另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
进入老年代还有如下情况: 1、大对象直接进入 2、长期存活的对象即将进入老年代

网上查阅的垃圾回收算法简单了解,如需深入自行查找资料:
1.标记-复制
它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次理掉。它的优点是实现简单,效率高,不会存在内存碎片。缺点就是需要2倍的内存来管理。
在这里插入图片描述
2.标记-清理
标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。它的优点是效率高,缺点是容易产生内存碎片。
在这里插入图片描述
3.标记-整理
标记操作和“标记-清理”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有 存活的对象都向一端移动,并更新引用其对象的指针。因为要移动对象,所以它的效率要比“标记-清理”效率低,但是不会产生内存碎片。
在这里插入图片描述
4.分代处理
由于对象的存活时间有长有短,所以对于存活时间长的对象,减少被gc的次数可以避免不必要的开销。这样我们就把内存分成新生代和老年代,新生代存放刚创建的和存活时间比较短的对象,老年代存放存活时间比较长的对象。这样每次仅仅清理年轻代,老年代仅在必要时时再做清理可以极大的提高GC效率,节省GC时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值