欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【JVM】-- JVM内存结构

程序员文章站 2022-03-13 10:33:41
...


JVM的内存结构一般指Java的运行时数据区:

【JVM】-- JVM内存结构

由方法区,堆区,虚拟机栈,程序计数器和本地方法栈组成。下面我们依次介绍这5部分。

1.程序计数器(Program Counter Register)

程序计数器:记录下一条要执行的JVM指令的执行地址,字节码解释器工作时就是通过改变程序计数器中的值来获取下一条要执行的指令,分支,循环,跳转,异常处理线程恢复等功能都是依赖程序计数器来完成。

程序计数器存放在cpu的寄存器中。每条线程都有自己独立的程序计数器,各个程序计数器互不影响,故程序计数器为线程隔离的数据。

如果当前执行的是Java的非native方法,那么程序计数器存储下一条要执行的指令。如果是native方法那么程序计数器中为null。

特点:

  • 线程私有
  • 无内存溢出

2.Java虚拟机栈(VM Stack)

Java虚拟机栈和程序计数器一样,都是线程私有的,它的生命周期与线程相同。

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

Java虚拟机栈的内存模型:

【JVM】-- JVM内存结构

有关Java虚拟机栈的问题

1.Java虚拟机栈内存是否受到内存回收机制的管理?

不会,因为在每次执行完方法后,内存会自动释放。当然线程执行完成后内存也是自动释放。

2.Java虚拟机栈内存分配越大越好?

不是,合适即可,如果栈内存分配过小容易造成内存溢出,而如果内存过大,那么Java开启的线程数就会变小,降低了Java代码的运行效率。

设置虚拟机栈的大小的虚拟机指令:

【JVM】-- JVM内存结构

3.方法内局部变量是否线程安全?

  • 如果方法内局部变量的只作用在方法内部,那么是线程安全的
  • 如果局部变量引用了对象,或作为返回值返回那么就需要考虑线程安全问题。

虚拟机栈的内存溢出

  • 栈帧过多会引发栈溢出(java.lang.*Error)。如程序中出现死循环时,造成虚拟机栈中不断创建栈帧导致内存溢出。
  • 栈帧过大时,也会造成内存溢出(OutofMemoryError)。如果虚拟机栈可以动态扩展,如果扩展到大小超过Java虚拟机规范中的规定的大小时也会造成溢出。

3.本地方法栈(Native Method Stack)

本地方法栈与Java虚拟机栈的作用非常相似,不过Java虚拟机栈调用的是Java方法(即二进制字节码),而本地方法栈调用虚拟机使用到的Native方法。虚拟机规范并未强制规定,该方法的具体实现的语言。

与虚拟机栈一样本地方法栈也会抛出*Error和OutofMemoryError异常。

4.堆(heap)

定义

堆是Java虚拟机管理中内存最大的一块,是一块线程共享的区域。在虚拟机启动时创建堆区。堆区的唯一目的是存放对象实例(即new出来的对象)。

Java堆是垃圾回收器管理的最主要区域,因此也被称为GC堆

堆内存溢出

Java堆内存溢出是堆中最常见的异常,出现的原因是当进程中不断有新的对象创建而老对象有在使用导致不能被垃圾回收机制回收,就会出现堆内存溢出情况

可设置堆内存的大小来观测堆内存溢出情况:

设置堆内存大小的参数是-Xmx size

-Xmx8m
java.lang.OutOfMemoryError: Java heap space

内存溢出异常
示例:

/**
 * java.lang.OutOfMemoryError:内存溢出问题
 * 设置堆的大小:
 * -Xmx8m
 * 设置为8m
 */
public class heapdemo {
    public static void main(String[] args) {
        int count = 0;
        List<String> list = new ArrayList<>();
        String str = "hello";
        try {
            while (true){
                list.add(str);
                str+=str;
                count++;
            }
        }catch (Throwable throwable){
            throwable.printStackTrace();
            System.out.println(count);
            System.out.println(str.length());
        }
    }
}

输出结果:

java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
	at java.lang.StringBuilder.append(StringBuilder.java:136)
	at wf.memo.heapdemo.main(heapdemo.java:21)
26
335544320

5.方法区(Method Area)

方法区( Method Area )与Java堆一样,是各个线程共享的内存区域**,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据**。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

组成

方法区在1.6版本以后发生了很大的改变,1.6以前方法区在一个叫永久代,此时方法区的数据被存储在堆中,有虚拟机进行管理。而在1.7后把方法区中的stringTable(字符串常量池)移出永久代还是存放在堆中。1.8以后把剩下的部分称为元空间(MetaSpace)并把元空间移出堆,存入内存中。

【JVM】-- JVM内存结构

方法区的内存溢出

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

演示永久代内存溢出java. lang . OutOfMemoryError: PermGen space

-XX:MaxPernSize=8m

1.8以后会导致元空间内存溢出

演示元空间内存溢出java . lang . OutOfMemoryError: Metaspace

  • -XX:MaxMetaspaceSize=8m

因为方法区主要存储的是被加载的Java的类文件信息,故如果使用cglib等代理方式在代码运行的过程中,不断加载新的lei,那么就可能引起内存溢出。

方法区内存溢出实例

/**
 * -XX:MaxMetaspaceSize=size
 */
public class Mareademo extends ClassLoader {
    public static void main(String[] args) {
        int count=0;
        try {
            Mareademo mareademo = new Mareademo();
            for (int i = 0; i < 100000; i++) {
                //ClassWriter作用是生成类的二进制字节码
                ClassWriter classWriter = new ClassWriter(0);
                //visit()方法各个参数的意义:1.Java的版本号,2.类修饰符(public);3.类名称;4.包名;4.父类;5.实现的接口
                classWriter.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"class"+count,null,"java/lang/Object",null);
                //返回byte数组及二进制字节码文件
                byte[] bytes = classWriter.toByteArray();
                //加载二进制文件
                mareademo.defineClass("class"+count,bytes,0,bytes.length);
                count++;
            }
        }finally {
            System.out.println(count);
        }
    }
}

在执行代码时因为方法区存在于本地内存所以可以放下的我们申请的类,所以我们在运行前需要设置参数减小方法区的大小,才能观察到内存溢出现象。
输出:

5411
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at wf.memo.Mareademo.main(Mareademo.java:23)

6.运行时常量池

运行时常量池( Runtime Constant Pool )是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池( Constant Pool Table ),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

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

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

我们来了解一下一个具体实例中常量池的信息有哪些:

二进制字节码由:类基本信息,常量池,类方法定义及虚拟机指令

//类基本信息
Classfile /E:/java/idea/ym/jvm/out/production/jvm/wf/memo/Demo.class
  Last modified 2020-2-11; size 531 bytes
  MD5 checksum c8ad1fc04006e932e4edcd9c9cfcebba
  Compiled from "Demo.java"
public class wf.memo.Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
//常量池
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // wf/memo/Demo
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lwf/memo/Demo;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Demo.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               wf/memo/Demo
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V、
//类方法定义
{
//默认的构造方法
  public wf.memo.Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lwf/memo/Demo;
//main方法
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}

在执行nain方法是主要是执行

0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc           #3                  // String hello world
5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

指令,getstatic 后面接 #2所以执行到getstatic时会到常量池中查询#2对应的值,#2的Fieldref表示是一个成员变量,而#2又对应#20和#21继续查找最终可以确定,getstatic是一个java/io/PrintStream所调用的 java /lang / System的out。

7.StringTables

存放在常量池中的类信息,都会被加载到运行时常量池中,但是如果类中存在字符串,那么字符串在刚载入运行时常量池时不会立即变为对象,只是常量池中的符号,只有执行的代码调用相应的字符串时,才会把字符串变为对象。这是一种懒加载。

特性:

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理:是StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化

String的intern()方法:

可以使用intern方法,主动将串池中还没有的字符串对象放入串池

  • 1.8 版本以后将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
  • 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,则放入串池,会把串池中的对象返回

面试题:

public class StringTableDemo {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        String s5 = "a" + "b";
        String s6 = s4.intern();
        String s7 = "a" + s2;
        System.out.println(s3 == s4);//false
        System.out.println(s3 == s5);//true
        System.out.println(s3 == s6);//true
        System.out.println(s3 == s7);//false
        String x1 = new  String("c") + new String("d");
        String x2 = x1.intern();
        String x3 = "cd";
        System.out.println(x1 == x2);//true
        System.out.println(x3 == x1);//true
    }
}

分析:

  • s3 == s4为false因为s3是字符串常量存在有StringTable(以下称:串表)中,而s4是由s1和s2两个字符串对象拼接而成,对象是可能变化的,所以s4对象是拼接过程中新new出来的(其实两个字符串对象拼接的过程就是new一个StringBuild然后往StringBuild中append添加函数的过程。),而且s4使用intern()函数前字符串"ab"在串表中已经存在,不会把s4的对象插入串表,所以为false
  • s3 == s5为true。因为s5是"a"和"b"两个字符串常量拼接而成,常量不会变化所以jvm会在编译其对常量字符串的拼接进行优化,直接把拼接后的结果存入串表中。
  • s3 == s6为true。因为intern函数无论调用该函数的字符串是否在串表中存在,函数都会把串表中的对应字符串返回,故s6和s3的地址相同为false
  • s3 == s7为false。因为s7拼接中也涉及到了变量的操作,而字符串拼接过程中一旦变量参与拼接,都会new一个StringBuild。
  • x1 == x2为true。因为x1调用intern函数时串表中不存在"cd"字符串,所以会把x1的地址添加到串表中,而该地址给x2返回故二者相等。
  • x3 == x1为true。因为Java字符串只有在调用时才会为其生成对象,在调用前这是常量池中的符号。而常量对象会从串表中取地址,如果不存在会创建新的对象返回地址。故在调用 String x3 = “cd”;时才会检测串表中是否存在 “cd”,因为已经存在故会把串表中的字符串地址返回给x3.

StringTable也会发生垃圾回收。

StringTable的性能调优:在使用String对象时,可以先调用一下intern函数把字符串入串表,来减少对象的空间占用。

8.直接内存

直接内存并不属于JVM内存结构,即直接内存被不受JVM的管理。直接内存( Direct Memory )并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。

在JDK 1.4中新加入了NIO ( New Input/Output )类,引入了一种基于通道( Channel )与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutCfMemoryError异常。

引入直接内存的原因

【JVM】-- JVM内存结构

这是不使用直接内存时,Java操作磁盘文件的过程。我们可以看到,如果Java想使用磁盘文件的内容,那么需要先把数据读入系统缓存区,然后从系统缓存区复制一份,给Java的缓冲区,才能使用。

而如果我们使用直接内存:

【JVM】-- JVM内存结构

那么表示Java可以直接使用内存的一部分,所以Java操控磁盘文件时,当文件写入系统的内存后,Java可以直接使用,不在进行复制到Java自己缓存区的操作,大大的提供了程序运行的效率。

我们可以使用代码来演示使用与不使用直接内存的情况,来观察二者的效率:

public class DirectDemo {
    private static String from = "F:\\game\\view.mp4";
    private static String to = "F:\\h.mp4";
    private static int SIZE = 1024*1024;
    public static void main(String[] args) {
        io();
        directBuffer();
    }
    private static void io() {
        long start = System.currentTimeMillis();
        try {
            FileOutputStream outputStream = new FileOutputStream(to);
            FileInputStream inputStream = new FileInputStream(from);
            byte[] bytes = new byte[SIZE];
            while (true){
                int read = inputStream.read(bytes);
                if (read == -1){
                    break;
                }
                outputStream.write(bytes,0,read);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("no direct use time:" + (System.currentTimeMillis()-start));
    }
    private static void directBuffer() {
        long start = System.currentTimeMillis();
        try {
            FileChannel outputStream = new FileOutputStream(to).getChannel();
            FileChannel inputStream = new FileInputStream(from).getChannel();
            ByteBuffer bb = ByteBuffer.allocateDirect(SIZE);
            while (true){
                int read = inputStream.read(bb);
                if (read == -1){
                    break;
                }
                bb.flip();
                outputStream.write(bb);
                bb. clear();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("direct use time:" + (System.currentTimeMillis()-start));
    }
}

输出结果:

no direct use time:423
direct use time:169
no direct use time:428
direct use time:169
no direct use time:425
direct use time:196

执行三次可以看到,使用直接内存的效率总是优于不使用直接内存。

直接内存的内存溢出

虽然直接内存不由JVM管理,但是它仍然会存在内存溢出问题。

案例:

public class StringTableDemo2 {
    public static void main(String[] args) {
        List list = new ArrayList();
        int count = 0;
        try {
            while (true){
            //ByteBuffer.allocateDirect方法可以分配在直接内存分配空间
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024*1024*100);//100m
                list.add(byteBuffer);
                count++;
            }
        }catch (Throwable throwable){
            System.out.println(count);
            throwable.printStackTrace();
        }
    }
}

输出结果:

36
java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at wf.memo.StringTableDemo2.main(StringTableDemo2.java:19)

可以看到循环了36次才释放内存(3.6g左右),即直接内存由上限,如果超过就会导致内存溢出。

直接内存的内存回收

虽然直接内存不受JVM管理,但是Java可以通过程序来控制直接内存的释放和申请。Java中的Unsafe就提供了对直接内存的回收和释放。而Unsafe类在具体的直接内存的申请和释放是native修饰即为本地方法。故就如之前所说Java的确不能控制直接内存的申请和释放。但Java可以通过其他语言对内存的操作来实现内存的使用。从而变相的实现JVM对直接内存的管理。

示例;

public class UnsafeDemo {
    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        int size = 1024*1024*1024;
        System.out.println("等待开始。。");
        System.in.read();
        System.out.println("资源开始写入。。。");
        long l = unsafe.allocateMemory(size);
        unsafe.setMemory(l,size,(byte)0);
        System.in.read();
        System.out.println("资源释放");
        unsafe.freeMemory(l);
        System.in.read();
    }
//我们不能直接获取Unsafe对象,故需要通过反射实现
    private static Unsafe getUnsafe(){
        Field theUnsafe;
        Unsafe unsafe = null;
        try {
            theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return unsafe;
    }
}

该类实现了对内存的操作,在运行该类后,可以查看任务管理器,可以看到在确定该类后,会增加Java程序对内存的使用量。
直接内存的分配与释放原理:

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

禁用显示回收对直接内存的影响:

-XX:+DisableExplicitGC

命令可以关闭显示的调用System.gc(),即在代码中使用该gc方法不会起任何作用。而有时我们要可能因为某些原因关闭显示调用gc方法,此时我们申请的直接内存得不到释放。所以如果我们想在程序中直接使用直接内存,那么最好在使用完成后调用Unsafe.freeMemory()方法对内存进行回收。和c++程序一样。

相关标签: jvm