JVM第三篇-内存结构

内存结构

一、程序计数器(线程私有)

程序计数器是一块较小的内存空间,是当前线程所执行的字节码的行号指示器

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻。一个处理器都只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都要有一个独立的程序计数器,各条线程之间互不影响,独立存储,我们称这类内存区域为"线程私有"的内存。

如果线程正在执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法,计数器值为空,此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemeoryError情况的区域

二、虚拟机栈(线程私有)

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 局部变量表: 存放了Java虚拟机基本数据类型、对象引用类型
  • 操作数栈: 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
  • 动态链接
  • 方法出口等信息

在《Java虚拟机规范中》,对这个内存区域规定了两种异常状况: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常

2.1 设置栈大小(-Xsssize | -XX:ThreadStackSize)

  • 一般默认为512k-1024k,取决于操作系统
  • 栈的大小直接决定了函数调用的最大可达深度。
  • jdk5.0之前,默认栈大小: 256k,jdk5.0之后,默认栈大小: 1024k(linux\mac\windows);

设置线程堆栈大小(以字节为单位)。附加字母k或K以指示 KB,m或M指示 MB,g或G指示 GB。默认值取决于平台:

以下示例以不同的单位将线程堆栈大小设置为 1024 KB:

	-Xss1m 
	-Xss1024k 
	-Xss1048576

测试代码

  • 经过测试,使用全名的参数有问题,使用缩写才起作用。
public class StackErrorTest {
    
    
    private static int count = 1;

    public static void main(String[] args) {
    
    
        /*try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
        try {
    
    
            count++;
            main(args);
        }catch (Throwable e){
    
    

            System.out.println("递归的次数为:" + count);
        }



    }

}

无参时

在这里插入图片描述

-Xss1024k

在这里插入图片描述

-Xss512k

在这里插入图片描述

-Xss256k

在这里插入图片描述

2.2 方法和栈帧之间存在怎样的关系?

  • 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
    -

2.3 栈帧内部结构

  • 局部变量表
  • 操作数栈
  • 动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)
  • 方法出口(或方法正常退出或者异常退出的定义)

在这里插入图片描述

局部变量表

  • 局部变量表也被称之为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型(8种)、对象引用(reference),以及returnAddress类型。
  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
  • 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

关于Slot的理解

public class LocalVariablesTest {
    
    
    private int count = 0;

    //(这个方法占了3个slot,args,test,num)
    public static void main(String[] args) {
    
    
        LocalVariablesTest test = new LocalVariablesTest();
        int num = 10;
        test.test1();
    }
    //关于Slot的使用的理解(这个方法占了3个slot,实例方法默认有个this变量)
    public void test1() {
    
    
        Date date = new Date();
        String name1 = "atguigu.com";
        test2(date, name1);
        System.out.println(date + name1);
    }

    //(这个方法占了3个slot,静态方法没有this变量)
    public static void testStatic(){
    
    
        LocalVariablesTest test = new LocalVariablesTest();
        Date date = new Date();
        int count = 10;
        System.out.println(count);
        //因为this变量不存在于当前方法的局部变量表中!!
//        System.out.println(this.count);
    }

    //(这个方法占了1个slot,有个this变量)
    public LocalVariablesTest(){
    
    
        this.count = 1;

    }

    //(这个方法占了6个slot,实例方法默认有个this变量,double占两个slot)
    public String test2(Date dateP, String name2) {
    
    
        dateP = null;
        name2 = "songhongkang";
        double weight = 130.5;// double 和 long 会占据两个slot
        char gender = '男';
        return dateP + name2;
    }

    public void test3() {
    
    
        this.count++;
    }

    //(这个方法占了3个slot,实例方法默认有个this变量)
    public void test4() {
    
    
        int a = 0;
        {
    
    
            int b = 0;
            b = a + 1;
        }
        //变量c的位置:复用了变量b的slot
        int c = a + 1;
    }

    public void test5Temp(){
    
    
        int num;
//        System.out.println(num);//错误信息:变量num未进行初始化
    }
}

与GC Roots的关系

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
在这里插入图片描述

操作数栈

  • 主要用于保存计算过程中的中间结果,同时作为计算过程中变量的临时的存储空间。
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

举例

public void testAddOperation(){
    
    
    byte i = 15;
    int j = 8;
    int k = i + j;
}

字节码分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

操作数栈的测试

方法1
  • 操作数栈深度为4
	public void testAddOperation() {
    
    
        //byte、short、char、boolean:都以int型来保存
        byte i = 15;
        short j = 8;
        int k = i + j;

        long m = 12L;
        int n = 800;
        //这里必须要转换成相同数据类型进行运算,所以是两个long类型数的运算,每个long类型占栈两个深度,double同。
        m = m * n;//存在宽化类型转换

    }

在这里插入图片描述

栈顶缓存技术?

将栈顶元素全部缓存在物理CPU的寄存器中。

动态链接(或指向运行时常量池的方法引用)

  • 每一个栈帧内部都包含一个指向运行时常量池该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如;invokedynamic指令。

  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如: 描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是将这些符号引用转换为调用方法的直接引用。

方法出口

  • 方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这一部分信息。
  • 正常完成出口和异常完成出口的区别在于: 通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

三、本地方法栈

3.1 什么是本地方法?

一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法: 该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,有很多其他的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。

3.2 为什么要使用Native Method?

  • 与Java环境外交互
  • 与操作系统交互

3.3 本地方法现状?

目前该方法使用的越来越少了,除非是与硬件有关的应用

3.4 本地方方法栈

  • 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
    • 他甚至可以直接使用本地处理器中的寄存器
    • 直接从本地内存的堆中分配任意数量的内存。
  • 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。

四、堆(线程共享)

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
  • 堆内存的大小是可以调节的。
  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上他应该被视为连续的。
  • 堆,是GC(Garbage Collection, 垃圾收集器)执行垃圾回收的重点区域。
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

4.1 概述

对象都分配在堆上?

存在特殊情况,会分配在栈上,和编译优化有关

所有的线程都共享堆?

所有的线程都共享堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)

4.2 堆的内部结构

现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
在这里插入图片描述

  • Java7及之前堆内存逻辑上分为三部分: 新生代+老年代+永久代
    在这里插入图片描述
  • Java8 及之后堆内存逻辑上分为三部分: 新生代+老年代+元空间

新生代和老年代

  • 存储在JVM中的Java对象可以被划分为两类:
    • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
    • 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
  • Java堆区进一步细分的话,可以划分为新生代和老年代
  • 其中新生代又可以划分为Eden区、S1区和S2区(有时也叫作from区、to区)。
    在这里插入图片描述
  • 几乎所有的Java对象都是在Eden区被new出来的。
  • 绝大部分的Java对象的销毁都在新生代进行了。

4.3 设置堆内存大小(-Xmssize & -Xmxsize)

  • -Xms用于表示堆区的起始内存,等价于-XX:InitialHeapSize
  • -Xmx用于表示堆区的最大内存,等价于-XX:MaxHeapSize
  • 通常会将-Xms-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
  • heap最大值默认为物理内存的1/4
  • head最小值默认为物理内存的1/16

测试

public class HeapSpaceInitial {
    
    
    public static void main(String[] args) {
    
    

        //返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");

        //System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
        //System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");
        
    }
}

如何设置新生代和老年代比例

下面这参数开发中一般不会调
在这里插入图片描述

  • 配置新生代与老年代在堆结构的占比。
    • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
    • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
  • 可以使用选项 "-Xmn"设置新生代最大内存大小
    • 这个参数一般使用默认值就可以了。

测试

public class HeapSpaceInitial {
    
    
    public static void main(String[] args) {
    
    

        //返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");

        //System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
        //System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");

        try {
    
    
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

  • 从测试结果可知新生代与老年代默认内存大小比值为1:2

如何设置Eden区域幸存者区比例?

  • 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
  • 开发人员可以通过选项"-XX:SurvivorRatio"调整这个空间的比例。
  • -XX:SurvivorRatio=7,即Eden区:s1区:s2区=7:1:1

测试

在这里插入图片描述

  • 任意测试一个程序可知其默认的Eden区:s1区:s2区=8:1:1

OOM测试

public class OOMTest {
    
    
    public static void main(String[] args) {
    
    
        ArrayList<Picture> list = new ArrayList<Picture>();
        while(true){
    
    
            try {
    
    
                Thread.sleep(10);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            list.add(new Picture(new Random().nextInt(1024 * 1024)));
        }
    }
}

class Picture{
    
    
    private byte[] pixels;

    public Picture(int length) {
    
    
        this.pixels = new byte[length];
    }
}

jvm参数

  • -Xms500m -Xmx500m -XX:SurvivorRatio=8
    在这里插入图片描述

参数设置小结

-Xms -Xmx

  • 前者设置堆的初始大小,后者设置堆的最大值

-Xmn

  • 设置新生代的初始值和最大值,如-Xmn256k表示将新生代的初始值和最大值设为256k

-XX:NewRatio

  • 设置新生代与老年代的比值,如-XX:NewRatio=2表示新生代与老年代比值为1:2

-XX:SurvivorRatio

  • 设置Eden区与幸存者区的比值,如-XX:SurvivorRatio=8表示Eden区:s1区:s2区比值为8:1:1

4.4 对象的内存分配

金句

  • 针对幸存者s0,s1区的总结: 复制之后有交换,谁空谁是to
  • 关于垃圾回收
    • 频繁在新生代收集
    • 很少在老年代收集
    • 几乎不在永久代/元空间收集

过程剖析

  1. new的对象先放Eden区。
  2. 当Eden区满时,程序又要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC/YGC),将Eden区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到Eden区
  3. 然后将Eden区的剩余对象移动到幸存者0区。
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
  5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
  6. 在幸存者0区和幸存者1区移动一定次数之后,就会放到老年区,默认次数是15
    • 可以设置参数: -XX:MaxTenuringThreshold=<N> 设置对象晋升老年代的年龄阈值。
  7. 在老年代,相对悠闲。当老年代内存不足时,再次触发GC: Major GC,进行养老区的内存清理
  8. 若老年代执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。

分配策略

内存分配策略
如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,就会被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor去中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中。

内存分配原则

针对不同年龄段的对象分配原则如下所示:

  • 优先分配到Eden
  • 大对象直接分配到老年代
    • 尽量避免程序中出现过多的大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    • 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  • 空间分配担保
    • -xx:HandlerPromotionFailure

测试

  • 大对象直接分配到老年代
public class YoungOldAreaTest {
    
    
    public static void main(String[] args) {
    
    
        byte[] buffer = new byte[1024 * 1024 * 20];//20m

    }
}
  • JVM参数: -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
    测试结果
    在这里插入图片描述

空间分配担保(-XX:HandlePromotionFailure)

在发生Minor GC之前,虚拟机惠检查老年代最大可用的连续空间是否大于新生代所有对象的总空间

  • 如果大于,则此次Minor GC是安全的
  • 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
  • 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HnadlerPromotionFailure=false,则改为进行一次Full GC。

在JDK 6 Update 24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码变化,虽然源码中还定义HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

4.5 解释MinorGC、MajorGC、FullGC

JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。

针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:

  • 一种是部分收集(Partial GC)
  • 一种是整堆收集(Full GC)

部分收集: 不是完整收集整个Java堆的垃圾收集。其中又分为:

  • 新生代收集(Minor GC / Young GC): 只是新生代(Eden\S0,S1)的垃圾收集
  • 老年代收集(Major GC / Old GC): 只是老年代的垃圾收集。
    • 目前,只有CMS GC会单独收集老年代的行为。
    • 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
  • 混合收集(Mixed GC): 收集整个新生代以及部分老年代的垃圾收集。
    • 目前,只有G1 GC会有这种行为
  • 整堆手机(Full GC): 收集整个java堆和方法区的垃圾收集

MinorGC触发机制

  • 当年轻代空间不足时,就会出发Minor GC。这里的年轻代满指的是Eden区满,
    survivor 区满不会触发GC。(每次Minor GC会清理年轻代的内存。)
  • 因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才回复运行。

MajorGC触发机制

  • 指发生在老年代的GC,对象从老年代消失时,我们说"Major GC" 或 “Full GC” 发生了。
    • 出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的手机策略里就有直接进行Major GC的策略选择过程)。
    • 也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发MajorGC
  • Major GC的速度一般会比Minor GC慢10被以上,STW的时间更长。
  • 如果Major GC后,内存还不足,就报OOM了。

FullGC触发机制

  • 调用System.gc()时,系统简易执行Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden区,s0向s1区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
    说明: full gc是开发或者调优中尽量要避免的。这样暂停时间会短一些。

4.6 OOM如何解决

  1. 要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak) 还是内存溢出(Memory Overflow)。
  2. 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位置。
  3. 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与及其物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗

4.7 堆空间分代思想

研究表明,不同对象的生命周期不同。70%-99%的对象是临时对象。
- 新生代: 有Eden、两块大小相同的Survivor(又称为from/to,s0/s1)构成,to总为空。
- 老年代:存放新生代中经历多次GC仍然存活的对象。

4.8 快速分配策略: TLAB(Thread Local Allocation Buffer)

为什么需要TLAB?

  • 堆是线程共享区域,任何线程都可以访问到堆区中的共享数据
  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

所以,多线程同时分配内存,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略

什么是TLAB?

  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
  • 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
    在这里插入图片描述

TLAB相关参数设置

  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
  • 在程序中,开发人员可以通过选项"-XX:+/-UseTLAB"设置是否开启TLAB空间。
  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项"-XX:TLABWasteTargetPercent"设置TLAB空间所占用Eden空间的百分比大小。
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
    在这里插入图片描述

测试

public class TLABArgsTest {
    
    
    public static void main(String[] args) {
    
    
        System.out.println("我只是来打个酱油~");
        try {
    
    
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

五、方法区(线程共享)

5.1 栈、堆、方法区的关系

在这里插入图片描述

5.2 方法区在哪里?

虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做"非堆"(Non-Heap),目的是与Java堆区分开来。

5.3 方法区的理解

在这里插入图片描述

  • 方法区与Java堆一样,是各个线程共享的内存区域。
  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
  • 方法区的大小,跟堆空间一致,可以选择固定大小或者可扩展。
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区移除,虚拟机同样会抛出内存溢出错误: java.lang.OutOfMemoryError: PermGen space或者
    java.lang.OutOfMemoryError: Metaspace
    • 加载大量的第三方的jar包,Tomcat部署的工程过多(30-50个);大量动态的生成反射类
  • 关闭JVM就会释放这个区域的内存。

5.4 HotSpot中方法区的演进

  • 在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代。
  • 本质山,方法区和永久代并不等价。仅是对hotspot而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。例如: BEA JRockit/ IBM J9中不存在永久代的概念。
    • 现在来看,当年使用永久代,不是好的idea。导致Java程序更容易OOM(超过-XX:MaxPermSize上限)
      在这里插入图片描述
  • 而到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(metaspace)代替
    在这里插入图片描述
  • 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于: 元空间不在虚拟机设置的内存中,而是使用本地内存。
  • 永久代、元空间二者并不只是名字变了,内部结构也调整了。
  • 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。

测试

public class OOMTest extends ClassLoader {
    
    
    public static void main(String[] args) {
    
    
        int j = 0;
        try {
    
    
            OOMTest test = new OOMTest();
            for (int i = 0; i < 10000; i++) {
    
    
                //创建ClassWriter对象,用于生成类的二进制字节码
                ClassWriter classWriter = new ClassWriter(0);
                //指明版本号,修饰符,类名,包名,父类,接口
                classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                //返回byte[]
                byte[] code = classWriter.toByteArray();
                //类的加载
                test.defineClass("Class" + i, code, 0, code.length);//Class对象
                j++;
            }
        } finally {
    
    
            System.out.println(j);
        }
    }
}

jvm参数 -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m

  • jdk1.8

在这里插入图片描述

5.5 方法区常用参数有哪些

  • jdk8及以后:
    • 元数据区大小可以使用参数-XX:MetaspaceSize-XX:MaxMetaspaceSize指定,替代上述原有的两个参数。
    • 默认值依赖于平台
    • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError: Metaspace
    • -XX:MetaspaceSize: 设置初始元空间大小
    • -XX:MaxMetaspaceSize: 设置元空间最大值

5.6 永久代和元空间

jdk1.6之前: 有永久代
jdk1.7: 有永久代,但已经逐步"去永久代化",字符串常量池、静态变量移除,保存在堆中
jdk1.8及之后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池仍在堆中。

为什么要用元空间替换永久代

  • 为永久代设置空间大小是很难确定的。
  • 对永久代进行调优是很困难的。

常量放在哪里?

  • jdk7之前放在方法区中
  • jdk7及之后放在堆中

测试

public class StringLocationTest {
    
    
    public static void main(String[] args) {
    
    
        //使用Set保持着常量池引用,避免full gc回收常量池行为
        Set<String> set = new HashSet<String>();
        //在short可以取值的范围内足以让6MB的PermSize或heap产生OOM了。
        short i = 0;
        while(true){
    
    
            set.add(String.valueOf(i++).intern());
        }
    }
}

jvm参数:-XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
在这里插入图片描述

静态变量放在哪里?

  • jdk7之前放在方法区中
  • jdk7及之后放在堆中

测试

public class StaticFieldTest {
    
    
    private static byte[] arr = new byte[1024 * 1024 * 100];//100MB -> 200+MB

    public static void main(String[] args) {
    
    
        System.out.println(StaticFieldTest.arr);
    }
}

jvm参数:-Xms300m -Xmx300m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
在这里插入图片描述

StringTable为什么要调整?

jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。
这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

5.7 方法区是否存在gc?回收什么?

存在,方法取得垃圾收集主要回收两部分内容: 常量池中废弃的常量和不再使用的类型。

猜你喜欢

转载自blog.csdn.net/qq_43478625/article/details/121413348
今日推荐