JVM(二)内存区域

一、内存处理流程

  • 申请内存

    ​ 通过配置参数或者默认的参数向操作系统申请内存,根据内存的大小找到内存段的起始地址和结束地址分配给JVM,由JVM进行内部分配。

  • 初始化运行数据区

    ​ 根据参数进行堆、方法区、栈的分配。

  • 类加载(后面的文章会详解)

    ​ 将class、常量、静态属性放到方法区,对象放在堆中。

  • 创建对象

    ​ 创建线程,分配内存给虚拟机栈,虚拟机栈(操作数栈)创建对象,局部变量表存储对象引用,对象放在堆中。

二、堆空间分代划分

​ 堆总被分为两个部分:新生代和老年代,其中新生代中又被分为Eden区和Survivor区,Survivor区由Form Survivor和To Survivor组成(具体的GC、对象分配方面会在后面的文章讲到)。

三、栈优化

​ 在栈帧中一般来说两个栈帧是不会相互有关系的,都是独立存在的。但是在某些情况下,会使两个独立的栈帧会有一个共享的数据区,此行为为栈的优化。

public class JVMTest01 {
    public int add(int num){
           return num + 2;
    }
    public static void main(String[] args) {
        JVMTest01 jvmTest01 = new JVMTest01();
        jvmTest01.add(50); //此时50就是add栈帧和main栈帧的共享数据
    }
}


​ 此时50就是main方法操作数栈的数据,同时也是add栈帧的局部变量表中的数据。此情况为栈优化。

三、JHSDB可视化JVM

JHSDB详情

四、内存溢出OOM

  • 堆异常

    ​ 内存溢出:申请的内存空间超过了堆最大的内存空间。

    ​ 若为内存溢出,则可以通过-Xms-Xmx参数来调整堆内存的大小。如果不是就是代码写的有问题,对象的生命周期太长或者存储结构上的设计不合理。

  • 方法区异常

    ​ 运行时常量池溢出。

    ​ 方法中的class对象占用的内存太多,超过了设置的内存大小。

  • 栈异常

    ​ 在Hotspot版本中的vm是不可以给栈设置大小,只能设置栈中的每个线程占用内存的大小。

    ​ 每调用一个方法都会创建一个栈帧,所以一般来说出现的栈的oom基本上就是递归的问题。

    同时要注意,栈区的空间JVM 没有办法去限制的,因为JVM 在运行过程中会有线程不断的运行,没办法限制,所以只限制单个虚拟机栈的大小。

  • 直接内存异常

    ​ 直接内存的容量可以通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM 异常;
    ​ 由直接内存导致的内存溢出,一个比较明显的特征是在HeapDump 文件中不会看见有什么明显的异常情况,如果发生了OOM,同时Dump 文件很小,可以考虑重点排查下直接内存方面的原因。

五、常量池

  • class常量池(静态常量池)

    ​ 主要存放字面量和符号引用。

    字面量:就是变量的值,例如:String str = "Hello" ,'Hello'就是str的字面量。同理,int、double等等都是这样。

    符号引用:就是在类被编译的时候会被编译成一个class字节码文件,如果类中存在其他类的方法的在编译的时候是不止到其他类的内存地址的,那么就用一个占位符(符号)来代替这个其他类,在类加载的时候就会把符号替换类的引用(内存地址)。

    ​ 例如:Student类中有调用Tool类中的一个方法,在编译的时候就不知道这个Tool类的引用(内存地址),那么就会用com.xxx.Tool来代替。之后在类被加载的时候就会被替换成具体的引用(内存地址)。

  • 运行时常量池

    ​ 在类被加载的时候会把符号转换为具体的引用(内存地址)时,会把引用(内存地址)保存到这个区域。

    ​ 在JDK1.7 被放在了堆中,但是理论中还是属于方法区(JVM规范中)。

    ​ JDK1.8 之后由元空间来代替的永久代实现方法区,但是理论上还是隶属于方法区。

  • 字符串常量池

    ​ 字符串常量池在网上是比较有争议的一个存在,实际在官方文档或者JVM规范中并没有这个概念。

    ​ 在JDK1.8中是存放在堆中的,且于Sting类有很大的关系。这块内存的存在使Sting可以被更高效的使用,从而提升了的整体性能。

    String (JDK 1.8):

​ 在String源码中可以看到String是被final所修饰的,它的底层为char数组同时也是被final所修饰。因此可以知道String一旦被赋值后就不可被更改,那么这样做的话会有什么好处呢?

​ (1)保证了String对象的安全性。

​ (2)保证它的hash值不会被频繁变动,使得HashMap容器才能实现k-v的缓存实现。

​ (3)可以实现字符串常量。

String创建方式

String str = "abc";
//此方法创建的话,首先会去常量池中查找是否存在abc这个常量,有的话就直接返回引用,没有就创建。

String str1 = new String("abc");
//这个方法比起上面,在堆中多了一个String对象,而这个String对象则指向常量池中的abc常量,所以str1首先指向的是堆中String对象而不是常量池中的abc。

public class Student {
    String name;
    String gender;
   public static void main(String[] args) {
        Student stu = new Student();
        stu.setName("小明");
    }
}
//此地的字符不会被存放在字符串常量池中,只会被存在的堆中。

String str2 = "a" + "b" + "c";
//这个在目前的主流编译器在被编译的时候会被优化成 String str2 = "abc"。以前则会生成 a、b、c、ab、abc这几个对象。

String str1 = new String("abc").intern();
//在对象中调用intern方法,会和 String str1 = "abc" 等效。

posted @ 2021-01-27 22:17  某人人莫  阅读(160)  评论(0编辑  收藏  举报