虚拟机内存与本地内存的区别
Java虚拟机在执行的时候会把管理的内存分配成不同的区域,这些区域被称为虚拟机内存,同时,对于虚拟机没有直接管理的物理内存,也有一定的利用,这些被利用却不在虚拟机内存数据区的内存,称它为本地内存,这两种内存有一定的区别:
JVM内存:
- 受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报OOM。
本地内存:
- 本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制。
虽然不受参数的限制,但是如果内存的占用超出物理内存的大小,同样也会报OOM。
方法区(元空间)
方法区绝对是网上所有关于java内存结构文章争论的焦点,因为方法区的实现在java8做了一次大革新,现在来讨论一下:
方法区是所有线程共享的内存,在java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制,在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现,并直接放到了本地内存中,不受JVM参数的限制(当然,如果物理内存被占满了,方法区也会报OOM),并且将原来放在方法区的字符串常量池和静态变量都转移到了Java堆中,方法区与其他区域不同的地方在于,方法区在编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:
类元信息(Klass)类元信息在类编译期间放入方法区,里面放置了类的基本信息,包括类的版本、字段、方法、接口以及常量池表(Constant Pool Table)。
- 常量池表(Constant Pool Table)存储了类在编译期间生成的字面量、符号引用(什么是字面量?什么是符号引用?),这些信息在类加载完后会被解析到运行时常量池中。
运行时常量池(Runtime Constant Pool)
- 运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些。
运行时常量池具备动态性,可以添加数据,比较多的使用就是String类的
intern()
方法。堆
Java堆是JVM内存中最大的一块,由所有线程共享,是由垃圾收集器管理的内存区域,主要存放对象实例,当然由于Java虚拟机的发展,堆中也多了许多东西,现在主要有:
对象实例
- 类初始化生成的对象。
- 基本数据类型的数组也是对象实例.
- 字符串常量池
- 字符串常量池原本存放于方法区,jdk7开始放置于堆中。
- 字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table。
- 静态变量
- 静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中。
- 线程分配缓冲区(Thread Local Allocation Buffer)
- 线程私有,但是不影响Java堆的共性。
- 增加线程分配缓冲区是为了提升对象分配时的效率。
堆一般会被分成年轻代和老年代。而年轻代又会被进一步分为1个Eden区和2个Survivor区。在内存分配上,如果保持默认配置的话,年轻代和老年代的内存大小比例为1 : 2,年轻代中的1个Eden区和2个Survivor区的内存大小比例为:8 : 1 : 1。
从 JDK 7 开始,Java 虚拟机已经默认开启逃逸分析了,意味着如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
逃逸分析(Escape Analysis),简单来讲就是,Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。
Java 创建的对象存放分析
栈
Java虚拟机栈是线程私有的,生命周期和线程一致,存储的是一个个栈帧,每个栈帧对应着一个被调用的方法。栈遵循的是后进先出的原则。
1、局部变量表
顾名思义,就是用来存储方法中的局部变量的,包括方法的参数。对于基本数据类型的变量,直接存储变量的值;对于引用类型的变量,存储的是对象的引用。局部变量表的大小在编译期间就确定了,程序执行期间,它的大小是不会改变的。
2、操作数栈
表达式的计算是在操作数栈中完成的。当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是入栈/出栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。
3、方法返回地址
方法执行完(不论是正常执行还是发生了异常)后需要返回到方法被调用的位置,程序才能继续执行,方法返回地址保存一些用来帮助恢复上层方法的执行状态的信息。
4、动态链接
每个栈帧都包含了一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
本地方法栈
本地方法栈与Java虚拟机栈的的作用和原理非常相似,其区别只不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
程序计数器(PC寄存器)
程序计数器,可以看作是当前线程所执行的字节码指令的行号指示器。
由于在JVM中多线程是通过线程轮流切换来换取CPU执行时间的,在任何一个确定的时刻,一个CPU只会执行一条线程的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。
程序计数器占用内存空间非常小,是线程私有的,每个线程运行的时候都会有一个独立的计数器,是Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。如果程序执行的时候线程执行的是一个Java方法,计数器是有值的,其记录的是程序正在执行的字节码指令的地址;如果执行的是Natvie方法,计数器的值则为空(Undefined)。
常见问题
什么是Native方法?
由于java是一门高级语言,离硬件底层比较远,有时候无法操作底层的资源,于是,java添加了native
关键字,被native关键字修饰的方法可以用其他语言重写,这样,就可以写一个本地方法,然后用C语言重写,这样来操作底层资源。当然,使用了native
方法会导致系统的可移植性不高,这是需要注意的。
成员变量、局部变量、类变量分别存储在内存的什么地方?
类变量
- 类变量是用
static
修饰符修饰,定义在方法外的变量,随着Java进程产生和销毁 - 在Java8之前把静态变量存放于方法区,在Java8时存放在堆中
成员变量
- 成员变量是定义在类中,但是没有
static
修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分 - 由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中
局部变量
- 局部变量是定义在类的方法中的变量
- 在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中
由 final 修饰的常量存放在哪里?
final
关键字并不影响在内存中的位置,具体位置请参考上一问题。类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?
类常量池与运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中。
在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符,在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池;
JDK1.8版本的字符串常量池中存的是字符串对象,以及字符串常量值。String类的intern()方法:一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。 对于任意两个字符串s和t,当且仅当s.equals(t)为true时,s.intern() == t.intern()才为true。所有字面值字符串和字符串赋值常量表达式都使用 intern方法进行操作。
什么是字面量?什么是符号引用?
字面量
java代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:
int a=1;//这个1便是字面量
String b="iloveu";//iloveu便是字面量
符号引用
由于在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,所以如果在一个类中引用了另一个类,那么完全无法知道他的内存地址,那怎么办,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取他的内存地址。
例子:在com.demo.Solution类中引用了com.test.Quest,那么会把com.test.Quest作为符号引用存到类常量池,等类加载完后,拿着这个引用去方法区找这个类的内存地址。