JVM之方法区

返回主博客

返回上一层

 

方法区

9.1 栈,堆,方法区交互关系

 

9.2 方法区的理解

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods  used in class and instance initialization and interface initialization.

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

The following exceptional condition is associated with the method area:

  • If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

Java虚拟机有一个方法区域,在所有Java虚拟机线程之间共享。方法区域类似于常规语言编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每类结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化以及接口初始化中使用的特殊方法。

方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但是简单的实现可以选择不对其进行垃圾收集或压缩。此规范不强制指定用于管理已编译代码的方法区域或策略的位置。方法区域可以是固定大小的,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的。

Java虚拟机实现可以提供程序员或用户对方法区域的初始大小的控制,并且,在大小不同的方法区域的情况下,还可以提供对最大和最小方法区域大小的控制。

 

如果类定义太多,方法区则会OOM

java.lang.OutOfMemoryError: PermGen space

java.lang.OutOfMemoryError: Metaspace

现在来看,当年使用永久代,不是好的idea,导致容易OOM(-XX:MaxPermSize)

到JDK8,废弃永久代,改用与Jrockit,J9,一样,在本地内存中实现的元空间代替。但是也是可以设置上限,也会OOM

元空间不像永久代,元空间不使用java内存,而是本地内存。

 

9.3 方法区大小设置和OOM

方法区的大小可以不必是固定的

1.7之前:

-XX:PermSize 设置初始大小 默认20.75M

-XX:MaxPerSize来设置最大配置内存 32位机型默认64M,64位机型默认82M

1.8之后

-XX:MetaspaceSize 默认和平台相关, 推荐大一些,

-XX:MaxMetaspaceSize   -1 不限制,推荐不限制

对于64位的的服务器端的JVM来说,其默认的-XX:MetaSpaceSize是21M,这是初始高水平线,一旦触及,Full GC将会触发,并卸载无用的类。并且调高或调低“高水平线”。

 

代码:可以通过CGLib或者ASM不断建类,并在1.8的jre环境下配置JVM参数(-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m)使方法区溢出。

package com.jack.ascp.purchase.app.test.vm.methodarea;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
 * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 * @author : 江鹏亮
 * @date : 2020-06-27 11:21
 **/
public class MethodAreaOOMTest extends ClassLoader{
    public static void main(String[] args) {
        MethodAreaOOMTest oomTest = new MethodAreaOOMTest();
        ClassWriter classWriter = new ClassWriter(0);
        int num = 0;
        try {
            for (int i = 0; i < 10000; i++) {
                classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                byte[] code = classWriter.toByteArray();
                oomTest.defineClass("Class" + i, code, 0, code.length);
                num ++;
            }
        } catch (Error e) {
            e.printStackTrace();
        }
        System.out.println(num);
    }
}

结果:

 

如何解决OOM(这个得看经验的,比如本来之前内存肯定够用的,这次发布突然就OOM了,肯定和这次发布有关了)

1、要解决OOM异常或者heap space的异常,一般手段是通过内存映像分析工具(如Eclipsing Memory Analyzer)对dump出来的堆转储快照进行分析。重点是确认内存中的对象是否是必要的,也就是要分析是内存泄漏还是内存溢出。

2、如果内存泄漏,可以进一步通过工具查看泄漏对象到GC Root的引用链,于是就能找到内存泄漏对象是通过怎么样的路径与GCRoot相关联的,并导致垃圾回收器无法自动回收。然后并将其断开

3、不存在内存泄漏,先排查某些对象是否有必要生命周期过长,并尝试修改他,或者扩容。

 

9.4 方法区中的存储数据

方法区用于存储已经被虚拟机加载的类型信息,常量,静态变量(其实字符串常量已经不在了),即时编译器编译后的代码缓存等。

 

1、存储类型信息:

对于class ,interface,enum, annotation 存储以下信息

      1、全限定名,2、父类、3、修饰符 4 接口列表

2、Fileds信息 以及他们的顺序

     域名称,域类型,域修饰符(public,private,final,volatile,transient等)。

3、方法信息

      名称,返回类型,参数数量和类型,方法修饰符,异常表。

 

4、加载的ClassLoader

5、静态变量

6、全局静态变量 final static修饰的在编译期间决定,在准备阶段赋值

 

9.5 运行时常量池VS 常量池

9.6 方法区演进过程

  只有HotSpot VM才会有永久代

 

为什么将永久代替换成本地内存管理的元空间?

http://openjdk.java.net/jeps/122

Motivation

This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.

Description

Move part of the contents of the permanent generation in Hotspot to the Java heap and the remainder to native memory.

Hotspot's representation of Java classes (referred to here as class meta-data) is currently stored in a portion of the Java heap referred to as the permanent generation. In addition, interned Strings and class static variables are stored in the permanent generation. The permanent generation is managed by Hotspot and must have enough room for all the class meta-data, interned Strings and class statics used by the Java application. Class metadata and statics are allocated in the permanent generation when a class is loaded and are garbage collected from the permanent generation when the class is unloaded. Interned Strings are also garbage collected when the permanent generation is GC'ed.

The proposed implementation will allocate class meta-data in native memory and move interned(拘留,禁闭,关押) Strings and class statics to the Java heap. Hotspot will explicitly allocate and free the native memory for the class meta-data. Allocation of new class meta-data would be limited by the amount of available native memory rather than fixed by the value of -XX:MaxPermSize, whether the default or specified on the command line.

Allocation of native memory for class meta-data will be done in blocks of a size large enough to fit multiple pieces of class meta-data. Each block will be associated with a class loader and all class meta-data loaded by that class loader will be allocated by Hotspot from the block for that class loader. Additional blocks will be allocated for a class loader as needed. The block sizes will vary depending on the behavior of the application. The sizes will be chosen so as to limit internal and external fragmentation. Freeing the space for the class meta-data would be done when the class loader dies by freeing all the blocks associated with the class loader. Class meta-data will not be moved during the life of the class.

意思就是jrockit用元空间,hotspot就用元空间了。

1、为永久代设置空间大小很难确定的。

     在某些情况下,如果动态加载类过多,容易Perm区OOM

2、对永久代调优时很困难的 而且FULL  GC效率低。

  VM判断类是否废弃是要经过一系列很多判断的,而且也不能100%保证类需要废弃。 判断类和常量是否不被使用是很困难的,而且类不用了,我们为什么不直接在代码中删了?既然不删,说明都是有可能会用的。(当然也有可能是某些框架启动前期需要用到某些类,比如Spring 启动用到AntPathMacher 等很多工具类,以及它定义的很多常量,后面有很多基本就不用了,这种容器框架启动时用到了很多类以及对象,其实后面不用了,确实需要优化,或者有时候我们代码中经常会用到第三方或者java提供的工具类,你用一个,我用一个之类的情况)。

 

StringTable为什么要调整

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

 

如何证明静态变量放到哪里。

分别使用一下JVM参数,在1.6 1.7 1.8运行 

/**
 * @author : 江鹏亮
 * -Xms600m -Xmx600m -XX:SurvivorRatio=8 -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
 * -Xms600m -Xmx600m -XX:SurvivorRatio=8 -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
 * @date : 2020-06-27 15:37
 **/
public class StaticFiledTest {
    /**
     *  200M的arr 堆比值: 160 : 20 : 20 : 400,
     *  为了是结果清晰,让这个arr大小大于160, 从而直接触发GC,晋身老年代,就可以看见老年代的占用为204800K
     */
    private static byte[] arr = new byte[1024 * 1024 * 200];
    public static void main(String[] args) {
        System.out.println(arr);
    }
}

静态变量的实体对象从始至终都在堆中(1.6-1.8)但是静态变量的引用在1.7之后随着java.lang.Class 对象实例存放在堆中


/**
 * -Xms600m -Xmx600m -XX:SurvivorRatio=8 -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
 * -Xms600m -Xmx600m -XX:SurvivorRatio=8 -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
 * @author : 江鹏亮
 * @date : 2020-06-27 16:06
 **/
public class StaticObjTest {
    static class Test {
         static ObjectHolder staticObj = new ObjectHolder();
         ObjectHolder instanceObj = new ObjectHolder();
         void foo() {
             ObjectHolder localObj = new ObjectHolder();
             System.out.println("done");
         }
    }
    static class ObjectHolder {
    }
    public static void main(String[] args) {
        Test test = new Test();
        test.foo();
    }
}

案例中

staticObj 引用随着java.lang.Class类型的对象实例,一起放到堆中(jdk1.7以后)

instanceObj 引用随着 ObjectHolder类型的对象实例,一起放到堆中

localObj 引用反正操作数栈和局部变量表上。

 

方法区的垃圾收集(GarbageCollect

java虚拟机规范中说:方法区不要求进行垃圾回收和压缩。

(事实上确实有未实现或未能完整实现方法区类型卸载的收集器存在,如ZGC)

一般来说,这个区域的回收效果比较难令人满意,尤其类型的卸载,条件相当苛刻。但是有时又由必要。

sun公司的bug列表中,曾出现若干严重问题的bug就是由于低版本的hotSpot虚拟机未完整回收。导致内存泄漏。

 

主要回收两个部分:常量池中废弃的常量和不再使用的类型。

1、hotSpot对常量池的回收策略时很明确的,只要常量池中的常量没有任何地方引用就可以被回收。

2、要想判断一个类是否可以被回收满足下面三个条件:

    1、所有类(以及其派生的类的)的实例已经被回收。

    2、类加载器已经被回收。

    3、class 对象不再被任何地方引用

就算满足也不一定就可以被回收。

运行时数据区总结:

 

 

 

 

 

10、对象的实例化,内存布局域访问定位

对象创建对应字节码:

public class ObjectTest {
    int id;
    String name;
    public ObjectTest(int id, String name) {
        this.id = id;
        this.name = name;
    }
    public static void main(String[] args) {
        int id = 0;
        String name = "aaaa";
        ObjectTest objectTest = new ObjectTest(id, name);
    }
}

 0: iconst_0     常量池中加载0
 1: istore_1     将0存到局部变量表1的位置
 2: ldc   #4     常量池中加载“aaaa”
 4: astore_2     将字符串“aaaa” 存储到局部变量表 2 的位置
 5: new   #5     new ObjectTest并对属性默认初始化(如int默认0,boolean默认false), 并将其返回的引用放到栈顶
 8: dup          将栈顶元素(也就是刚刚new出来的对象引用)dup 一份,这样栈顶的两个元素都是这个new出来的对象引用
 9: iload_1      加载局部变量表1的元素,即 0
10: aload_2      加载局部变量表2的元素,即 "aaaa"
11: invokespecial #6 //Method "<init>":(ILjava/lang/String;)V   调用init方法,因为调用完,对象引用就出栈了,所以前面dup了一份
14: astore_3     将对象引用存到局部变量表3的位置
15: return

重点说明:

 1、new的时候对象的大小就固定了,我猜:byte,short,char,boolean,float,int占32位,地址引用占32位,long和double占两个32

 2、之所以dup一份是因为调用构造方法后,栈顶的一个对象引用就pop了,所以要先dup一个,用于后面的astore。

 

对象的内存布局

 

对象访问定位

句柄访问:

 

使用直接指针(hotsport使用)

比较:

句柄访问需要再次开辟空间,还得间接访问一次。

但是当对象发生移动时,句柄访问就只需要修改句柄池部分就可以了。而直接指针方式就得修改栈中的指针。

 

11、直接内存(本地内存)

不是JVM运行时数据区的一部分,不在java虚拟机规范中

元空间使用的就是本地内存

是C++直接向系统直接申请的内存。

来源于NIO,通过堆中的directByteBuffer操作的native内存。

什么是NIO ?对比IO

 

IO(New IO/ IO)
blockingNon-blocking
使用byte[]或者char[]进行传输。使用Buffer进行传输。
基于 Stream 基于 Channel
  

 

代码案例:

public class BufferTest {
    private static final int  BUFFER = 1024 * 1024 * 1024;
    public static void main(String[] args) {
        //直接分配比迪内存空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
        System.out.println("直接内存分配完毕");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        System.out.println("直接内存开始释放");
        byteBuffer = null;
        System.gc();
        scanner.next();
    }
}

 

查看任务管理器,该进程占用了1G多内存

通常我们使用直接内存的速度优于使用堆内存,读写性能高。

     基于性能考虑,读写频繁场合可以考虑使用直接内存。

     Java的NIO库允许java程序使用直接内存。

 

直接内存也有可能OOM (java.lang.OutOfMemoryError: Direct buffer memory)

由于直接内存在堆外,他的大小不会直接受限于-Xms。但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

缺点

   分配回收成本高

   不受JVM内存回收管理

可以用-XX:MaxDirectMemorySize 设置。

如果不指定默认于-Xmx一致。

他的大小使用jvisualvm或者jprofiler是查看不到的。可以用任务管理器查看。

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值