jvm_01内存结构篇(代码片段)

爪洼ing 爪洼ing     2023-02-15     308

关键词:

JVM(Java Virtual Machine)

一、前言

1、什么是 JVM ?

1、定义:

  • Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。

2、好处:

  • 一次编译,处处执行
  • 自动的内存管理,垃圾回收机制
  • 数组下标越界检查

3、比较:

JVM、JRE、JDK 的关系如下图所示

2、学习 JVM 有什么用?

  • 面试必备
  • 中高级程序员必备
  • 想走的长远,就需要懂原理,比如:自动装箱、自动拆箱是怎么实现的,反射是怎么实现的,垃圾回收机制是怎么回事等待,JVM 是必须掌握的。

3、常见的 JVM

这里需要重点了解:JVM是一套规范,而我们也可以遵守这套规范实现自己的JVM(有能力的前提下!)

4、JVM整体预览

先做一个整体预览,然后逐个击破!

二、内存结构

1、程序计数器

1.1、概述:

JVM 中的程序计数器(Program Counter Register)有的时候也被称作PC寄存器,为了避免混淆这里解释一下,这里,并非是广义上所指的

物理寄存器,或许将其翻译为 PC 计数器(或指令计数器)更加贴切(也称为程序钩子),并且也不容易引起误会。

JVM 中 PC 寄存器是堆物理 PC 寄存器的一种抽象模拟。

1.2、特点:

  • 是线程私有的
  • 不会存在内存溢出(OOM)

1.3、作用:

PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。

2、虚拟机栈

栈帧:每个方法执行的时候需要的内存空间,其中占用空间的有(参数、局部变量、返回地址等)

当我们调用一个方法的时候,会在虚拟机栈当中给他开辟一个栈帧大小的空间,然后让栈帧入栈,方法执行完毕栈帧出栈!

定义:

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

相关问题:

1、垃圾回收是否涉及占栈内存 ?

答: 不会,因为我们的栈中的栈帧空间是用完就释放的!

2、栈内存的分配越大越好吗?

答:不是,由于我们的物理内存是有限的,一个栈的内存越大,能开线程越少,并发降低,其作用仅仅是增加了方法的递归调用

3、方法内的局部变量是否为线程安全的?

答:是的,局部变量是在线程私有的,不会与其他线程共享,不会产生线程安全问题(前提是局部变量没有逃离方法的作用范围!)

2.1、栈内存溢出

栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!

2.2、线程运行诊断

案例一:cpu 占用过多

解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

  • top 命令,查看是哪个进程占用 CPU 过高
  • ps H -eo pid (进程id), tid(线程id),%cpu | grep : 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
  • jstack 进程 id : 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。

案例二:长时间运行未出现结果(可能出现死锁)

解决方法:使用jstack工具 + 进程号,就会定位到死锁问题!

3、本地方法栈

一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。

4、堆

Heap 堆

  • 通过new关键字创建的对象都会被放在堆内存

特点 :

  • 它是线程共享,堆内存中的对象都需要考虑线程安全问题
  • 有垃圾回收机制

4.1、堆内存溢出

发生堆内存溢出会抛出 java.lang.OutOfMemoryError

可以使用 -Xmx8m 来指定堆内存大小。将堆内存调小就可以便于我们排查问题!

4.2、堆内存诊断

1、jps工具:查看当前系统中有哪些java进程 : jps

2、jmap工具 :查看堆内存的占用情况 : jmap - heap + 进程id

3、jconsole工具 : 图形化界面多功能的检测工具,可以连续监测

4、jvisualvm工具 : 相对jconsole更加强大的可视化工具

案例 : 垃圾回收后,内存占用仍然很高!

使用jvisualvm,启动可视化工具检测我们的虚拟机内存,找到HeapDump,对堆内存进行一个快照(内存转储),然后获取并且分析此时详细数据,定位原因!

5、方法区

首先看一个定义:我们的JVM规范中对于方法区(Method Area)的定义:

看一下内存结构

5.1、方法区内存溢出

  • 1.8 之前会导致永久代内存溢出

    使用 -XX:MaxPermSize=8m 指定永久代内存大小
    
  • 1.8 之后会导致元空间内存溢出

    使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
    

5.2、运行时常量池

首先先理解什么是常量池

1、我们可以通过堆一下代码得字节码文件进行反编译,拿到我们的反编译信息

//想要运行就需要被编译为二进制字节码(类基本信息,常量池,类方法定义,包含虚拟机指令)
public class ContentPoolTest 
    public static void main(String[] args) 
        System.out.println("Hello World");
    

2、找到字节码文件执行

javap -v ContentPoolTest.class

3、可以得出我们得字节码文件,反编译后的结果

然后我们对字节码文件进行说明

由此可以总结得出:常量池就是一张表,虚拟机指令根据这张常量表,去找到要执行的类名、方法名、参数类型、字面量等信息

我们一个类的字节码文件包含一个常量池,多个类一起运行的情况下,会将每个类的常量池表汇聚在一起,放在我们的运行时常量池

其中也不会是#1 #2 #3 这种地址,而是真实的内存地址!

5.3、串池 StringTable

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象【懒加载,只有当JVM指令用到的时候才会创建对象】
  • 利用串池的机制,来避免重复创建字符串对象 【主要是因为串池是HashTable实现的,底层是Hash表不可扩容】
  • 字符串变量的拼接原理:StringBuilder(1.8)
  • 字符串常量的拼接原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池子!

案例一

   //StringTable["a","b","ab"]
        String s1 = "a" ;  //当执行到此出处的时候会将a转为"a",然后放入StringTable中
        String s2 = "b" ;
        String s3 = "ab" ;
        String s4 = s1 + s2 ; //new StringBuilder().append("a").append("b").toString ; new String("ab")保存在堆中!
        String s5 = "a" + "b" ; //在编译期间已经优化,在编译期确定为ab ;
        System.out.println(s3 == s4); // false ;
        System.out.println(s3 == s5); // true

案例二

intern方法1.7以后


		String s = new String("a") + new String("b") ;
        //StringBuilder动态字符串拼接,保存在堆中【此时串池没有"ab"】
        System.out.println(s == "ab"); //false ;
        String str = s.intern();// 如果串池没有s,intern方法会将s的值传入串池当中,并且返回串池子中的对象
								// 如果串池中有s,s就不会入池但是,仍然会返回串池中的对象 【无论怎样s都会入池】
        System.out.println(str == "ab"); //返回true 
        System.out.println(s == "ab"); //true ; 

intern方法1.6以前

        String s = new String("a") + new String("b") ;
        //StringBuilder动态字符串拼接,保存在堆中【此时串池没有"ab"】
        System.out.println(s == "ab"); //false ;
        String str = s.intern();// intern方法会将s的值拷贝一份传入串池当中,并且返回串池中的对象 
		
	//	【注意:此时的s仍然是在堆中】
		
        System.out.println(str == "ab"); //返回true
        System.out.println(s == "ab"); //false ; 

5.4、StringTable 的位置

  • jdk1.6 StringTable 位置是在永久代中
  • jdk1.8 StringTable 位置是在堆中

5.5、StringTable 垃圾回收

  • -Xmx10m 指定堆内存大小
  • -XX:+PrintStringTableStatistics 打印字符串常量池信息
  • -XX:+PrintGCDetails打印GC信息
  • -verbose:gc 打印 gc 的次数,耗费时间等信息``
/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Code_05_StringTableTest 

    public static void main(String[] args) 
        int i = 0;
        try 
            for(int j = 0; j < 10000; j++)  // j = 100, j = 10000
                String.valueOf(j).intern();
                i++;
            
        catch (Exception e) 
            e.printStackTrace();
        finally 
            System.out.println(i);
        
    

5.6、StringTable 性能调优

  • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
  • 考虑是否需要将字符串对象入池,可以通过 intern 方法减少重复入池

6、直接内存

Direct Memory

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

6.1、使用直接内存的好处

文件读写流程:

因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。

使用了 DirectBuffer 文件读取流程

直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。

减少了不必要的复制操作

6.2、直接内存回收原理

直接内存的回收不是通过 JVM 的垃圾回收来释放的,,而是通过unsafe.freeMemory 来手动释放。

public class Code_06_DirectMemoryTest 

    public static int _1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException 	
//        method();
          method1();
     
    
    // 演示 直接内存 是被 unsafe 创建与回收
    private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException 

        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe)field.get(Unsafe.class);

        long base = unsafe.allocateMemory(_1GB);
        unsafe.setMemory(base,_1GB, (byte)0);
        System.in.read();

        unsafe.freeMemory(base);
        System.in.read();
    

    // 演示 直接内存被 释放
    private static void method() throws IOException 
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        System.out.println("分配完毕");
        System.in.read();
        System.out.println("开始释放");
        byteBuffer = null;
        System.gc(); // 手动 gc
        System.in.read();
    

直接内存的回收机制总结

使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法freeMemory 来释放内存

简述:一但BtyeBuffer这个这个java类被回收,就会将我们的直接内存释放!

然而我们一般用 jvm 调优时,会加上下面的参数:

-XX:+DisableExplicitGC  //禁止显示的 GC

意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。

所以我们就通过 unsafe 对象主动的调用 freeMemory 的方式释放内存。

jvm之内存与垃圾回收篇虚拟机栈(代码片段)

...样的功能需要更多的指令。有不少Java开发人员一提到Java内存结构,就会非常粗粒度地将JVM中的内存区理解为仅有Java堆(heap)和Java栈(stack)?为什么?首先栈是运行时的单位,而堆是存储的单位。 查看详情

jvm之内存与垃圾回收篇执行引擎(代码片段)

执行引擎执行引擎概述执行引擎属于JVM的下层,里面包括解释器、及时编译器、垃圾回收器执行引擎是Java虚拟机核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理... 查看详情

jvm技术专题深入挖掘java对象的内存结构「原理篇」(代码片段)

...有充分的时间。📕基本概念在JVM虚拟机种Java对象的内存结构如图所示分为三大块:对象头(ObjectHeader)、实例数据(InstanceData)、对齐填充(Padding)。对象头:标记字段、类型指针、数组长度... 查看详情

jvm基础认知篇(上)(代码片段)

...的工作,相对于C、C++语言来说不用我们去管理内存 查看详情

了解jvm和jvm内存结构(jvm运行时数据区)(代码片段)

上一篇:Java线程池使用详解之前的文章中,我们大多是了解并发是怎么回事儿,怎么解决并发问题,juc给我们提供锁都有什么效果,是如何使用的。实际上,了解完前面的知识,日常的并发问题大多都... 查看详情

面试题jvm篇-10道常见面试题(代码片段)

文章目录【面试题】JVM篇-10道常见面试题1.说一下JVM的内存整体的结构(运行时数据区)和各自的功能?2.说一下如何判断一个对象是否可以回收?3.谈谈对Java中引用的了解?4.常用的垃圾收集算法有哪些?... 查看详情

jvm内存堆栈监控之jmap篇(代码片段)

...得运行中的jvm的堆的快照,从而可以离线分析堆,以检查内存泄漏,检查一些严重影响性能的大对象的创建,检查系统中什么对象最多,各种对象所占内存的大小等等jmap(linux下特有,也是很常用的一个命令)观察运行中的jvm物... 查看详情

java复习基础篇—-jvm内存结构(转)

主要内容如下:JVM启动流程JVM基本结构内存模型编译和解释运行的概念 一、JVM启动流程:JVM启动时,是由java命令/javaw命令来启动的。二、JVM基本结构:JVM基本结构图:《深入理解Java虚拟机(第二版)》中的描述是下面这个... 查看详情

图解jvm内存结构(代码片段)

文章目录JVM内存结构JVM包含哪几部分?在内存中java代码的执行的流程Java内存分布:各组件详细说明1.程序计数器2.Java虚拟机栈3.本地方法栈4.Java堆5.方法区6.运行时常量池7.直接内存总结问题那些区域会发生内存溢出?... 查看详情

jvmday01jvmjvm内存结构直接内存(代码片段)

(目录JVMJVM内存结构1、程序计数器(线程私有)2、虚拟机栈(线程私有)3、线程运行诊断4、本地方法栈(线程私有)5、堆(线程共享)6、方法区(线程共享)7、stringtable(垃圾回收... 查看详情

jvm内存结构和垃圾回收(代码片段)

JVM内存结构和垃圾回收Java内存管理机制JAVA运行时区域栈内存堆内存的构成永久代JVM结构总结Java8新变化垃圾回收永久代的垃圾回收垃圾收集器前言在做性能测试之前,首先我们需要了解JVM的内存结构和垃圾回收机制。只有了... 查看详情

jvm入门——jvm内存结构(代码片段)

...r及其子类来完成JVM的类加载  3.类执行:字节码被装入内存,进入JVM虚拟机,被解释器解释执行   注:Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,       查看详情

jvm内存结构(代码片段)

文章目录JVM-内存结构1.程序计数器1-1定义1-2作用1-3特点2.虚拟机栈2-1定义2-2演示2-3常见问题2-4栈内存溢出2-5线程运行诊断3.本地方法栈4.堆4-1定义4-2特点4-3堆内存溢出4-4堆内存诊断5.方法区5-1定义5-2结构5-3方法区内存溢出5-4通过反... 查看详情

java虚拟机详解02----jvm内存结构(代码片段)

主要内容如下:JVM启动流程JVM基本结构内存模型编译和解释运行的概念 一、JVM启动流程:JVM启动时,是由java命令/javaw命令来启动的。二、JVM基本结构:JVM基本结构图:《深入理解Java虚拟机(第二版)》中的描述是下面这个... 查看详情

jvm之内存结构(代码片段)

...是怎么执行的`JVM`扮演了什么角色?【3】JVM的内存划分一、Java虚拟机栈二、本地方法栈三、方法区四、堆【1】JDK、JRE、JVM的区别和联系JRE(JavaRuntimeEnvironment,Java运行环境)是Java 查看详情

jvm技术专题深入研究jvm内存逃逸原理分析「研究篇」(代码片段)

前提概要JVM的内存分配主要在是运行时数据区(RuntimeDataAreas),而运行时数据区又分为了:方法区,堆区,PC寄存器,Java虚拟机栈(就是栈区,官方文档还是叫Java虚拟机栈),本地方法区,内存逃逸主... 查看详情

jvm--11---直接内存(代码片段)

文章目录直接内存DirectMemory直接内存是在Java堆外的、直接向系统申请的内存区间。NIONIO--01--BIO,NIO,AIO简介缓冲区(Buffer)[NIO--02--NIO简介和缓冲区(Buffer)](https://blog.csdn.net/weixin_48052161/article/ 查看详情

(超详解)jvm-内存结构(代码片段)

文章目录JVM-内存结构1.程序计数器1-1定义1-2作用1-3特点2.虚拟机栈2-1定义2-2演示2-3常见问题2-4栈内存溢出2-5线程运行诊断3.本地方法栈4.堆4-1定义4-2特点4-3堆内存溢出4-4堆内存诊断5.方法区5-1定义5-2结构5-3方法区内存溢出5-4通过反... 查看详情