前言

从本篇开始咱们就要进入运行时数据区的办法区学习

一、栈、堆、办法区的交互关系


那么接下来咱们从线程共享与否的角度来看运行时数据区看看是怎么样的?

上面就是看看栈、堆、办法区的交互关系是怎么样的?

从简略的代码角度登程,以后申明的变量对象是person,类型则是Person类。

针对于这个类型咱们须要将它加载到办法区,咱们new 的对象放入堆空间当中

接下来咱们从栈堆办法区的内存构造来看看是怎么样的?

二、办法区的了解


能够进入官网文档查看更具体的介绍与理解:拜访入口

办法区在哪里?

================================

《Java虚拟机标准》中明确阐明:只管所有的办法区在逻辑上是属于堆的一部分,但一些简略的实现可能不会抉择去进行垃圾收集或者进行压缩

但对于HotSpotJVM而言办法区还有一个别名叫做Non-Heap(非堆),目标就是要和堆离开

办法区次要寄存的是 Class,而堆中次要寄存的是实例化的对象

办法区根本了解

================================

办法区(Method Area)与Java堆一样,是各个线程共享的内存区域

多个线程同时加载对立个类时,只能有一个线程能加载该类,其余线程只能等期待该线程加载结束,而后间接应用该类,即类只能加载一次

办法区在JVM启动的时候被创立,并且它的理论的物理内存空间中和Java堆区一样都能够是不间断的

办法区的大小,跟堆空间一样,能够抉择固定大小或者可扩大

办法区的大小决定了零碎能够保留多少个类,如果零碎定义了太多的类导致办法区溢出

虚拟机同样会抛出内存溢出谬误:

  • java.lang.OutofMemoryError:PermGen space (JDK 7 前)
  • java.lang.OutOfMemoryError:Metaspace(JDK 7 后)

咱们能够应用一个示例并且关上Java VisualVM查看到底加载了多少个类

public class MethodAreaDemo {    public static void main(String[] args) {        System.out.println("start...");        try {           Thread.sleep(1000000);        }catch(IhterruptedException e) {            e.printStackTrace();        }            System.out.println("end...");    }}

可见咱们这么简略的代码,还是会加载一千多的类。那么当什么状况下加载太多的类会爆异样呢?

  • 加载大量的第三方的jar包
  • Tomcat部署的工程过多(30~50个)
  • 大量动静的生成反射类

当咱们敞开JVM虚拟机的时候,就会开释这个区域的内存

Hotstop中办法区的演进

================================

在 JDK 7 及以前习惯上把办法区称为永恒代。JDK 8 开始应用元空间取代了永恒代

咱们能够将办法区类比为Java中的接口,将永恒代或元空间类比为Java中具体的实现类

实质上办法区和永恒代并不等价,但仅对Hotspot而言的能够看作等价

《Java虚拟机标准》对如何实现办法区,不做对立要求

例如:BEAJRockit / IBM J9 中不存在永恒代的概念

再比方在广东地区的人们喜爱将动物:狗称说为旺财,在广东看来是等价的,但在别的中央并不等价

在 JDK 8 终于齐全废除了永恒代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替

而元空间的实质和永恒代实质有一些相似,都是对JVM标准中办法区的实现。不过元空间与永恒代最大的区别在于:元空间不在虚拟机设置的内存中,而是应用本地内存

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

依据《Java虚拟机标准》的规定,如果办法区无奈满足新的内存调配需要时,将抛出OOM异样

三、设置办法区大小与OOM


在JVM标准中提到:办法区的大小不用是固定的,JVM能够依据利用的须要动静调整

JDK7及以前(永恒代)设置

================================

  • 通过-XX:Permsize来设置永恒代初始调配空间。默认值是20.75M
  • -XX:MaxPermsize来设定永恒代最大可调配空间。32位机器默认是64M,64位机器模式是82M

当JVM加载的类信息容量超过了这个值,会报异样OutofMemoryError:PermGen space

咱们将之前的那个类应用JDK7及以前的环境运行起来看看这个调配空间的大小

当咱们运行起来后,在应用命令查看这个类看看是多少把

那么应用JDK 8 的环境下,是不能应用这两个参数的,在JVM 参数列表有阐明该状况

JDK8及当前(元空间)设置

================================

元数据区大小能够应用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定

默认值依赖于平台Windows下

  • -XX:MetaspaceSize 约为21M
  • -XX:MaxMetaspaceSize的值是-1,即没有限度

-XX:MetaspaceSize:对于一个 64位 的服务器端 JVM 来说,其默认的值为21MB。

这就是初始的高水位线一旦涉及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),而后这个高水位线将会重置

新的高水位线的值取决于GC后开释了多少元空间。如果开释的空间有余,那么在不超过MaxMetaspaceSize时,适当进步该值。如果开释空间过多,则适当升高该值

咱们将刚刚JDk 7 及以前的环境转为JDK 8 在运行起来看看

当咱们运行起来后,在应用命令查看这个类看看是多少把

如果初始化的高水位线设置过低,上述高水位线调整状况会产生很屡次。

通过垃圾回收器的日志能够察看到Full GC屡次调用。为了防止频繁地GC,倡议将-XX:MetaspaceSize设置为一个绝对较高的值

四、办法区的内部结构


接下来咱们将介绍办法区的内部结构,就是办法区里到底存储的是什么信息?

请先看下图的简图

在《深刻了解Java虚拟机》书中对办法区(Method Area)存储内容形容如下:它用于存储已被虚拟机加载的类型信息、常量、动态变量、即时编译器编译后的代码缓存等

接下来针对将这些信息具体开展内容看看具体是什么

类型信息:

对每个加载的类型(类class、接口interface、枚举enum、注解annotation)

JVM必须在办法区中存储以下类型信息:

  • 这个类型的残缺无效名称(全名=包名.类名)
  • 这个类型间接父类的残缺无效名(对于interface或是java.lang.Object,都没有父类)
  • 这个类型的修饰符(public,abstract,final的某个子集)
  • 这个类型间接接口的一个有序列表
域(属性)信息:

JVM必须在办法区中保留类型的所有域的相干信息以及域的申明程序

域的相干信息包含:域名称,域类型,域修饰符(public,private,protected,static,final,volatile,transient的某个子集)

办法信息:

JVM必须保留所有办法的以下信息,同域信息一样包含申明程序:

  • 办法名称
  • 办法的返回类型(包含 void 返回类型),void 在 Java 中对应的为 void.class
  • 办法参数的数量和类型(按程序)
  • 办法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
  • 办法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native办法除外)
  • 异样表(abstract和native办法除外),异样表记录每个异样解决的开始地位、完结地位、代码解决在程序计数器中的偏移地址、被捕捉的异样类的常量池索引

接下来咱们通过一个示例来查看具体的办法区是怎么记录的?

** * 测试方法区的外部形成 */public class MethodInnerStrucTest extends Object implements Comparable<String>,Serializable {    //属性    public int num = 10;    private static String str = "测试方法的内部结构";    //结构器    //办法    public void test1(){        int count = 20;        System.out.println("count = " + count);    }    public static int test2(int cal){        int result = 0;        try {            int value = 30;            result = value / cal;        } catch (Exception e) {            e.printStackTrace();        }        return result;    }    @Override    public int compareTo(String o) {        return 0;    }}

此时咱们将该类进行编译:工具栏->Build->Recompild...InnerStrucTest.java

咱们能够在编译后的out目录下找到对应的类,并且对Class文件右键抉择Open in...

仅接着能够输出命令:javap -v -p MethodInnerStrucTest.class > test.txt

这时咱们的命令会在以后相对路径下创立一个text.txt的文本保存信息

那么咱们就能够关上该test.txt 文档查看对于类的信息了

以后咱们展现这个类的类型信息

以后咱们展现这个类的域信息

以后咱们展现这个类的办法信息

刚刚咱们举例了一个类并进行编译以及反编译查看办法区里对应的存放数据信息有哪些

那么在这之后咱们再阐明一个信息:non-final 类型的类变量

non-final 类型的类变量

================================

动态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分

类变量被类的所有实例共享,即便没有类实例时,你也能够拜访它

上面咱们应用一个示例来领会一下static润饰的办法与字段

public class MethodAreaTest {    public static void main(String[] args) {        Order order = null;        order.hello();        System.out.println(order.count);    }}class Order {    public static int count = 1;    public static final int number = 2;    public static void hello() {        System.out.println("hello!");    }}//运行后果如下:hello!1

即便咱们把order设置为null也不会呈现空指针异样,表明了 static 类型的字段和办法随着类的加载而加载,并不属于特定的类实例

那么咱们在static上在应用final 润饰呢?

全局常量:static final

================================

应用 static final 进行润饰咱们称说为:全局常量

这样申明为final的类变量的解决办法则不同,每个全局常量在编译的时候就会被调配了

那么咱们依据刚刚的示例代码编译并查看一下是怎么样回事呢?

关上咱们的text1.txt文档看看具体内容是什么呢?

运行时常量池:

在咱们以后办法区的时候会有一个:运行时常量池,而在字节码文件外部蕴含了常量池

要想搞懂运行时常量池就要先搞懂字节码里的常量池

下面咱们举例子并进行编译查看字节码文件的类型信息、域信息、办法信息

这些信息通过类加载器把咱们刚刚提到的信息加载到办法区外面

那么咱们字节码文件当中的常量池,把它加载到办法区构造里咱们就称为运行时常量池

那么字节码文件的常量池在哪呢?长什么样子呢?

咱们能够拜访官网文档查看ClassFile主体构造长什么样样子:拜访入口

那么咱们应用图画的构造大略就是以下的图片那样

那么为什么须要提供一个常量池呢?常量池是干什么的呢?有什么用呢?

咱们这里举一个代码的例子

public class SimpleClass {    public void sayHello() {        System.out.println("hello");    }}

当咱们将它这源文件编译后产生一个字节码文件,大略只有194字节然而外面却应用了String、System、PrintStream及Object等构造。

若咱们代码多的话,援用到的构造会更多。比如说咱们这个文件中有6个中央用到了”hello”这个字符串。

如果不必常量池就须要在6个中央全写一遍,造成臃肿。

若咱们应用常量池的话将”hello”等所需用到的构造信息记录在常量池中,应用的时候通过援用的形式,来加载、调用所需的构造

这就是为什么须要常量池了

常量池中有啥?

================================

  • 数量值
  • 字符串值
  • 类援用
  • 字段援用
  • 办法援用

咱们能够依据后面的示例代码查看一下他的字节码,咱们看看具体有哪些信息用到了

但凡 #3、#5、#13、#15等等这些带#的,都是援用了常量池。

咱们能够将常量池能够看做是一张表,虚拟机指令依据这张常量表找到要执行的类名、办法名、参数类型、字面量等类型

那么运行时常量池是啥?

================================

咱们刚刚介绍了一下常量池是什么,有什么货色。 接下来咱们要介绍的是运行时常量池

咱们说常量池表(Constant Pool Table)是Class字节码文件的一部分,用于寄存编译期生成的各种字面量与符号援用,这部分内容将在类加载后寄存到办法区的运行时常量池中

那么运行时常量池(Runtime Constant Pool)它是办法区的一部分

这时运行时常量池中蕴含多种不同的常量,包含编译期就曾经明确的数值字面量,也包含到运行期解析后才可能取得的办法或者字段援用。此时不再是常量池中的符号地址了,这里换为实在地址

运行时常量池绝对于Class文件常量池的另一重要特色是:具备动态性

运行时常量池相似于传统编程语言中的符号表(symbol table),然而它所蕴含的数据却比符号表要更加丰盛一些

然而当创立类或接口的运行时常量池时,如果结构运行时常量池所需的内存空间超过了办法区所能提供的最大值,则JVM会抛OutofMemoryError异样

五、办法区的应用案例


接下来咱们通过一个应用案例来感触办法区的应用

public class MethodAreaDemo {    public static void main(String[] args) {        int x = 500;        int y = 100;        int a = x / y;        int b = 50;        System.out.println(a + b);    }}

咱们将这段代码进行编译输入Class字节码文件


这时咱们和下面的一样采纳命令将以后文件写入txt文档当中

应用命令:javap -v -p MethodAreaDemo.class > test2. txt

接下来咱们就能够关上查看这个类的字节码文件并开始进行剖析起来了

那么对于main办法咱们次要关注它的字节码指令执行过程是怎么样的,一起来剖析看看吧

第二步程序计数器往下移,执行下一条字节码指令

第三步程序计数器往下移,执行下一条字节码指令

第四步程序计数器往下移,执行下一条字节码指令

第五步程序计数器往下移,执行下一条字节码指令

第六步程序计数器往下移,执行下一条字节码指令

第七步程序计数器往下移,执行下一条字节码指令

那么仅接着执行istore_3的操作,将咱们计算结果:5,压入本地变量表当中里来

那么程序计数器接着往下移,雷同操作我想应该是晓得是什么意思了

咱们接着执行第八步,程序计数器往下移至15,执行下一条字节码指令

刚刚咱们执行计算的时候,将计算结果:5放入本地变量表的索引:3中了

这时咱们程序计数器往下移,执行下一条字节码指令

第九步程序计数器往下移,执行下一条字节码指令

第十步程序计数器往下移,执行下一条字节码指令

第十一步程序计数器往下移,执行下一条字节码指令

这时若是没有其余的操作,则return 返回

下面代码调用 System.out.println() 办法时,首先须要看看 System 类有没有加载,再看看 PrintStream 类有没有加载

如果没有加载,则执行加载,执行时,将常量池中的符号援用(字面量)转换为运行时常量池的间接援用(真正的地址值)

六、办法区演进细节


首先明确:只有Hotspot才有永恒代BEA JRockit、IBMJ9等来说,是不存在永恒代的概念的

原则上如何实现办法区属于虚拟机实现细节,不受《Java虚拟机标准》管教,并不要求对立

那么咱们就说说Hotspot中办法区的变动:

接下来离开应用图画的形式别离解说JDK6、JDK7、JDk8及当前

JDk 6 示意图如下:

JDk 7 示意图如下:

JDk 8 示意图如下:

永恒代为什么要被元空间代替?

================================

咱们之前也说过能够设置虚拟机的内存,然而这个大小是很难确定的。

在某些场景下如果动静加载类过多,容易产生Perm区的OOM。比方某个理论Web工程中因为性能点比拟多在运行过程中,要一直动静加载很多类,也会经常出现致命谬误

因而咱们将这些元空间不设置在虚拟机中而是应用本地内存,这样仅受本地内存限度

并且咱们之前对永恒待进行调优是比拟艰难的,对于办法区的垃圾收集次要回收两局部内容:

  • 常量池中废除的常量
  • 不再用的类型

HotSpot虚拟机对常量池的回收策略:只有常量池中的常量没有被任何中央援用,就能够被回收

那么对于一个类型是否属于“不再被应用的类”的条件就比拟刻薄了须要同时满足上面三个条件:

  • 该类所有的实例都曾经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
  • 加载该类的类加载器曾经被回收
  • 该类对应的java.lang.Class对象没有在任何中央被援用并通过反射拜访该类的办法

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

字符串常量池 StringTable 为什么要调整地位?

================================

JDK7中将StringTable放到了堆空间中。

然而永恒代的回收效率很低,在Full GC的时候才会执行永恒代的垃圾回收,而Full GC是老年代的空间有余、永恒代有余时才会触发

这就导致StringTable回收效率不高,而咱们开发中会有大量的字符串被创立,回收效率低,导致永恒代内存不足。所以调整到放到堆里,能及时回收内存

谈谈动态变量放在哪呢?

================================

咱们应用示例代码去说说这个事件,代码块如下:

public class StaticFieldTest {    private static byte[] arr = new byte[1024 * 1024 * 100];//100MB    public static void main(String[] args) {        System.out.println(StaticFieldTest.arr);    }}

咱们先看看JDk 7 及以前的环境到底是怎么样的


咱们先看看JDk 8 的环境到底是怎么样的

咱们能够晓得对象的自身始终是在堆空间的,不同的是指向这个对象的变量自身的地位不同

应用JHSDB工具来进行剖析

================================

这个工具是JDK9开始自带的(JDK9以前没有),在bin目录下能够找到

接下来咱们应用示例演示工具的剖析

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");        }    }        private static class ObjectHolder { }        public static void main(String[] args) {        Test test = new StaticObjTest.Test();        test.foo();    }}

咱们的问题是:staticObj、instanceObj、localObj变量寄存在哪里?自身寄存在哪里?

依据后面咱们学习的局部变量、实例对象、以及类变量的常识,咱们能够得出

  • localObject则是寄存在foo()办法栈帧的局部变量表中
  • instanceObj随着Test的对象实例寄存在Java堆
  • staticObj随着Test的类型信息寄存在办法区

那么接着咱们会在一个Java.lang.Class的实例里找到一个援用该staticObj对象的中央

那么在咱们的《Java虚拟机标准》所定义的概念模型来看,所有Class相干的信息都应该寄存在办法区之中,但办法区该如何实现《Java虚拟机标准》并未做出规定,这就成了一件容许不同虚拟机本人灵便把握的事件

而咱们的JDK7及其当前版本的HotSpot虚拟机抉择把动态变量与类型在Java语言一端的映射Class对象寄存在一起存储于Java堆之中,从咱们的试验中也明确验证了这一点

七、办法区的垃圾回收


有些人认为办法区(如Hotspot虚拟机中的元空间或者永恒代)是没有垃圾收集行为的

其实不然,在《Java虚拟机标准》中对办法区的束缚是十分宽松的,提到过能够不要求虚拟机在办法区中实现垃圾收集。

事实上也的确有未实现或未能残缺实现办法区类型卸载的收集器存在(如JDK11期间的ZGC收集器就不反对类卸载)

一般来说这个区域的回收成果比拟难令人满意,尤其是类型的卸载,条件相当刻薄。然而这部分区域的回收有时又的确是必要的。

以前sun公司的Bug列表中,曾呈现过的若干个重大的Bug就是因为低版本的HotSpot虚拟机对此区域未齐全回收而导致内存透露

咱们刚刚说办法区的垃圾收集次要回收两局部内容:常量池中废除的常量和不再应用的类型

先来说说办法区内常量池之中次要寄存的两大类常量:字面量和符号援用

字面量比拟靠近Java语言档次的常量概念,如文本字符串、被申明为final的常量值等

而符号援用则属于编译原理方面的概念,包含上面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 办法的名称和描述符

而对于不再应用类型咱们后面也说到了,条件比拟刻薄,须要同时满足上面是三个条件

  • 该类所有的实例都曾经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
  • 加载该类的类加载器曾经被回收
  • 该类对应的java.lang.Class对象没有在任何中央被援用并通过反射拜访该类的办法

Java虚拟机被容许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被容许”,而并不是和对象一样,没有援用了就必然会回收。

那么咱们对于是否要对类型进行回收HotSpot虚拟机提供了-Xnoclassgc参数进行管制,还能够应用-verbose:class 以及 -XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息

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

八、运行时数据区总结


咱们后面也说到了线程公有的中央有:程序计数器、本地办法栈、虚拟机栈

而线程共有的中央有:办法区、堆空间,并且办法区是hotstop 虚拟机才有的

九、间接内存


咱们这里说的间接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机标准》中定义的内存区域。

咱们说的间接内存是在Java堆外的、间接向零碎申请的内存区间。来源于NIO通过存在堆中的DirectByteBuffer操作Native内存

通常拜访间接内存的速度会优于Java堆,即读写性能高

  • 因而出于性能思考,读写频繁的场合可能会思考应用间接内存
  • Java的NIO库容许Java程序应用间接内存,用于数据缓冲区

接下来咱们举一个例子来领会一下间接内存

public class BufferTest {    private static final int BUFFER = 1024 * 1024 * 1024;//1GB    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();    }}

当咱们运行起该类的时候,再关上工作管理器查看一下咱们这个间接内存的应用

这时咱们就能够晓得DirectByteBuffer会间接操作本地内存的空间

BIO与NIO

================================

传统的架构在读写本地文件时,咱们须要从用户态切换成内核态

当咱们应用NIO 时会间接操作物理磁盘,省去了两头过程

间接内存与OOM

================================

间接内存也可能导致OutofMemoryError异样,因为因为间接内存在Java堆外,因而它的大小不会间接受限于-Xmx指定的最大堆大小,然而零碎内存是无限的.Java堆和间接内存的总和仍然受限于操作系统能给出的最大内存

间接内存的毛病为:

  • 调配回收老本较高
  • 不受JVM内存回收治理

间接内存大小能够通过MaxDirectMemorySize设置,如果不指定默认与堆的最大值-Xmx参数值统一

参考资料


尚硅谷:JVM虚拟机(宋红康老师)