• JVM(三)运行时数据区-堆和方法区


    录:

    1、堆
    1.1、进程中堆的唯一性
    1.2、堆空间关于对象创建和和GC的概述
    1.3、堆的细分内存结构
    1.4、堆空间大小的设置和查看
    1.5、新生代与老年代中相关参数的设置
    1.6、对象分配的一般过程
    1.7、对象分配的特殊情况
    1.8、代码举例与JVisualVM演示对象的分配过程
    1.9、常用优工具概述与Jprofiler的演示
    1.10、MinorGC、MajorGC和FullGC的对比
    1.11、内存分配策略(或对象提升Promotion规则)
    1.12、逃逸分析和栈上分配

    2、方法区
    2.1、栈、堆、方法区的交互关系
    2.2、方法区的理解
    2.3、HotSpot中方法区的演进
    2.4、设置方法区大小的参数
    2.5、方法区的内部结构
    2.6、方法区在jdk6、jdk7、jdk8中的演进细节
    2.7、方法区的回收行为

    1、堆

    1.1、进程中堆的唯一性    <--返回目录

      一个JVM实例只存在一个堆内存,堆也是java内存管理的核心区域。
      java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的。
      《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
      所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)。

    1.2、堆空间关于对象创建和和GC的概述    <--返回目录

      《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
      数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或数组在堆中的位置。
      在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
      堆是GC(Garbage Collection 垃圾收集器)执行垃圾回收的重点区域。

    1.3、堆的细分内存结构    <--返回目录

      现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

    1.4、堆空间大小的设置和查看    <--返回目录

    Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了:
        -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小,比如-Xms600m
        -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小,比如-Xmx600m
        
    一旦堆区的内存大小超过"-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。
    通常会将 -Xms和-Xmx 两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
    默认情况下,初始内存大小:物理电脑内存大小/64,最大内存大小:物理电脑内存大小/4。

    // 返回java虚拟机中的堆内存总量,单位M
    long initialMemory = Runtime.getRuntime().totalMemory()/1024/1024;
    // 返回java虚拟机试图使用的最大堆内存量,单位M
    long maxMemory = Runtime.getRuntime().maxMemory()/1024/1024;
    
    // 第一次测试时,不设置-Xms和-Xmx
    //System.out.println("系统内存大小为:" + initialMemory*64/1024 + "G");
    //System.out.println("系统内存大小为:" + maxMemory*4/1024 + "G");
    
    // 第二次测试设置"-Xms600m -Xmx600m"
    //-Xms:575M -Xmx:575M, 因为s0和s1区为25M,同时只有一个s区能够使用
    System.out.println("-Xms:" + initialMemory + "M");
    System.out.println("-Xmx:" + maxMemory + "M");

      查看堆空间大小:

      方式一:jps查询java程序的进程id , jstat -gc 进程id

      方式二:-XX:+PrintGCDetails

    1.5、新生代与老年代中相关参数的设置    <--返回目录

      年轻代和老年代(为什么要分代)

      存储在JVM中的java对象可以被划分为两类:一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速;另外一类对象的生命周期却非常长,在某些极端情况下还能够与JVM的生命周期保持一致。

      JVM堆区进一步细分的话,可以划分为年轻代(YonngGen)和老年代(OldGen),其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)

      配置新生代和老年代在堆结构的占比

      默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3

     

      如何查看NewRatio的值

      在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1,当然开发人员可以通过选项 “-XX:SurvivorRatio”调整这个空间比例。比如-XX:SurvivorRatio=8

      几乎所有的java对象都是在Eden区被new出来的。

      绝大部分的java对象的销毁都在新生代进行。IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。

      可以使用选项“-Xmn”设置新生代最大内存大小,一般这个参数使用默认值就可以了

    1.6、对象分配的一般过程    <--返回目录

      为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。

      1、new的对象先放伊甸园区。此区有大小限制。

      2、当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。

      3、然后将伊甸园中的剩余对象移动到幸存者0区。

      4、如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。

      5、如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再区幸存者1区。

      6、啥时候能去养老区呢?可以设置次数,默认是15次。-XX:MaxTenuringThreshold=<N>

      7、在养老区,相对悠闲。当养老区内存不足时,再次触发GC(Major GC),进行养老区的内存清理。

      8、若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常(java.lang.OutOfMemoryError: Java heap space)。

      总结:

      针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to。

      关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。

    1.7、对象分配的特殊情况    <--返回目录

      对象直接晋升到老年代的两种情况:

      1)新new出来的对象,首先考虑在Eden区分配,如果Eden内存不足,则进行YGC(Young GC),回收后仍然不足则考虑在老年代分配;如果老年代内存不足,则进行FGC(Full GC),回收后仍然内存不足则报OOM。

      1)发生YGC时,Eden区的存活对象要放入S0/S1,如果此时S0/S1放不下,考虑直接晋升到老年代。

    1.8、代码举例与JVisualVM演示对象的分配过程    <--返回目录

      启动程序,cmd -> jvisualvm

      jvisualvm安装Visual GC插件:工具->插件->设置->编辑 URL: https://visualvm.github.io/archive/uc/8u40/updates.xml.gz

       测试代码

    /**
     * VM arguments:
     * -Xms600m -Xmx600m -XX:+PrintGCDetails
     */
    public class Demo02 {
        byte[] buffer = new byte[new Random().nextInt(1024*200)];
    
        public static void main(String[] args) throws Exception {
            List<Demo02> list = new ArrayList<>();
            while(true) {
                list.add(new Demo02());
                TimeUnit.MILLISECONDS.sleep(10);
            }
        }
    }

      JVisualVM演示对象的分配过程

    1.9、常用优工具概述与Jprofiler的演示    <--返回目录

      常用调优工具:

    JDK命令行
    Eclipse:Memory Analyzer Tool
    Jconsole
    VisualVM
    Jprofiler
    Java Elight Recorder
    GCViewer
    GC Easy

       首先安装JProfiler v11.0.2 64位.zip,idea安装jprofiler插件

    1.10、MinorGC、MajorGC和FullGC的对比    <--返回目录

      JVM进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。

      针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

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

        - 新生代收集Minor GC/Young GC:只是新生代的垃圾收集。

        - 老年代收集Major GC/Old GC:只是老年代的垃圾收集。目前只有CMS GC会有单独收集老年代的行为。注意:很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。

      混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前只有G1 GC会有这种行为。

      整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

      最简单的分代式GC策略的触发条件:

      年轻代GC(Minor GC)触发机制

        - 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden满,Survivor满不会引发GC。每次Minor GC会清理年轻代的内存。

        - 因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。

        - Minor GC会引发STW,暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行。

      老年代GC(Major GC/Full GC)触发机制

        - 指发生在老年代的GC,对象从老年代消失时,我们说Major GC或Full GC发生了。

        - 出现了Major GC,通常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程。)也就是在老年代空间不足时,会先尝试                          触发Minor GC。如果之后空间还不足,则触发Major GC。

        - Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。

        - 如果Major GC后,内存还不足,就报OOM。

      Full GC触发机制:full gc是开发和调优中尽量要避免的。这样暂停时间会短一些。

        - 调用System.gc()时,系统建议执行Full GC,但是不必然执行

        - 老年代空间不足

        - 方法区空间不足

        - 通过Minor GC后进入老年代的平均大小大于老年代的可用内存

        - 由Eden区、Survivor space0(From Space)区 向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

      为什么需要把Java堆分代?不分代就不能正常工作了吗?

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

      其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,每次GC的时候对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

     

    1.11、内存分配策略(或对象提升Promotion规则)    <--返回目录

      如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认位15,其实每个JVM每个GC都有所不同)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过选项-XX:MaxTenuringThreshold来设置。

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

      - 优先分配到Eden

      - 大对象直接分配到老年代(尽量避免程序中出现过多的大对象)

      - 长期存活的对象分配到老年代

      - 动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

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

    1.12、逃逸分析和栈上分配    <--返回目录

      在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:随着JIT的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

      在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收了。这也是最常见的堆外存储技术。

      此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

      逃逸分析概述:

      如何将堆上的对象分配到栈,需要使用逃逸分析手段。这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

      通过逃逸分析,Java HotSpot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

      逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后对象只是在方法内部使用,则认为没有发生逃逸。当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。

      使用逃逸分析,编译器可以对代码做如下优化:

    栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
    同步省略(锁消除)。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
    分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分或全部可以不存储在内存,而是存储在cpu寄存器中。
    /**
     * VM arguments
     *  不开启逃逸分析:-Xms1G -Xmx1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
     *  开启逃逸分析 :  -Xms1G -Xmx1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
     */
    public class Demo02 {
        public static void main(String[] args) throws Exception {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 1000 * 10000; i++) {
                alloc();
            }
            System.out.println("花费时间:" + (System.currentTimeMillis() - start) + " ms");
        }
    
        private static void alloc() {
            User u = new User();
        }
    }
    private static void f() {
        Object obj = new Object();
        synchronized (obj) { // obj生命周期旨在f()方法内,不会被其他线程访问到,JIT通过逃逸分析后进行锁消除
            // 其他代码
        }
    }


    2、方法区

    2.1、栈、堆、方法区的交互关系    <--返回目录

     

    2.2、方法区的理解    <--返回目录

      《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。所以,方法区可以看作是一块独立于Java堆的内存空间。

      方法区于Java堆一样,是各个线程共享的内存区域。

      方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样都可以是不连续的。

      方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

      方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误 java.lang.OutOfMemoryError: PermGen space 或者 java.lang.OutOfMemoryError: Metaspace。

      关闭JVM就会释放这个区域的内存。

    2.3、HotSpot中方法区的演进    <--返回目录

      在jdk7及以前,习惯上把方法区称为永久代。jdk8开始,使用元空间取代了永久代。

      本质上,方法区和永久代并不等价。仅是对HotSpot而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。现在看来,当年使用永久代,不是好的idea,导致java程序更容易OOM(超过-XX:MaxPermSize上限)。

      而到了jdk8,终于完全废弃了永久代的概念,改用于JRockit、J9一样在本地内存中实现的元空间(Metaspace)来替代。

      元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存

      永久代、元空间二者并不只是名字变了,内部结构也调整了。

    2.4、设置方法区大小的参数    <--返回目录

      方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。jdk7及以前,通过-XX:PermSize来设置永久代初始分配空间。默认值是20.75M。-XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器默认是82M。当JVM加载的类信息容量超过了这个值,会报OutOfMemoryError:PermGen spacce。

      jdk7查看方法区大小参数:jps,  jinfo -flag PermSize 进程号。

      jdk8及以后:

      元数据区大小使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定。

      默认值依赖于平台,windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。

      与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OufOfMemoryError:Metaspace。

      -XX:MetaspaceSize设置初始的元空间大小,对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize值为21M。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类,然后这个高水位线将会重置。新的高水位线的值取决于GC会释放了多少空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值,如果释放空间过多,则适当降低该值。

      如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁GC,建议将-XX:MetaspaceSize设置为一个相对较高的值

    2.5、方法区的内部结构    <--返回目录

      《深入理解Java虚拟机》书中对方法区存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

       javap -v -p xxx.class > xxx.txt 查看字节码文件。

      全局常量 static final 的处理方式:在编译的时候就被分配值了。

    public class Demo03 {
        private static int num1 = 1;
        private static final int num2 = 2;
    }

      javap -v -p Demo03.class > Demo03.txt

    Classfile /D:/workspaces/eclipse201812_workspace/demo/bin/com/oy/Demo03.class
      Last modified 2021-10-5; size 396 bytes
      MD5 checksum 483ba634e19f7ffd6f9d944505c849dc
      Compiled from "Demo03.java"
    public class com.oy.Demo03
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
      
    // 字节码中的常量池 => 方法区中的运行时常量池
    Constant pool:
       #1 = Class              #2             // com/oy/Demo03
       #2 = Utf8               com/oy/Demo03
       #3 = Class              #4             // java/lang/Object
       #4 = Utf8               java/lang/Object
       #5 = Utf8               num1
       #6 = Utf8               I
       #7 = Utf8               num2
       #8 = Utf8               ConstantValue
       #9 = Integer            2
      #10 = Utf8               <clinit>
      #11 = Utf8               ()V
      #12 = Utf8               Code
      #13 = Fieldref           #1.#14         // com/oy/Demo03.num1:I
      #14 = NameAndType        #5:#6          // num1:I
      #15 = Utf8               LineNumberTable
      #16 = Utf8               LocalVariableTable
      #17 = Utf8               <init>
      #18 = Methodref          #3.#19         // java/lang/Object."<init>":()V
      #19 = NameAndType        #17:#11        // "<init>":()V
      #20 = Utf8               this
      #21 = Utf8               Lcom/oy/Demo03;
      #22 = Utf8               SourceFile
      #23 = Utf8               Demo03.java
    {
      // static 静态变量编译期没有分配值
      private static int num1;
        descriptor: I
        flags: ACC_PRIVATE, ACC_STATIC
        
      // 全局常量 static final 编译期就分配了值
      private static final int num2;
        descriptor: I
        flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
        ConstantValue: int 2
    
      // <clinit> 给静态变量进行显示初始化
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=1, locals=0, args_size=0
             0: iconst_1
             1: putstatic     #13                 // Field num1:I
             4: return
          LineNumberTable:
            line 11: 0
            line 12: 4
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
      
      // 提供的默认构造器
      public com.oy.Demo03();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #18                 // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 10: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/oy/Demo03;
    }
    SourceFile: "Demo03.java"

      字节码中的常量值:包括各种字面量和对类型、域和方法的符号引用。在动态链接时,符号引用转换成直接引用。

      运行时常量值 Runtime Constant Pool 是方法区的一部分。常量池表 Constant Pool Table 是class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

      运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。

      JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。

      运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里转换为真实地址。运行时常量池,相对于class文件常量池的另一重要特征是:具备动态性。

      运行时常量池类似于传统编程语言中达到符号表(symbol table),但是它所包含的数据却比符号表要丰富一些。

      当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值是,则JVM会抛OutOfMemoryError异常。

    2.6、方法区在jdk6、jdk7、jdk8中的演进细节    <--返回目录

      首先明确:只有HotSpot才有永久代。BEA JRockit、IBM J9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。

      HotSpot中方法区的变化:

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

      永久代为什么被元空间替换?

      随着java8的到来,HotSpotVM中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间。由于类的元数据分配再本地内存中,元空间的最大可分配空间就是系统可用内存空间。这项改动是很有必要的,原因是:

      1)为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。比如某个实际web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间的大小仅受本地内存限制。

      2)对永久代进行调优是很困难的。

      为什么要调整stringtable位置?

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

    2.7、方法区的回收行为    <--返回目录

      有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》堆方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK11时期的ZGC收集器就不支持类卸载)。

      一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相对苛刻。但是这部分区域的回收有时又确实是必要的。以前sun公司的bug列表中,曾出现过的若干个严重的bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。

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

      先来说说方法区内常量池中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:1)类和接口的全限定名 2)字段的名称和描述符 3)方法的名称和描述符

      HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。回收废弃常量与回收Java堆中的对象非常类似。

      判断一个常量释放废弃还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

      1)该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。

      2)加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGI、JSP的重加载等,否则通常是很难达成的。

      3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

      Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息。

      在大量使用反射、动态代理、CGLIB等字节码框架,动态生成JSP以及OSGI这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

  • 相关阅读:
    R 语言
    Maven
    IntelliJ IDEA
    Windows Terminal
    pip
    批处理编程案例
    Windows DOS命令批处理脚本
    Day15 T1 库特的向量
    Day12 T1 少女觉
    Day10 T2 邦德
  • 原文地址:https://www.cnblogs.com/xy-ouyang/p/15259451.html
Copyright © 2020-2023  润新知