参悟jvm方法区(传递、静态等等)
1874 2021-10-10 10:56
java内存管理里分三个区域:栈、堆、方法区
《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 同时大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。换句话说:方法区是一种规范,永久代是Hotspot针对这一规范的一种实现。而永久代本身也在迭代中:
在Java 6中,方法区中包含的数据,除了JIT编译生成的代码存放在native memory的CodeCache区域,其他都存放在永久代;
在Java 7中,Symbol的存储从PermGen移动到了native memory,并且把静态变量从instanceKlass末尾(位于PermGen内)移动到了java.lang.Class对象的末尾(位于普通Java heap内);
在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间(Metaspace),‑XX:MaxPermSize 参数失去了意义,取而代之的是-XX:MaxMetaspaceSize。
对于Java8, HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它与永久代有什么不同的?
存储位置不同,永久代是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;
存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
1.程序计数器(Program Counter Register)
2.Java虚拟机栈(Java Virtual Machine Stacks)
3.本地方法栈(Native Method Stacks)
4.Java堆(Java Heap)
5.方法区(Method Area)——又名Non-Heap
6.运行时常量池(Runtime Constant Pool)
7.直接内存(Direct Memory)
类方法里的非引用变量(基本数据类型有8种)直接放在栈中,reference 类型回去堆中建立。而reference 类型的对象类型数据(也就是类的版本、字段、方法、接口等描述等信息),要去方法区中寻找,寻找的方式主流的访问方式有两种:使用句柄和直接指针。
使用句柄:
直接指针:
由一个例子开始:
上面的过程实际上就是装入class文件到1.方法区(查找定义)、2.分配堆空间(包括声明成员变量以及有时候初始化成员变量为默认值)、3.分配栈空间(方法调用后,在栈帧中赋值给局部变量进行运算等操作)
栈Java方法执行的内存模型,其组成是一个一个的栈帧。栈帧是给方法用的,所以速度快。
堆是给对象用的,所以成员变量会存储在堆上,不管你是不是八大基本数据类型。
栈内存是线程私有的,当线程执行一个方法,这个方法作为栈帧入栈,同时方法内部的局部变量也会在栈帧内分配内存,其中基本类型的值就直接保存在栈帧里。
而大多数对象初始化会在堆内存开辟空间,然后把这个堆内存的地址给引用类型变量。堆内存非线程私有,几乎所有对象都保存在这个这个区域,对象内的基本类型的成员变量也保存在这个区域。所以堆内存中的对象可以同时被多个线程访问,并非线程安全。
所以方法中的基本类型局部变量一定是保存在线程的栈内存,而在方法中构造的对象的基本类型成员变量,如果该对象是分配在堆内存,那么它的成员变量自然是在堆内存保存。
上面我没有说对象的基本类型成员变量就一定是在堆内分配内存的,因为虚拟机会对方法做逃逸分析优化,如果一个对象在方法内创建,它的引用不会离开方法,也就是这个对象的生命周期随着栈帧出栈结束,那么虚拟机为了高效回收内存可能就把这个对象分配在栈内了,所以该对象的基本类型成员变量就保存在栈上。以上为引用:-----------------以上。
为了支持分离式编译,c++语言将声明和定义区分来。
declaration
声明说明了变量的名字和类型,但并不分配存储空间。
definition
定义也说明了变量的名字和类型,而且还为它分配了存储空间。并不一定要填充初始值
initialization
初始化是一种特殊的定义,初始化 = 定义的同时并赋初始值。
assignment
将某一个数值赋给一个变量的过程,赋值只针对于已分配存储空间的的变量而言。可以多次进行。在对类成员变量在构造函数内部赋值和在初始值列表中初始化而言,二者具有一些差异,这会影响程序的效率。
变量的声明有两种情况:
一种是需要建立存储空间的。例如:int a。在声明的时候就已经建立了存储空间。这种声明是"定义性声明(defining declaration)",即我们平时所说的“定义”。
另一种是不需要建立存储空间的,只是告诉编译器某变量已经在别处定义过了。例如:extern int a。其中,变量a是在别处定义的。这种声明是"引用性声明(referncing declaration)",即我们平时所说的“声明”。
C++中:
1. extern int i; // 声明(但不分配内存),类初始化的时候会给默认值0
2. int i; // 定义(声明+分配了存储空间。并不一定要填充初始值)
3. int i;
i = 99; // 赋值
4. //初始化
C++中如果内置属性变量未被显示初始化,它的值由定义的位置决定:
定义于任何函数体之外的变量被初始化为0;定义在函数体内部的内置类型变量将不被初始化(uninitialized)。
为什么一定要初始化(没有初始化的后果):
一个未被初始化的内置属性变量的值是未定义的,如果试图拷贝或者以其他形式访问此类值将引发错误。
Java中
成员变量:定义在类中,方法体之外。
局部变量:定义在方法体,构造方法,语句块中的变量。
局部变量必须要初始化
有意思的字符串存储:
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
类和接口的全限定名
字段名称和描述符
方法名称和描述符
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
尽管在输出中调用intern方法并没有什么效果,但是实际上后台这个方法会做一系列的动作和操作。在调用”ab”.intern()方法的时候会返回”ab”,但是这个方法会首先检查字符串池中是否有”ab”这个字符串,如果存在则返回这个字符串的引用,否则就将这个字符串添加到字符串池中,然会返回这个字符串的引用。
另:static的方法存在方法区,与类信息同境界,而对于属性中不是变量的常量(常量是用final修饰的成员变量,java没有C语言的 constant),也存在方法区中称之为常量池,这些不会在多线程多实例产生多份的东西极大的优化了内存的使用。死记硬背这些划分是没什么用的。如果知道当初为什么划分出来,是为了解决什么资源问题或者受限于什么历史因素。那么你就会对发展和实现有更深层次的理解。而过程往往就是真实的进化。近乎天道。
物理实现的一些粗犷做法,如果合情合理,就会触类旁通左右逢源的恰巧顺路反应了现实的真谛:那就是属性不属于类,而属于各个实例。属于类的也就是那些方法,那些存储在方法区的方法才是表明了类是个干什么用的东西。而这些方法所操作的变量(也就是属性),如果是静态的,那么就归于类(方法区存储),如果是非静态的,那么就归于本地变量表(栈)或者引用(堆)。多线程中,方法是不会互相干扰的。会产生迷惑和干扰的原因,往往是那些实例属性共享造成的。所以如何判断实例的属性状态,才是需要好好设置锁给方法的关键。诀窍在于每个实例属性的具体,以及类的那些对于属性统一方法的抽象。分得清的人,才能不混乱的设计好一个不打架的系统。
全部评论