JVM_01 内存结构篇

Posted 爪洼ing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM_01 内存结构篇相关的知识,希望对你有一定的参考价值。

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_01 内存结构篇的主要内容,如果未能解决你的问题,请参考以下文章

详解Jvm内存结构

详解Jvm内存结构

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

JVM升级篇六(JVM内存模型)

黑马程序员JVM教程笔记完整目录

黑马程序员JVM教程笔记完整目录