阅读 327
Java程序运行内存分析

Java程序运行内存分析

这是我参与8月更文挑战的第30天,活动详情查看:8月更文挑战

1、基本概念

​ 每运行一个java程序会产生一个java进程,每个java进程可能包含一个或者多个线程,每一个Java进程对应唯一一个JVM实例,每一个JVM实例唯一对应一个堆,每一个线程有一个自己私有的栈。进程所创建的所有类的实例(也就是对象)或数组(指的是数组的本身,不是引用)都放在堆中,并由该进程所有的线程共享。Java中分配堆内存是自动初始化的,即为一个对象分配内存的时候,会初始化这个对象中变量。虽然Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在栈中分配,也就是说在建立一个对象时在堆和栈中都分配内存,在堆中分配的内存实际存放这个被创建的对象的本身,而在栈中分配的内存只是存放指向这个堆对象的引用而已。局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。

2、java虚拟机运行时数据区

image-20210502122208977

2.1 方法区(线程共享)

  • 方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集和压缩;

  • JDK1.8 中取消了永久代,取而代之使用了元空间来实现方法区。

  • 用于存放被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等(要创建一个类的实例,首先把该类的代码加载到方法区中,并且初始化);

  • 方法区中包含的都是整个程序中永远唯一的元素,class、static变量;

  • 字符串常量池和运行时常量池逻辑上属于方法区中,但实际存放在堆内存中。

拓展:java的三种常量池

==各常量池所在的位置待求证?==

  • 全局字符串常量池

    • 类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中;(string pool 中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是key(字面量“abc”, 即驻留字符串)-value(字符串"abc"实例对象在堆中的引用)键值对,也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享(享元模式)
    • 字符串常量池里放什么?
      • JDK 6.0及之前的版本,String Pool 里放的都是字符串常量;
      • JDK 7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。
    • String的intern()方法
      • JDK 6.0中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串的实例的引用,而StringBulder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。
      • JDK1.7中,intern()的实现不会在复制实例,只是在常量池中记录首次出现的实例引用,因此返回的是引用和由StringBuilder.toString()创建的那个字符串实例是同一个。
    • 字符串常量池在Java内存区域的哪个位置?
      • 在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
      • 在JDK7.0版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。
    • new String("abc") 生成了几个对象?
      • 常量池中没有 "abc",两个;首先在常量池中查找是否是否存在 "abc" ,不存在,"abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量;而使用 new 的方式会在堆中创建一个字符串对象。
  • class文件常量池

    • 我们写的每一个Java类被编译后,就会形成一份class文件,class文件中处理包含 类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池;

    • 用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)

      • 字面量包括:1.文本字符串;2. 八种基本类型的值;3.被声明为final的常量等;
      • 符号医用包括:1.类和方法的全限定名;2.字段的名称和描述符;3.方法的名称和描述符;
    • 每个class文件都有一个class常量池。

  • 运行时常量池

    • 类加载到内存中后,jvm将class常量池中的内容存放到运行时常量池中;不同的是:它的字面量是可以动态的添加(String#intern()),符号引用可以被解析成直接引用;

      • 符号引用:即用用字符串符号的形式来表示引用,其实被引用的类、方法或者变量还没有被加载到内存中;
      • 直接引用:则是有具体引用地址的指针,被引用的类、方法或者变量已经被加载到内存中;
      • 那为什么要用符号引用呢?这是因为类加载之前,javac会将源代码编译成.class文件,这个时候javac是不知道被编译的类中所引用的类、方法或者变量他们的引用地址在哪里,所以只能用符号引用来表示。
    • JVM在执行某个类的时候,必须经过加载、链接、初始化,而链接又包括验证准备解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

    • 类加载过程:(第一次使用该类)

      www.jianshu.com/p/dd3965423…

      • 加载:类加载器(双亲委派模式)通过全限定名读取类的二进制流到 JVM 内部,存储在运行时内存的方法中,java对中生成一个代表该类的 java.lang.Class 对象,作为方法区数据的访问入口;
      • 链接
        • 验证:比如 final 类不能被继承,final方法不能被重写;
        • 准备:为静态变量分配内存;
        • 解析:将常量池中 符号引用(如类的全限定名)转为 直接引用(类在实际内存中的地址);
      • 初始化(先父后子):为静态变量赋值、执行 static 代码块。

2.2 堆区(线程共享)

  • 主要存放对象实例和数组,每个对象都包含一个与之对应的class的信息(class的目的是得到操作指令);
  • 可以位于物理上不连续的空间,但是逻辑上要连续。

2.3 虚拟机栈(线程私有)

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表操作数栈动态链接方法返回地址等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

2.4 本地方法栈(线程私有)

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowErrorOutOfMemoryError 异常。

* 在 Java 语言中 堆(heap)和栈(stack)的区别

  • 栈(stack)和堆(heap)都是 Java 用来在 RAM 中存放数据的地方;与 C++ 不同,Java 自动管理栈和堆,程序员不能直接设置栈或堆;
  • 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享
  • 堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
  • 栈数据可以共享:
    • 编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,如果没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
    • 这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
文章分类
后端
文章标签