JVM学习-运行时数据区2
Posted TyuIn
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM学习-运行时数据区2相关的知识,希望对你有一定的参考价值。
上一篇博客我们讲了运行时数据区的一些部分的内容,接下来是运行时数据区中最为重要的两个部分 方法区【Method Area】及 堆【Heap】的介绍。
一、方法区(Method Area)
方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。方法区通常和永久区(Perm)关联在一起,但永久代与方法区不是一个概念,只是有的虚拟机用永久代来实现方法区,这样就可以用永久代GC来管理方法区,省去专门内存管理的工作。根据Java虚拟机规范的规定,当方法区无法满足内存分配的需求时,将抛出 OutOfMemoryError 异常。
1、方法区的组成
- 类型信息:类的完整名称、类的直接父类的完整名称、类的直接实现接口的有序列表、类型标志(类类型还是接口类型)、类的修饰符(public private defautl abstract final static)
- 类型的常量池:(该部分是独有的,然后运行时,把该部分加载进运行时常量池,当调用时则从符号引用解析为直接引用,但是有些确定的方法会直接转换,比如静态方法,比如构造方法),存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类型、字段、方法的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用到的类型、字段、方法的符号引用,所以它也是动态连接(栈中对应的方法指向这个引用)的主要对象(在动态链接中起到核心作用)。
- 字段信息(该类声明的所有字段):字段修饰符(public、peotect、private、default)、字段的类型、字段名称
- 方法信息:1. 方法名 2.方法的返回类型(包括void)3. 方法参数的类型、数目以及顺序 4. 方法修饰符(public、private、protected、static、final、synchronized、native、abstract) 5. 针对非本地方法,还有些附加方法信息需要存储在方法区中(局部变量表大小和操作数栈大小、方法体字节码、异常表)
- 类变量(静态变量):指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。它们与类进行绑定。
- 指向类加载器的引用:每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。
- 指向Class实例的引用:类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。
- 方法表:JVM 对每个加载的非虚拟类的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法)。JVM 可以通过方法表快速激活实例方法。
- 运行时常量池(Runtime Constant Pool)
2、运行时常量池(Runtime Constant Pool)
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用。其中,字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等;而符号引用则属于编译原理方面的概念,包括以下三类常量:类和接口的全限定名、字段的名称和描述符 和 方法的名称和描述符。因为运行时常量池(Runtime Constant Pool)是方法区的一部分,那么当常量池无法再申请到内存时也会抛出 OutOfMemoryError 异常。
运行时常量池相对于Class文件常量池的一个重要特征是具备动态性。Java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如字符串的手动入池方法intern()。
3、方法区的演变细节
4、扩展
4.1 方法区的回收
方法区的内存回收目标主要是针对 常量池的回收 和 对类型的卸载。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
4.2 判定一个类是否是“无用的类”
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:(判断类是无用类的三个条件)
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收(卸载),这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。特别地,在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
4.3 StringTable移入堆空间
jdk7将StringTable放入堆空间,因为永久代回收效率低,full gc时候才会触发,而full gc是老年代的空间不足,永久代空间不足才触发。这就导致StringTable回收效率低,而开发中有大量的字符串被创建,回收效率低,导致内存不足。当放入堆中,提高回收效率。
二、堆 (Heap)
对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
jdk7与jdk8堆内存结构比较
1、新生代(Yong/New Generation)
新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。
HotSpot将新生代划分为三块,一块较大的Eden(伊甸)空间和两块较小的Survivor(幸存者)空间,默认比例为8:1:1。(但是这个比例在虚拟机中开启了自适应调节策略,比例进行了调节 6:1:1,在后面垃圾回收篇介绍)划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
2、老年代(Tenured/Old Generationn)
在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
本篇主要记录博主在学习java虚拟机过程的笔记以及自己的总结,内容及配图来源《深入理解java虚拟机(第三版)-JVM高级特性与最佳实践 》,书的作者是:周志明老师。文章中的图片主要是尚硅谷JVM课程学习的图片,有兴趣的可以去 blibli 搜索学习。【文章中的内容参考的文章忘记是哪篇了,如有侵权,请联系我】
以上是关于JVM学习-运行时数据区2的主要内容,如果未能解决你的问题,请参考以下文章