JVM内存结构

程序计数器-PC-Register

程序计数器的作用

在这里插入图片描述
源代码经过编译之后会成为jvm指令,Java的跨平台性其实就是基于这套jvm指令而来的,指令通过解释器之后会转换为机器码,CPU可以识别机器码做出响应的反应,而程序计数器的作用就是记住jvm的吓一跳执行地址,然后按照一定的顺序去执行命令。它的物理实现是寄存器。

特点

程序计数器是线程私有的

因为Java是支持多线程的,所以如果所有线程都是公用一个程序计数器,那么执行的流程就会非常混乱,导致执行错误。

程序计数器是没有内存溢出的

Java虚拟机栈

在这里插入图片描述

问题

在这里插入图片描述
垃圾回收不会涉及到栈内存。
栈内存的分配都有一个默认值,但是可以给JVM调整参数-Xss,来指定栈内存的大小,但是并不是越大越好,因为我们的内存是有限的,每一个线程运行的时候都会有一个该线程的栈(栈也是线程私有的)所以如果栈比较大,那么可以存在的线程就会减少,所以不是越大越好。
假如多个线程执行同一个方法,每一个线程在执行的时候都会创建一个栈帧出来而局部变量就存在于栈帧中,所以多个线程在运行的时候读取到的局部变量都存在于自己的栈的栈帧中,所以是不会被共享到的,所以是线程安全的。

内存溢出

导致的原因

  • 栈帧过多
  • 栈帧过大

第一种我们的栈帧过多其实就相当于我们的递归调用,非常好理解,栈帧过大,比如我们使用Json转换工具的时候比如我有个对象引用了另外一个对象然后这个对象也引用了我这个对象,那么在用JSON转换的时候就会不停的在这两者之间进行JSON转换导致虚拟机栈内存溢出

线程运行诊断

在这里插入图片描述

本地方法栈

在这里插入图片描述
在操作系统中会有一些方法Java是需要用到的,但是这些方法是操作系统的,我们是不可以直接进行调用的,所以需要有一个本地方法栈和程序计数器与本地方法接口进行配合才可以调用本地方法。
本地方法其实用的很多,在JDK源代码中有很多的体现,比如打开Object类我们可以看到
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这些等等等等的方法都是没有实现的,他们上面都加了一个native关键字,这个关键字就表示用的是本地的方法。

Heap堆

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

还有一个工具可以查看内存和线程信息
JvisualVm

方法区

在这里插入图片描述
方法区是在JVM启动的时候创建的,里面会存储常量池(主要是字符串常量池)还有所有类的类信息和类加载器。这些东西显然是一直都会去使用的,而且我们的方法区是一个逻辑概念不是一个独立的结构,在1.6版本的时候是在堆内存中的,而且因为类信息和类加载器还有常量池都是一直会使用的所以也不会被GC掉所以又叫做永久代,所以当时的永久代其实是方法区的一种实现。
在1.8以后,方法区不在堆内存中了,存储的东西也还是Class,ClassLoader,常量池,但是注意一点,字符串常量池没有了,字符串常量池被留在了堆内存中,方法区被移动到了本地内存中,叫做元空间。

内存溢出

我们的方法区是共享的一片区域,存储的是Class和ClassLoader和一些常量,那么好像存储的不太多,好像不太可能发生内存溢出,但是其实也是有可能的因为我们的Spring或者Mybatis等技术会生成一些动态代理的对象,这些个对象都是会有类的,所以其实还是很有可能出现非常多的类导致内存溢出,并且内存溢出我们可以知道在我们的1.6的时候用的是堆内存,在1.8的时候用的是本地内存,所以其实1.6的时候与1.8相比会更加容易发生内存溢出。且二者的报错信息不太一样,我们如果希望也可以去手动的设置方法区的内存大小。
在这里插入图片描述
这里贴一张网络截图,1.7的可以不用太了解是在设置永久代的大小(PerGen),1.8的我们可以设置元空间的大小。

方法区常量池

我们前面说过方法区会存储一个常量池,那么这个常量池是拿来干什么的呢?

常量池的作用就是当类被编译为jvm指令之后为jvm指令提供一些常量字符串和名词
在这里插入图片描述

我们来看一个演示案例
首先编写一个Hello World的程序

class Solution {
    public static void main(String[] args){
        System.out.println("Hello World!");
    }
}

也可以运行
在这里插入图片描述
好然后我们利用Java的一个指令

javap -v 字节码文件

进行一个反编译操作,-v是用来显示详细的信息的
然后我们反编译了这个类,并且获取到了详细信息
在这里插入图片描述
首先我们看到这里,这里就是类的详细信息,Classfile表示的是类的文件路径,还有上一次修改时间,MD5签名,从哪个文件编译而来,还有一些版本信息啥的。
然后我们看下面
在这里插入图片描述
这里有一个很长的叫做Constant pool也就是我们的常量池,这里面存储了很多奇奇怪怪的东西,我们先不看,然后继续看下面
在这里插入图片描述
这里就是我们反编译出来的代码指令,我们来看,里面有很多编号,就是用来给程序计数器使用的编号,然后我们看后面还有注释,第一个是getstatic,是获取一个静态东西的,看以前的代码我们知道肯定是System.out这个静态对象,但是JVM怎么知道获取哪一个变量呢?看后面跟上了一个参数#2,这个和前面的常量是对应起来的。回到常量池找到#2对应的常量
在这里插入图片描述
这个常量对应一个方法语句,是Fieldref用来寻找一个属性引用,参数是#21 和#22,继续看
在这里插入图片描述
可以看到是找一个类为#28的东西,并且找一个名称和类型为#29和#30的东西
继续看
在这里插入图片描述
所以其实第一句语句是在找一个类为java/lang/System路径下的名字是out的类型为Ljava/io/PrintStream的静态成员变量。
然后我们再来看一个例子,比如
在这里插入图片描述
我们看后面的注释可以知道这里是一个Hello World!字符串,那么是怎么得到的呢?
找到#3
在这里插入图片描述
可以看到是String类型的引用为#23
在这里插入图片描述
找过去之后可以看到是Hello World!的一个utf8的字符串
从上面的例子可以清楚的看出来常量池确实是给各个指令提供常量。

那么下面我们再看一波老师给我们讲解的炒鸡细的常量池和运行时常量池的案例

class Solution {
    public static void main(String[] args){
        String s1 = "a";
        String s2 = "b";
        String s3 = s1 + s2;
    }
}

然后我们运行一波然后反编译
这里是反编译出来的指令
在这里插入图片描述

这是变量表
在这里插入图片描述
我们可以看到有三个ldc,前面我们说过了,这个ldc其实就是从常量池里面去加载常量,然后我们看到注释里面也是加载了a,然后立马astore_1,也就是把这个存储到变量表里面的第一个位置,可以看到啊,Slot为1的变量确实是s1,然后后面和前面一样加载存储了b字符串。然后我们我们继续看,本来是应该执行s1+s2了,然后后面指令调用了new方法,我们看注释,确实是new方法,调用了StringBuilder
然后
在这里插入图片描述
这里调用了StringBuilder的空参构造方法 <init>表示初始方法,()V表示空参void的构造方法。
继续看
在这里插入图片描述
后面aload_1其实就是加载刚才的变量表Slot为1的变量,可以知道是s1,然后后面调用了append方法,同时对s2的操作亦是如此。
在这里插入图片描述
然后调用toString方法,并且存储到Slot为3的变量中,然后return。
可以说我们甚至了解了底层的字符串拼接。同时我们来说一个重要的问题
我们的常量是存在于常量池中的,但是字符串常量池却是在堆内存中的。我们的常量池一开始就存在,但是其实是一种懒加载的机制,本来就在常量池中有这个符号,但是其实是没有对象的,这个对象是当用到了比如String s1 = “a” 的时候才会去依照a这个常量创建出一个String对象,然后会把这个创建出来的对象放到串池(字符串常量池中)。然后我们知道我们刚刚创建s1+s2的时候没有到字符串常量池中去寻找,而是利用StringBuilder重新创建了一个,可见
“ab” == s3 应该是错误的,并且常量池中没有“ab”所以其实动态拼接出来的字符串是不会放到常量池中的。
在这里插入图片描述
确实是错误的。

然后我们来看另外一个例子

class Solution {
    public static void main(String[] args){
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = "a" + "b";
    }
}

可以看见这个例子,我们依然运行一下然后反编译

在这里插入图片描述
看图,我们可以看到前面8步都还是非常的容易看懂,都是从常量池获取符号然后变成对象存储到变量表中,然后我们前面说过对于s1 + s2这种操作在String拼接的底层是会调用StringBuilder的。但是这个确没有调用,我们来看,它也用了ldc去常量池中获取"ab"字符串。然后存储到s4中返回。所以啊,我们可以知道s4==s3应该是true的。实验一下
在这里插入图片描述
确实是没有问题的。
但是为什么s1+s2和"a" + "b"这两种情况不一样呢?这个其实是JDK在编译过程中对其进行的优化操作
因为“a” + "b"这个操作是两个常量在进行的操作,所以在编译过程中这个结果就已经确认了。所以创建对象的时候直接拿就完事了。但是s1+s2是两个变量进行的拼接操作,在编译期间是不可以确定结果的,所以使用了StringBuilder进行拼接和toString方法而不会去常量池中获取。
然后我们下面来验证一下JVM中的懒创建对象机制
我们调试启动
在这里插入图片描述
我们可以看见右下位置中String类型变量是3627个
在这里插入图片描述
然后我们尝试慢慢运行
在这里插入图片描述
我们发现运行到s2的申明的时候String的个数增加了,这个就说明了一个问题,虽然一开始常量池中华就存在了1这个符号但是其实是没有对象的在执行到了String s1 = "1"的时候才创建出的对象然后放到串池中的。
然后我们继续看
在这里插入图片描述
光标运行到下面的地方,s11="1"显然正常情况这个"1"的字符串对象已经存在了,只需要到串池中找就可以了。
在这里插入图片描述
所以我们运行到下面一步之后字符串数量并没有改变,所以这里用到了串池。

StringTable的特性

在这里插入图片描述
我们之前说过,常量拼接最后会变成常量,这是因为在编译期间的优化,然后变量拼接因为有不确定因素所以是使用了StringBuilder对象进行的。这就会导致变量拼接不会使用StringTable,我们还是来验证一下吧。

class Solution {
    public static void main(String[] args){
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = new String("a") + new String("b");
        System.out.println("ab" == ("a" + "b"));
        System.out.println(s3 == s4);
    }
}

结果
在这里插入图片描述
也确实是这样,s4就是两个不同的变量进行的拼接,而“a”+“b”却是常量拼接,最后结果显而易见,确实是变量拼接和串池无关,常量拼接和串池有关。

我们再来说一下刚刚说到的intern方法。
可以把不在常量池中的字符串加到常量池
在这里插入图片描述
看这个案例,s把自己加到了常量池中,然后返回给s2,它俩一定相同,然后s3是一个常量,从常量池里面找找到了s加进去的对象,所以s3其实和s也是相同的。
但是如果常量池里面有这个字符串,那么intern就加不进去
那么我们反这来一波看看
在这里插入图片描述

首先我们s3 = “ab“那么ab已经被加到常量池里面了,现在s是一个新的”ab“对象,然后尝试加到常量池中,但是常量池中已经有s3了所以没加进去,并且s2等于这个intern的返回值,也就是常量池中的s3,所以这个时候s3和s2相等,s2和s不一样。

直接内存

定义

在这里插入图片描述

读写性能原理

我们举一个例子,比如现在要进行一个文件的复制,这个时候我们有两种操作方案
我们可以用Java中常用的输入输出流,工作原理大概向下面这样
在这里插入图片描述
首先Java代码需要从文件中读取数据然后写到一个文件中,那么CPU最初还在执行Java代码,然后因为要调用系统的IO所以就会切换为内核态,然后调用native本地方法进行IO操作,然后数据先是从磁盘文件被读到系统内存中,Java再从内存中读取到Java的堆内存中进行使用,因为缓冲区相当于是数据的一个副本,一部分一部分的去读取。那么比如一个文件先要到系统缓存区然后再复制到Java缓冲区。这样非常浪费时间,所以如果需要优化这种操作我们就需要用到Java中的直接内存操作,ByteBuffer,这样就会使效率提高,那么它的工作原理如下
在这里插入图片描述
文件数据最初也是再磁盘文件上的额,然后这里开辟了一个直接内存空间,这个空间会把系统内存和Java堆内存连接起来,Java也就是Java可以直接操作这个内存空间,系统把数据从磁盘文件读取到直接内存空间之后Java堆内存就可以直接进行操作了。所以效率成倍增加

垃圾回收和内存溢出

那么这个东西这么好,那么涉及到了内存空间了,它会不会发生内存溢出呢?
其实也是会的,它会发生内存溢出,但是不是Java虚拟机中的什么堆内存溢出啥的,而是会抛出异常说直接内存溢出,因为它不属于JVM管理,也不会被JVM的GC进行回收,那么它会不会进行垃圾回收呢?
其实也是会的。这个东西也是会进行垃圾回收的。那么我们说过它不会被GC掉,那么它是怎么回收的呢?
来看一个例子

import java.nio.ByteBuffer;
import java.util.Scanner;

class Solution {
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        sc.nextLine();
        System.out.println("1");
        ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*200);
        sc.nextLine();
        System.out.println("2");
        bb = null;
        System.gc();
        sc.nextLine();
        System.out.println("结束-----");
    }
}

我们写了一个测试代码,使用ByteBuffer来申请一个直接空间,然后我们后面让这个bb=null,调用gc进行垃圾回收。
因为我们的直接内存空间不属于JVM所以我们要想观察这个内存占用我们就需要打开任务管理器查看内存情况
在这里插入图片描述
刚开始运行的时候还没有申请直接内存,这个时候注意那个12M的Java进程,然后我点击回车,申请空间
在这里插入图片描述
可以看到,立马就多出了大概200M的空间,然后我再按下回车调用gc
在这里插入图片描述
发现欸!内存占用下降了,这是为什么呢?
不是说好了这个直接内存是不会受到JVM的GC清理吗?但是为什么我调用了gc之后还是被清理了呢?
其实是这个样子的。我们打开这个ByteBuffer的源代码
**加粗样式
这里我们看到了这个地方申请直接内存的时候调用了一个ByteBuffer的实现,我们继续点进去
在这里插入图片描述
注意这个unsafe对象,关键就在这里,我们获取到了一个unsafe对象,这个类是JDK中专门用于申请和释放直接内存这种东西的。可以看见try中调用了unsafe.allocateMemory(size),这里就是申请了一块空间作为内存,返回的base是一个Long用来表示内存地址,然后有有一个unsafe.setMemory(base,size…)
这个就是申请这块空间注册到这个unsafe对象中。好,然后我们继续看倒数第二排
在这里插入图片描述
看到这个cleaner对象了吗?它的类型是
在这里插入图片描述
是一个清理器,里面调用create方法创建一个清理器,传入了bytebuffer对象自己,还new了一个Deallocator,

在这里插入图片描述
这个Deallocator是一个内部类实现了Runnable接口,其实就是一个回调接口,
在这里插入图片描述
这句代码的意思其实就是当this的这个对象被GC掉的时候就会调用Deallocator中的方法
调用的肯定是重写的Runnable中的run方法了
在这里插入图片描述
来看,里面有个关键的东西,还是这个unsafe对象,调用了freeMemory,很显然就是清理这个空间,释放掉,可以看到申请是他释放也是他,而且确实不是JVM中的作用
总结一下

在这里插入图片描述
那么还有一个问题,会有影响,就是这种情况,你看我们是手动调用gc的时候这个bytebuffer被gc,然后这个时候会调用回调清理空间,那么如果我不手动调用gc这个时候因为JVM的gc频率不是很高,所以空间一直得不到释放,那么就只能等到JVM自己gc的时候才会释放了,那么解决方案就是我手动调用gc,但是一般来说gc时间长影响效率,为了防止有的程序员多次再代码中进行gc操作很多时候会把JVM中的手动gc关闭,让这些gc的代码失效。就是下面这样。
在这里插入图片描述
这个时候我们的gc代码就失效了,那么我们正确的解决方案是什么呢?就是自己去操作这个unsafe对象了。

本次学习参考bilibili视频 黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值