JVM内存结构详解

本文详细探讨了JVM内存结构,包括JMM、类加载器、类加载过程及其验证、准备、解析阶段。讲解了程序计数器、虚拟机栈、本地方法栈、方法区和堆的原理和作用,以及对象生命周期、垃圾收集机制,特别是新生代和老年代的内存动态变化。此外,还介绍了Minor、Major和Full GC的触发条件以及对象存活判断的引用计数法。

JVM内存结构

在这里插入图片描述

JMM(java内存模型)

在这里插入图片描述

类加载器的作用:

类加载器就是把java的字节码文件加载虚拟机中.

什么时候进行类加载?

在程序主动使用某个类时,如果这个类没有被加载到内存中,通过JVM加载,连接,初始化这步操作,将类加载到JVM中.
加载过程中也可能会出现加载失败的情况,没有问题的话,这三步成为类加载或者类初始化操作.
在这里插入图片描述
在这里插入图片描述

ClassLoader classLoader = String.class.getClassLoader();
		System.out.println(classLoader);//null

		//jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
		ClassLoader classLoader1 = T.class.getClassLoader();
		System.out.println(classLoader1);

		// jdk.internal.loader.ClassLoaders$PlatformClassLoader@1d251891
		ClassLoader parent = classLoader1.getParent();
		System.out.println(parent);

		ClassLoader parent1 = parent.getParent();
		System.out.println(parent1);//null

在jdk8中它的包结构是sun,misc.Luanchcher&
在jdk11中对包做了整合,调整了位置
1.null
2.AppClassLoader
3.PlatformClassLoader
4.null

1.String.class.getClassLoader();//null
String 是jdk自带的类,所以它使用的类加载器是启动类加载器,启动类加载器是C++编写,所以结果为null.
其他的jdk自带的类 也是直接使用启动类加载器(BootstrapClassLoader).
2.自定义的类,它会被AppClassLoader应用程序加载器去加载
3.jdk11使用类加载器是(PlatformClassLoader)去完成加载.
4,.因为启动类加载器是C++编写,所以在调用方法获取父加载器的时候返回null.

介绍一下java加载一个类的过程?

类从被加载到虚拟机内存再到卸载出内存一共需要经历七个阶段(这七个阶段也就包括了类的整个生命周期)
包括:加载,验证,准备,解析,初始化,使用和卸载(这七个阶段也就是类的生命周期)
其中类加载的过程包括:加载验证准备解析初始化五个阶段
在这五个阶段中,加载验证准备和初始化这四个阶段的顺序是确定的,不变解析阶段可能在顺序上发生变化,解析可能会发生在初始化阶段之后,
原因:这是因为为了支持java语言的运行时绑定特点(动态绑定)
逐个说明
1.加载:就是指将类的class文件读入到内存中,并将这些静态数据转换成方法区中的运行时数据结构
同时,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这里的过程需要类加载器参与
java的类加载器由JVM提供,本身就是所有程序运行的基础,这些加载器都可以统称为系统类加载器除此之外,程序员可以通过继承ClassLoader类来创建自己的类加载器(自定义类加载器的创建方式)
类加载器的加载范围:
可以加载本地的class文件也可以加载Jar包中的class文件还可以加载网络上的class文件

使用类加载器产生的最终产物:

会根据类中的代码,在堆和方法区中分别处理我们程序

连接过程
发生在类加载之后,系统会为其生成一个类Class对象,接着就会进入连接阶段
在连接阶段主要负责把二进制数据合并到JRE中,(大白话:就是将类的二进制代码合并到JVM运行状态中)可以将其分为三个小阶段:
验证,准备,解析
2.验证
加载的类的信息是否符合JVM规范,检查一下有没有安全方面的问题
最主要就是来检查class文件的格式是否规范
检查当前加载的这个class文件能否被JVM虚拟机加载处理
3.准备
为变量分配内存并设置类变量的初始化,在这个阶段中分配的仅仅是类的变量(static修饰的变量)对非final的变量JVM会将其设置成0
(对非final的变量不处理)
private static int num = 23;
在这个阶段, num的值0不是23
final修饰的类的变量将被设置成真实的值.
4.解析
(符号引用被换成直接引用):这个解析过程就是将常量池内的符号引用替换成直接引用,主要包含四种类型引用的解析接口解析,属性解析,方法解析,接口方法解析
5.初始化
为类中所有的静态变量赋初值,同时静态代码块执行,
当初始化一个类的时候,这个类有父类,这个父类还没有进行初始化需要先对其父类进行初始化,初始化好父类之后在初始化子类
JVM虚拟机会保证一个类的初始化过程在多线程场景下也是安全的,保证安全的方式也是加锁
6.使用
7.卸载

jvm中的程序计数器

在这里插入图片描述
程序计数器是一块比较小的内存空间,记录了当前线程执行到的字节码行号, 每个线程都由自己的程序计数器,相互不影响,native()方法,计数器为0.
程序计数器搭配多线程使用.比如线程中断和恢复的操作都是用程序计数器记录线程执行的信息,

虚拟机栈

java虚拟机栈是线程私有的,它的生命周期和线程相同.
虚拟机栈主要是描述java方法执行的内存模型,每个java方法在执行期间都有自己的栈帧(Stack Frame),
方法执行时栈帧就是个入栈操作,方法执行完之后栈帧就会出栈.
这个栈帧主要主要用来存储局部变量表,方法出口(返回值),操作数栈,动态连接等信息.

在这里插入图片描述

局部变量表: 可以存储8大基本数据类型和返回值以及方法参数和对象的引用.
局部变量表是一个数组,数组的长度在编译期间确定,通过下标去获取内容.

操作数栈: 就是用来操作的. int b = 20+1; 先是读取我们的代码,然后进行计算,结果放入到局部变量表
运行时常量池中数据以及局部变量表中得值都可以由操作数栈进行获取。

动态连接: 把符号转换为直接引用分为两种情况。
在JVM加载或第一次使用转换时称为静态链接或静态解析.
而在运行期间把符号转换为直接引用称为都动态连接(我们在方法中 要使用其他类中的方法 就是动态连接).
方法返回值(returnAddress返回地址,方法出口): 实际上它保存的是return后程序计数器中要执行的字节码的指令地址

栈指向堆是什么意思(栈中要是使用成员变量怎么办)?

首先 栈中是不会存储成员变量的,只会存储一个地址,成员变量是在堆中存储.

标题 方法1调用2,2->3,3>4

栈结构是什么样的?

答:方法1先入栈,2,入栈,3.入栈.4入栈

弹栈: 栈帧4,3,2,1

在这里插入图片描述

为什么会出现栈溢出?

栈溢出错误:StackOverFlowError
因为无线压栈,就会导致栈空间不足
比如:

public static void main(String[] args) {
		new T().A();
	}
	public void A(){
		B();
	}
	public void B(){
		A();
	}

本地方法栈

本地方法栈和虚拟机栈有些类似,虚拟机栈是管理java方法的调用.
本地方法栈 是用于管理非java语言方法的调用.
native修饰的方法都是本地方法,由c/c++编写.
在这里插入图片描述

方法区

运行时数据区(就是指JVM)分出了好多个小区域,遵守JVM规范,是属于jvm规范的具体实现
不同的虚拟机厂商指定的规范不一样.
方法区是jvm规范的一部分
在HotSpot虚拟机中,会常提到永久代
Hotspot虚拟机在jdk8之前用永久代实现了方法区,
java7永久代与堆内存连续
java8的元空间放在了直接内存中(电脑上的内存条)
方法区主要作用:
存储虚拟机加载的 类的相关信息.
方法区所有线程共享的区域,考虑线程安全.
类的信息:类的版本 属性 方法 接口 常量 静态变量 父类等信息以及及时编译器编译后的代码等数据
常量池:运行时常量池 字符串常量池 类常量池 (在;另一个文档)
运行时常量池存储的是类加载后,解析后生成的直接引用 等信息.

什么是直接引用?

jdk8把方法区实现从永久变成了元空间, 有什么区别?
最主要的区别:
元空间的位置 不在JVM虚拟机上,而是在直接内存上(就是电脑的内存条)
永久代不是真实存在,是jvm 的虚拟内存.
JVM就不会出现内存溢出问题
以前的永久代 经常因为内存不够导致抛出OOM错误(Out Of Memory)

按照jdk8来说,类信息存储在元空间的,元空间在直接内存上,相当于类信息存储在直接内存上.
其实在Java 7 时,部分数据已经移植到本地内存上了。例如:符号引用(Symbols)

堆:

堆本身是线程共享的区域,类的实例和数组分配都是在堆中
堆被划分为新生代区域和老年代区域
其中新生代区域又被进一步划分为Eden 和Survivor区,
其中的Survivor区 又被进一步分为From Survivor区(S0区 或者from区)
和To Survivor区(S1区或者to区)
官方分配比例:8:1:1,实际比例:6:1:1
java中的堆是jvm所管理的最大的一块内存空间,主要用于存放各种类的实例对象,
在这里插入图片描述

关于堆:
1.什么时候用哪个空间存储数据
2.垃圾回收怎么回收?
将堆划分几个区域是为了方便回收数据
-Xmx:最大堆空间数 -Xms:最小堆空间数

假设:
1G = 1024M
3G = 3072M
新生代占1G
伊甸区占800M S0区占100M S1区占100M
关于设置空间的思想: 往大了设置 不往小了修改
老年代占2G
JVM中的堆 处于物理内存上不连续的内存空间
如果想给堆修改空间大小 我们可以通过-Xms -Xmx进行设置
在修改对空间大小的时候 , 我们99%的场景 都是往大了设置
新创建出来的对象, 通常都是放Eden区,
但是如果这个对象很大, 就直接放到老年代中
大对象就是指需要大量空间的java对象

JVM对于堆的使用:,每次只会使用Eden区和Survivor区其中一个,也就是说新生代的实际使用空间是9/10 总是在Survivor区中有一块分区闲置

为什么要这么分?
答;主要是根据各个分区的特点对 对象进行分区存储,方便在垃圾回收的时候结合不同的算法提升垃圾回收的速度.
垃圾回收的特点是什么?

新生代,每次垃圾回收时都会发现大批对象死亡,只有少量对象存活的时候,此时在新生代采用复制算法,存活的对象越少,执行的速度就越快,成本就越低.
老年代中因为对象存活时间较长,对象存活率较高,它没有额外的内存空间来分担存储任务,所以采用标记清除或者标记整理算法进行垃圾回收.
.

在新生代每次Monior GC的内存动态变化

1换名字,位置不变
2数据位置变了,名字不变
本质上都是交替使用from区和to区
正常使用的时候,当Eden区没有足够的空间,JVM就会发起一次Minor GC,如果回收掉了一部分垃圾数据,咱们就有空间可用了.
在Eden区如果对象经过了一次monior GC并且它还能够被Survivor空间接收,那么这个数据就会被移动到Survivor空间中,同时年龄+1,
对象在Survivor区每度过一次MinorGC且存活下来 年龄+1,当年龄达到一定程度时(默认值15),就会被升到老年代中.这个默认值是可以设置的,通常不会去更改.
在Eden区如果对象经过了一次monior GC,对象会从Eden区进入Survivor区,如果Survivor空间内存满了不能接收,则直接放入老年代中(根据分配担保机制).

在这里插入图片描述
当我们实例化对象的时候 数据区域的变化
new Student();
XX:PretenureSizeThreshold=1M 设置大对象直接放到老年代的临界值

Minor GC Major GC Full GC区别以及触发条件

Minor GC是年轻代GC,它是新生代触发垃圾回收时使用的垃圾收集器.它的触发比较频繁同时也因为java对象的生命周期比较短暂,通常回收速度比较块
Major GC 是老年代GC,通常这个GC连着Minor GC一起执行(当Minor GC执行的时候,Major GC也会执行),在执行速度上比Minor GC要慢一些.

Full GC 当堆满的时候就会触发垃圾回收机制.不做选择,清理整个堆(年轻代+老年代).

Minor GC的触发条件
1.当Eden区满时,触发Minor GC每当发现Eden满的时候,触发Minor GC
2.比如Eden区空间大小是800M,已经使用799.9M,
新创建对象的大小 > Eden区所剩空间,也会触发Minor GC

当产生一个新对象,新对象优先在Eden区分配。如果Eden区放不下这个对象,虚拟机会使用复制算法发生一次Minor GC,清除掉无用对象,同时将存活对象移动到Survivor的其中一个区(fromspace区或者tospace区)。虚拟机会给每个对象定义一个对象年龄(Age)计数器,对象在Survivor区中每“熬过”一次GC,==(清理数据是针对整个新生代区)==年龄就会+1。待到年龄到达一定岁数(默认是15岁),虚拟机就会将对象移动到年老代。如果新生对象在Eden区无法分配空间时,此时发生Minor GC.对象会从Eden区进入Survivor区,如果Survivor区放不下从Eden区过来的对象时,此时会使用分配担保机制将对象直接移动到年老代(此时不会清理数据.直接进入老年代)。

Major GC的触发条件:

1.当Minor GC触发的时候,Major GC就会跟着执行
2.每次从新生代升到老年代的对象大小 超过老年代剩余空间
3.当大对象放入老年代的时候超过老年代剩余空间.
在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显示触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存(System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用)。

如何判断对象是否存活?

答:引用计数法
如果在堆中的对象没有指向任意引用,就会认对象没有被使用,就会当成垃圾处理.
引用计数法有缺点,不能监测到环的存在

什么是引用计数法?

当每个对象在创建的时候, 就给这个对象绑定一个计数器, 每当有一个引用指向这个对象, 计数器加1
每当有一个指向这个对象的引用被删除时, 计数器就减1
最后我们通过计数器是否为0 就可以判断出这个对象是否是垃圾数据

优点:
引用计数器的算法 实现简单 判定垃圾数据效率也非常高, 大部分情况下 都是一个非常优秀的算法

缺点:
很难解决对象间的相互引用.

执行引擎:

执行引擎的作用非常重要 , 虚拟机的核心就是执行引擎, 它就是负责执行虚拟机的字节码文件,
通过javac 命令 将.java文件 编译成 .class 类加载器将.class文件 加载到jvm 如果我们想让电脑
执行.class文件 还需要将.class文件 转成 电脑认识的机器码, 执行引擎来负责干这件事.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值