详解JVM内存管理与垃圾回收机制 (上) 上

转自:https://www.jianshu.com/p/f8d71e1e8821

前言:

或许当你再看见我的时候你会发现我变了,至于变好了还是变坏了、我也不清楚

CHEN川:将从理论角度介绍虚拟机的内存管理和垃圾回收机制,算是入门级的文章,希望对大家的日常开发有所助益

一、内存管理

   启动时通过-Xmx或者-XX:MaxPermSize这样的参数来显式的设置应用的堆(Heap)和永久代(Permgen)的内存大小,但为什么不直接设置JVM所占内存的大小,而要分别去设置不同的区域?JVM所管理的内存被分成多少区域?每个区域有什么作用?如何来管理这些区域?

1.1运行时数据区

JVM在执行Java程序时会把其所管理的内存划分成多个不同的数据区域,每个区域的创建时间、销毁时间以及用途都各不相同。比如有的内存区域是所有线程共享的,而有的内存区域是线程隔离的。线程隔离的区域就会随着线程的启动和结束而创建和销毁。JVM所管理的内存将会包含以下几个运行时数据区域,如下图的上半部分所示。

Method Area方法区:

所有线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据。在Java虚拟机规范中,方法区属于堆的一个逻辑部分,但很多情况下,都把方法区与堆区分开来说。大家平时开发中通过反射获取到的类名、方法名、字段名称、访问修饰符等信息都是从这块区域获取的。

对于HotSpot虚拟机,方法区对应为永久代(Permanent Generation),但本质上,两者并不等价,仅仅是因为HotSpot虚拟机的设计团队是用永久代来实现方法区而已,对于其他的虚拟机(JRockit、J9)来说,是不存在永久代这一概念的。

但现在看来,使用永久代来实现方法区并不是一个好注意,由于方法区会存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,在某些场景下非常容易出现永久代内存溢出。如Spring、Hibernate等框架在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。在JSP页面较多的情况下,也会出现同样的问题。可以通过如下代码来测试:

/**
 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M(JDK6.0)
 * VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M(JDK8.0)
 */
public class CGlibProxy {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(ProxyObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] os, MethodProxy proxy) throws Throwable {
                    System.out.println("I am proxy");
                    return proxy.invokeSuper(o,os);
                }
            });
            ProxyObject proxy = (ProxyObject) enhancer.create();
            proxy.greet();
        }
    }
    static class ProxyObject {
        public String greet() {
            return "Thanks for you";
        }
    }
}

在JDK1.8中运行一小会儿出现内存溢出错误:

Exception in thread "main" I am proxy
java.lang.OutOfMemoryError: Metaspace
    at org.mockito.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:238)
    at org.mockito.cglib.proxy.Enhancer.createHelper(Enhancer.java:378)
    at org.mockito.cglib.proxy.Enhancer.create(Enhancer.java:286)
    at com.lwork.mdo.CGlibProxy.main(CGlibProxy.java:23)

在JDK1.8下并没有出现我们期望的永久代内存溢出错误,而是Metaspace内存溢出错误。这是因为Java团队从JDK1.7开始就逐渐移除了永久代,到JDK1.8时,永久代已经被Metaspace取代,在JDK1.8中,JVM参数-XX:PermSize-XX:MaxPermSize已经失效,取而代之的是-XX:MetaspaceSizeXX:MaxMetaspaceSize。注意:Metaspace已经不再使用堆空间,转而使用Native Memory。关于Native Memory,下文会详细说明。

还有一点需要说明的是,在JDK1.6中,方法区虽然被称为永久代,但并不意味着这些对象真的能够永久存在了,JVM的内存回收机制,仍然会对这一块区域进行扫描,即使回收这部分内存的条件相当苛刻。

 

Runtime Constant Pool (运行时常量池)

回过头来看下图1的下半部分,方法区主要包含:

  1. 运行时常量池(Runtime Constant Pool)
  2. 类信息(Class & Field & Method data)
  3. 编译器编译后的代码(Code)等等
    后面两项都比较好理解,但运行时常量池有何作用,其意义何在?抛开运行时3个字,首先了解下何为常量池。

java源文件经编译后得到存储字节码的class文件,class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在class文件中:哪个字节代表什么含义、长度多少,先后顺序如何都是被严格限定的

       开头4字节存放在魔数(确定该文件是否被jvm接受)接下来4字节存放版本号,接着存放常量池(长度不固定、常量池入口存放着常量池容量计数值

常量池存放两大类常量:字面量和符号引用

1、字面量:相当于Java语言层面常量的概念,字符串常量 声明为final的常量等

2、符号引用:用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义定位到目标即可。理解不了?举个例子,有如下代码:组符号

public class M {
    private int m;
    private String mstring = "chen";
    public void f() {
    }
}

使用javap工具输出M.class文件字节码的部分内容如下:

......
Constant pool:
   #1 = Methodref          #5.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // chen
   #3 = Fieldref           #4.#22         // com/lwork/mdo/M.mstring:Ljava/lang/String;
   #4 = Class              #23            // com/lwork/mdo/M
   #5 = Class              #24            // java/lang/Object
   #6 = Utf8               m
   #7 = Utf8               I
   #8 = Utf8               mstring
   #9 = Utf8               Ljava/lang/String;
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/lwork/mdo/M;
// 方法名称
  #17 = Utf8               f
  #18 = Utf8               SourceFile
// 类名称
  #19 = Utf8               M.java
  #20 = NameAndType        #10:#11        // "<init>":()V
  #21 = Utf8               chen
  #22 = NameAndType        #8:#9          // mstring:Ljava/lang/String;
// 类的完整路径,注意class文件中是用"/"来代替"."
  #23 = Utf8               com/lwork/mdo/M
  #24 = Utf8               java/lang/Object
......

这里只保留了常量池的部分,从中可以看到M.class文件的常量池总共24项,其中包含类的完整名称、字段名称和描述符、方法名称和描述符等等。当然其中还包含IV<init>LineNumberTableLocalVariableTable等代码中没有出现过的常量,其实这些常量是用来描述如下信息:方法的返回值是什么?有多少个参数?每个参数的类型是什么…… 这个示例非常直观的向大家展示了常量池中存储的内容。

接下来就比较好理解运行时常量池了。我们都知道:Class文件中存储的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。运行时常量池就可以理解为常量池被加载到内存之后的版本,但并非只有Class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也可能产生新的常量,它们也可以放入运行时常量池中。



作者:CHEN川
链接:https://www.jianshu.com/p/f8d71e1e8821
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处

猜你喜欢

转载自blog.csdn.net/ma15732625261/article/details/82085055