JVM-内存结构

在这里插入图片描述

堆:线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。

堆结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OOSbB9Sc-1653482504594)(imgclip.png "imgclip.png")]

  1. 存放数据:

对象实例,数组。

  1. 控制参数:
参数描述
-Xms堆内存初始大小
-Xmx(MaxHeapSize)堆内存最大允许大小,一般不要大于物理内存的80%
-XX:NewSize(-Xns)年轻代内存初始大小
-XX:MaxNewSize(-Xmn)年轻代内存最大允许大小,也可以缩写
-XX:NewRatio新生代和老年代的比值,值为4 表示 新生代:老年代=1:4,即年轻代占堆的1/5
-XX:SurvivorRatio=8年轻代中Eden区与Survivor区的容量比例值,默认为8,表示两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10
-XX:+HeapDumpOnOutOfMemoryError)内存溢出时,导出堆信息到文件
-XX:+HeapDumpPath堆Dump路径 -Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
-XX:OnOutOfMemoryError当发生OOM内存溢出时,执行一个脚本 -XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat
-XX:MaxTenuringThreshold=7表示如果在幸存区移动多少次没有被垃圾回收,进入老年代
  1. 垃圾回收

从结构上来分可以分为新生区、老年区、永久区(JDK 8)时取消永久区使用元空间取代;新生区又分为eden区以及survivor0区和survivor1区。所有新生成的对象首先会存放在新生区中的eden 区,在进行垃圾回收时,先将eden 区存活的对象复制到survivor0中,当survivor0 区存满就会将eden 区、survivor0 区存活对象复制到survivor1 区中,清空eden 区、survivor0 区;survivor1 区与survivor0 区互换角色。始终保持一个survivor 区是空的。以此往复… 当最终eden区 与某一个survivor 区存满时,存活对象就会被移送到老年区中。如果老年区都存满对象就会触发Full GC对所有区域进行垃圾回收。新生区发生的垃圾回收称Minor GC,并且发生频率非常高,不一定是Eden区满了才执行。

  1. 异常情况

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常(OOM 异常)

方法区:线程共享。存储类元数据,常量池,方法信息,静态类变量等。(JDK 7 永久区)(JDK 8 元空间)

  1. 存放数据:

类元数据,常量池,方法信息,静态类变量等。

  1. 控制参数:

-----------------------JDK 7 永久区------------------------

-XX:PermSize 设置最小空间。

-XX:MaxPermSize 设置最大空间。

-----------------------JDK 8 元空间------------------------

MetaSpaceSize 初始化元空间大小,控制发生GC阈值

MaxMetaspaceSize 限制元空间大小上限,防止异常占用过多物理内存

  1. 垃圾回收

对此区域会涉及但是很少进行垃圾回收。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

  1. 错误信息

如果方法区太大,超过设置,会报OutOfMemoryError:PermGen space错误。

注意:JDK1.8使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。为了释放管理压力,把运行时常量池交给堆去管理。

常量池

  1. 什么是常量池?
  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fFYqyaJh-1653482531170)(imgclip_2.png "imgclip_2.png")]

  • 运行时常量池,常量池是class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真是地址
  1. 常量池的作用?

常量池的作用就是避免重复创建和销毁对象影响性能,实现对象共享。

  1. Integer 常量池
public class Demo {
    public static void main(String[] args) {
        Integer integer = new Integer(10);
        Integer integer2 = new Integer(10);
        // 自动装箱:实际上是调用了 Integer.valueOf(int)
        Integer integer3 = 10;
        Integer integer4 = 10;
        Integer integer5 = 600;
        Integer integer6 = 600;
        System.out.println(integer == integer2);  // false
        System.out.println(integer3 == integer4);  // true
        System.out.println(integer5 == integer6);  // false 为什么上面是 true 而下面是 false?原因就是底层在常量池中存在一个Integer 缓冲数组
    }
}

常量池中的Integer 缓冲数组,源码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LMfCIlBp-1653482531179)(imgclip_1.png "imgclip_1.png")]

  1. String 常量池(串池)

常量池底层使用StringTable数据结构保存字符串引用,实现和HashMap类似,根据字符串的hashcode定位到对应的数组,遍历链表查找字符串,当字符串比较多时,会降低查询效率。

String 通常有两种方式来创建对象

String str = new String("abc");

String str2 = "abc";

第一种方式是通过new 实例化对象,new String(“abc”) 会保存到堆中。每次调用都会创建一个新对象。

第二种方式现在栈上创建一个String 类对象引用变量str ,然后通过符号引用取字符串的常量池寻找是否存在“abc”,如果没有就将“abc” 放到常量池中,如果存在就引用常量池中的“abc”。

使用第二种创建方法查看,是否引用字符串常量池的同一个字符串:

public class Demo{
	public static void main(String[] args){
		String str1 = "abc";
		String str2 = "abc";
		System.out.println(str1==str2);  // true	
	}
}

因为对于String 来说,String 类属于引用类型,equals方法判断值内容是否相同,==判断值地址是否相同,经上述的比较发现内容相同的情况下地址是相同的,说明引用的都是常量池中的值也就是“abc”。

注意点:

  1. 字符串的+ 号拼接问题:
public class Demo{
	public static void main(String[] args){
		String str1 = "abcde";
		String str2 = "abc"+"de";	
		System.out.println(str1==str2);  // true	
	}
}

原因:对于字符串常量的 + 号连接,在程序编译期,JVM就会将其优化为 + 号连接后的值。所以在编译期其字符串常量的值就确定了。

  1. 字符串引用的+ 号拼接问题:
public class Demo{
	public static void main(String[] args){
		String str1 = "abc";
		String str2 = "abcde";
		String str3 = 	str1+"de";
		System.out.println(str2==str3);  // false	
	}
}

原因:对于字符串引用的 + 号连接问题,由于字符串引用在编译期是无法确定下来的,在程序的运行期动态分配并创建新的地址存储对象。

发生了什么?

反编译:

在这里插入图片描述

解决方法:

1. 使用final 是否可以解决字符串引用的 + 号连接问题呢?

public class Demo{
	public static void main(String[] args){
		final String str1 = "abc";
		String str2 = "abcde";
		String str3 = 	str1+"de";
		System.out.println(str2==str3);  // true
	}
}

因为:使用了final 修饰在编译期就可以确定str1 的值。所以 str1 + "de"就等同于 “abc” + “de”,所以结果是 true。

2. String对象的intern()方法主动将串池中还没有的字符串对象放入串池

在我们调用String.intern的时候会往hashtable里插入一项,这个table就是stringtable

JDK1.8:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回

JDK1.6:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

public class Demo{
	public static void main(String[] args){
		String str1 = "abc";
		String str2 = "abcde";
		String str3 = 	str1+"de";
		System.out.println(str2==str3.intern());  // true
	}
}

方法栈:线程私有。存储局部变量表(基本数据类型、形参)、操作栈、动态链接、方法出口,对象指针。

区别于本地方法栈,java虚拟栈为jvm 执行Java方法服务,本地方法栈则是jvm 执行native 方法服务。

  1. 存放数据:

局部变量表(基本数据类型、形参)、操作栈、动态链接、方法出口,对象指针。

  1. 控制参数:

-Xss控制每个线程栈的大小。

  1. 垃圾回收

见 Slot 复用?

  1. 错误信息

在Java 虚拟机规范中,对这个区域规定了两种异常状况:

  • StackOverflowError: 异常线程请求的栈深度大于虚拟机所允许的深度时抛出;例如:不合理的方法递归调用

  • OutOfMemoryError 异常: 虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出。

  1. 拓展

什么是栈与栈帧?

栈就是一条先进后出的队列,而栈帧就是栈中的元素。每个线程都会分配一个栈的空间,即每个线程拥有独立的栈空间。

入栈和出栈?

压栈(入栈)和弹栈(出栈)都是对栈顶元素(栈帧)进行操作的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQXOfsa0-1653482623935)(imgclip_1.png "imgclip_1.png")]

栈帧中储存什么?

每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表(基本数据类型、形参)、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T9NbGJV2-1653482623936)(imgclip_2.png "imgclip_2.png")]

局部变量表?

栈帧中,由一个局部变量表存储数据。局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。以下简称 Slot 。

JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用。

验证Slot 存在,以int 和long类型为例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HUT1eKak-1653482623944)(imgclip_5.png "imgclip_5.png")]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tMpjNEGx-1653482623946)(imgclip_6.png "imgclip_6.png")]

操作数栈?

操作数栈是一个后进先出栈。操作数栈的元素可以是任意的Java数据类型。方法刚开始执行时,操作数栈是空的,在方法执行过程中,通过字节码指令对操作数栈进行压栈和出栈的操作。通常进行算数运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递。操作数栈可以理解为栈帧中用于计算的临时数据存储区。

看一个例子:

public class Demo {
    public static void main(String[] args) {
        test(10, 20);
    }

    public static int test(int i, int j) {
        int k = i + j;
        return k;
    }
}

cmd 反汇编得到

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2IiqmEJm-1653482623950)(imgclip_3.png "imgclip_3.png")]

说明:test 方法刚开始执行时,操作数栈是空的。当执行 iload_0 时,把局部变量 0 压栈,即 100 入操作数栈。然后执行 iload_1,把局部变量1压栈,即 98 入操作数栈。接着执行 iadd,弹出两个变量(100 和 98 出操作数栈),对 100 和 98 进行求和,然后将结果 198 压栈。然后执行 istore_2,弹出结果(出栈)。

下面通过一张图,对比执行100+98操作,局部变量表和操作数栈的变化情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s5cpPWYx-1653482623951)(imgclip_4.png "imgclip_4.png")]

Slot 复用?

为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用,也就是所谓的 Slot 复用。通过一个例子来理解变量“失效”。

看一个例子,看看什么是 slot 复用:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UPee0CeU-1653482623952)(imgclip_7.png "imgclip_7.png")]

再看一个例子,看看垃圾回收,如何判定:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Grw7yrfg-1653482623954)(imgclip_8.png "imgclip_8.png")]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MNbaJ6i6-1653482623956)(imgclip_9.png "imgclip_9.png")]

本地方法栈:线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。jvm调用一些本地方法,为这些本地方法提供的内存空间。

  1. 异常情况

与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。

程序计数器:线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。

程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。多线程执行程序依赖于CPU分配时间片执行,画个简单的图,看看多线程怎么利用CPU时间片的。如下图,线程0和线程1分配cpu时间片交替执行程序,假设此时线程0先获取到了时间片,时间片用完后CPU会将时间片再分配给线程1,线程1执行完毕后,此时,时间片又回到线程0来执行,那么问题来了,线程0上次执行到哪儿了呢?具体是代码的多少行了呢,该行代码有没有执行完毕?此时程序计数器就发挥作用了,程序计数器保存了线程的执行现场,方便下次恢复运行。这也是为什么程序计数器是线程独享的原因。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FLrlHN3W-1653482762434)(imgclip.png "imgclip.png")]

如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。

程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值