参考:http://www.hollischuang.com/archives/80
http://www.hollischuang.com/archives/2509
Java虚拟机的内存组成以及堆内存介绍
一、java内存组成介绍:堆(Heap)
和非堆(Non-heap)
内存
按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给 自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中。
二、JVM内存区域模型
1.方法区 也称”永久代” 、“非堆”, 它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize
和 -XX:MaxPermSize
参数限制方法区的大小。
运行时常量池:是方法区的一部分,其中的主要内容来自于JVM对Class的加载。
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
2.虚拟机栈
描述的是java 方法执行的内存模型:每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。
局部变量表存放了编译器可知的各种基本数据类型(boolean
、byte
、char
、short
、int
、float
、long
、double
)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
3.本地方法栈
与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。
4.堆
也叫做java 堆、GC堆是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在JVM启动时创建。该内存区域存放了对象实例及数组(所有new的对象)。其大小通过-Xms
(最小值)和-Xmx
(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx
为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx
指定的大小,可通过-XX:MinHeapFreeRation=
来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=
来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。
由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。
新生代: 程序新创建的对象都是从新生代分配内存,新生代由
Eden Space
和两块相同大小的Survivor Space
(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation
来调整Eden Space
及Survivor Space
的大小。 老年代: 用于存放经过多次新生代GC任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:①.大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024
(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。②.大的数组对象,切数组中无引用外部对象。 老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
Young Generation 即图中的Eden + From Space + To Space
Eden 存放新生的对象
Survivor Space 有两个,存放每次垃圾回收后存活的对象
Old Generation Tenured Generation 即图中的Old Space
主要存放应用程序中生命周期长的存活对象
5.程序计数器
是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
6、堆和栈的区别
-
功能不同
- 栈内存用来存储局部变量和方法调用。
-
而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
-
共享性不同
- 栈内存是线程私有的。
- 堆内存是所有线程共有的。
-
异常错误不同
如果栈内存或者堆内存不足都会抛出异常。
- 栈空间不足:java.lang.StackOverFlowError。
- 堆空间不足:java.lang.OutOfMemoryError。
-
空间大小
栈的空间大小远远小于堆的。
三、直接内存
直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。
四、Java堆内存的10个要点
- Java堆内存是操作系统分配给JVM的内存的一部分。
- 当我们创建对象时,它们存储在Java堆内存中。
- 为了便于垃圾回收,Java堆空间分成三个区域,分别叫作New Generation, Old Generation或叫作Tenured Generation,还有Perm Space。
- 你可以通过用JVM的命令行选项 -Xms, -Xmx, -Xmn来调整Java堆空间的大小。不要忘了在大小后面加上”M”或者”G”来表示单位。举个例子,你可以用 -Xmx256m来设置堆内存最大的大小为256MB。
- 你可以用JConsole或者 Runtime.maxMemory(), Runtime.totalMemory(), Runtime.freeMemory()来查看Java中堆内存的大小。
- 你可以使用命令“jmap”来获得heap dump,用“jhat”来分析heap dump。
- Java堆空间不同于栈空间,栈空间是用来储存调用栈和局部变量的。
- Java垃圾回收器是用来将死掉的对象(不再使用的对象)所占用的内存回收回来,再释放到Java堆空间中。
- 当你遇到java.lang.outOfMemoryError时,不要紧张,有时候仅仅增加堆空间就可以了,但如果经常出现的话,就要看看Java程序中是不是存在内存泄露了。
- 请使用Profiler和Heap dump分析工具来查看Java堆空间,可以查看给每个对象分配了多少内存。
五、深入理解栈——栈的组成
栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,他们是按字长计算的。但调用一个方法时,它从类型信息中得到此方法局部变量区和操作数栈大小,并据此分配栈内存,然后压入Java栈。
-
局部变量区
局部变量区被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值即可,如某个long值在局部变量区中占据的索引时3、4项,取值时,指令只需取索引为3的long值即可。
如下代码以及图所示:
-
操作数栈
和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。可把操作数栈理解为存储计算时,临时数据的存储区域。下面我们通过一段简短的程序片段外加一幅图片来了解下操作数栈的作用。
int a = 1;
int b = 98;
int c = a+b;
从图中可以得出:操作数栈其实就是个临时数据存储区域,它是通过入栈和出栈来进行操作的。
帧数据区
除了局部变量区和操作数栈外,java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在java栈帧的帧数据区中。
当JVM执行到需要常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。
除了处理常量池解析外,帧里的数据还要处理java方法的正常结束和异常终止。如果是通过return正常结束,则当前栈帧从Java栈中弹出,恢复发起调用的方法的栈。如果方法又返回值,JVM会把返回值压入到发起调用方法的操作数栈。
为了处理java方法中的异常情况,帧数据区还必须保存一个对此方法异常引用表的引用。当异常抛出时,JVM给catch块中的代码。如果没发现,方法立即终止,然后JVM用帧区数据的信息恢复发起调用的方法的帧。然后再发起调用方法的上下文重新抛出同样的异常。
栈的整个结构
在前面就描述过:栈是由栈帧组成,每当线程调用一个java方法时,JVM就会在该线程对应的栈中压入一个帧,而帧是由局部变量区、操作数栈和帧数据区组成。那在一个代码块中,栈到底是什么形式呢?下面是我从《深入JVM》中摘抄的一个例子,大家可以看看:
public class Main{
public static void addAndPrint(){
double result = addTwoTypes(1,88.88);
System.out.println(result);
}
public static double addTwoTypes(int i,double d){
return i + d;
}
}
执行过程中的三个快照:
上面所给的图,只想说明两件事情:
- 只有在调用一个方法时,才为当前栈分配一个帧,然后将该帧压入栈
- 帧中存储了对应方法的局部数据,方法执行完,对应的帧则从栈中弹出,并把返回结果存储在调用 方法的帧的操作数栈中
常见误区
一、Java中的基本数据类型一定存储在栈中吗?
不一定。栈内存用来存储局部变量和方法调用。
如果该局部变量是基本数据类型例如
int a = 1;
那么直接将该值存储在栈中。
如果该局部变量是一个对象如
int[] array=new int[]{1,2};
那么将引用存在栈中而对象({1,2})存储在堆内。
二、栈的速度比堆快吗?
即一定情况下栈的速度是比堆快的,但是快的并不明显。毕竟都是RAM。所以这算不上堆和栈的一大区别。
总结:
基本类型数据如果是局部变量并且非对象那么JVM中是把值直接存入栈中的而不是存储一个引用对象然后借由这个对象来找到值。这其实算的上是实际运行时JVM提供的性能优化。因此基本数据类型和引用类型在栈中的存储情况就是不一样的了。
但是这些不一样,对于用户(程序员)来说是透明的。所以如果仅仅从语义的角度把基本类型看成引用类型,虽然不够严谨,但是对于使用者(程序员)来说有利于理解和学习。
Java 内存之方法区和运行时常量池
一、相关特征
1、方法区特征
- 同 Java 堆一样,方法区也是全局共享的一块内存区域
- 方法区的作用是存储 Java 类的结构信息,当我们创建对象实例后,对象的类型信息存储在方法堆之中,实例数据存放在堆中;实例数据指的是在 Java 中创建的各种实例对象以及它们的值,类型信息指的是定义在 Java 代码中的常量、静态变量、以及在类中声明的各种方法、方法字段等等;同事可能包括即时编译器编译后产生的代码数据。
- JVMS 不要求该区域实现自动的内存管理,但是商用 JVM 一般都已实现该区域的自动内存管理。
- 方法区分配内存可以不连续,可以动态扩展。
- 该区域并非像 JMM 规范描述的那样数据一旦放进去就属于 “永久代”;在该区域进行内存回收的主要目的是对常量池的回收和对内存数据的卸载;一般来说这个区域的内存回收效率比起 Java 堆要低得多。
- 当方法区无法满足内存需求时,将抛出 OutOfMemoryError 异常。
2、运行时常量池的特征
- 运行时常量池是方法区的一部分,所以也是全局共享的。
- 其作用是存储 Java 类文件常量池中的符号信息。
- class 文件中存在常量池(非运行时常量池),其在编译阶段就已经确定;JVM 规范对 class 文件结构有着严格的规范,必须符合此规范的 class 文件才会被 JVM 认可和装载。
- 运行时常量池 中保存着一些 class 文件中描述的符号引用,同时还会将这些符号引用所翻译出来的直接引用存储在 运行时常量池 中。
- 运行时常量池相对于 class 常量池一大特征就是其具有动态性,Java 规范并不要求常量只能在运行时才产生,也就是说运行时常量池中的内容并不全部来自 class 常量池,class 常量池并非运行时常量池的唯一数据输入口;在运行时可以通过代码生成常量并将其放入运行时常量池中。
- 同方法区一样,当运行时常量池无法申请到新的内存时,将抛出 OutOfMemoryError 异常。
二、HotSpot 方法区变迁
1、JDK1.2 ~ JDK6
在 JDK1.2 ~ JDK6 的实现中,HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;
2、JDK7
由于 GC 分代技术的影响,使之许多优秀的内存调试工具无法在 Oracle HotSpot之上运行,必须单独处理;并且 Oracle 同时收购了 BEA 和 Sun 公司,同时拥有 JRockit 和 HotSpot,在将 JRockit 许多优秀特性移植到 HotSpot 时由于 GC 分代技术遇到了种种困难,所以从 JDK7 开始 Oracle HotSpot 开始移除永久代。
JDK7中符号表被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。
3、JDK8
在 JDK8 中,永久代已完全被元空间(Meatspace)所取代。
三、永久代变迁产生的影响
1、测试代码1
public class Test1 {
public static void main(String[] args) {
String s1 = new StringBuilder("漠").append("然").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("漠").append("然").toString();
System.out.println(s2.intern() == s2);
}
}
以上代码,在 JDK6 下执行结果为 false、false,在 JDK7 以上执行结果为 true、false。
首先明确两点: 1、在 Java 中直接使用双引号展示的字符串将会在常量池中直接创建。 2、String 的 intern 方法首先将尝试在常量池中查找该对象,如果找到则直接返回该对象在常量池中的地址;找不到则将该对象放入常量池后再返回其地址。JDK6 常量池在方法区,频繁调用该方法可能造成 OutOfMemoryError。
产生两种结果的原因:
在 JDK6 下 s1、s2 指向的是新创建的对象,该对象将在 Java Heap 中创建,所以 s1、s2 指向的是 Java Heap 中的内存地址;调用 intern 方法后将尝试在常量池中查找该对象,没找到后将其放入常量池并返回,所以此时 s1/s2.intern() 指向的是常量池中的地址,JDK6常量池在方法区,与堆隔离,;所以 s1.intern()==s1 返回false。
2、测试代码2
public class Test2 {
public static void main(String[] args) {
/**
* 首先设置 持久代最大和最小内存占用(限定为10M)
* VM args: -XX:PermSize=10M -XX:MaxPremSize=10M
*/
List<String> list = new ArrayList<String>();
// 无限循环 使用 list 对其引用保证 不被GC intern 方法保证其加入到常量池中
int i = 0;
while (true) {
// 此处永久执行,最多就是将整个 int 范围转化成字符串并放入常量池
list.add(String.valueOf(i++).intern());
}
}
}
以上代码在 JDK6 下会出现 Perm 内存溢出,JDK7 or high 则没问题。
原因分析:
JDK6 常量池存在方法区,设置了持久代大小后,不断while循环必将撑满 Perm 导致内存溢出;JDK7 常量池被移动到 Native Heap(Java Heap),所以即使设置了持久代大小,也不会对常量池产生影响;不断while循环在当前的代码中,所有int的字符串相加还不至于撑满 Heap 区,所以不会出现异常。
JVM内存结构 VS Java内存模型 VS Java对象模型
Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文我们要讨论的JVM内存结构、Java内存模型和Java对象模型,这就是三个截然不同的概念,但是很多人容易弄混。
可以这样说,很多高级开发甚至都搞不不清楚JVM内存结构、Java内存模型和Java对象模型这三者的概念及其间的区别。甚至我见过有些面试官自己也搞的不是太清楚。不信的话,你去网上搜索Java内存模型,还会有很多文章的内容其实介绍的是JVM内存结构。
首先,这三个概念是完全不同的三个概念。本文主要对这三个概念加以区分以及简单介绍。其中每一个知识点都可以单独写一篇文章,本文并不会深入介绍,感兴趣的朋友可以加入我的知识星球和球友们共同学习。
JVM内存结构
我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。其中有些区域随着虚拟机进程的启动而存在,而有些区域则依赖用户线程的启动和结束而建立和销毁。在《Java虚拟机规范(Java SE 8)》中描述了JVM运行时内存区域结构如下:
各个区域的功能不是本文重点,就不在这里详细介绍了。这里简单提几个需要特别注意的点:
1、以上是Java虚拟机规范,不同的虚拟机实现会各有不同,但是一般会遵守规范。
2、规范中定义的方法区,只是一种概念上的区域,并说明了其应该具有什么功能。但是并没有规定这个区域到底应该处于何处。所以,对于不同的虚拟机实现来说,是由一定的自由度的。
3、不同版本的方法区所处位置不同,上图中划分的是逻辑区域,并不是绝对意义上的物理区域。因为某些版本的JDK中方法区其实是在堆中实现的。
4、运行时常量池用于存放编译期生成的各种字面量和符号应用。但是,Java语言并不要求常量只有在编译期才能产生。比如在运行期,String.intern也会把新的常量放入池中。
5、除了以上介绍的JVM运行时内存外,还有一块内存区域可供使用,那就是直接内存。Java虚拟机规范并没有定义这块内存区域,所以他并不由JVM管理,是利用本地方法库直接在堆外申请的内存区域。
6、堆和栈的数据划分也不是绝对的,如HotSpot的JIT会针对对象分配做相应的优化。
如上,做个总结,JVM内存结构,由Java虚拟机规范定义。描述的是Java程序执行过程中,由JVM管理的不同数据区域。各个区域有其特定的功能。
Java内存模型
Java内存模型看上去和Java内存结构(JVM内存结构)差不多,很多人会误以为两者是一回事儿,这也就导致面试过程中经常答非所为。
在前面的关于JVM的内存结构的图中,我们可以看到,其中Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可能可以操作保存在堆或者方法区中的同一个数据。这也就是我们常说的“Java的线程间通过共享内存进行通信”。
Java内存模型是根据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是真实存在的。他只是一个抽象的概念。JSR-133: Java Memory Model and Thread Specification中描述了,JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。
那么,简单总结下,Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。
在Java中,JMM是一个非常重要的概念,正是由于有了JMM,Java的并发编程才能避免很多问题。这里就不对Java内存模型做更加详细的介绍了,想了解更多的朋友可以参考《Java并发编程的艺术》。
Java对象模型
Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。
HotSpot虚拟机中,设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass
,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc
对象,这个对象中包含了对象头以及实例数据。
这就是一个简单的Java对象的OOP-Klass模型,即Java对象模型。
总结
我们再来区分下JVM内存结构、 Java内存模型 以及 Java对象模型 三个概念。
JVM内存结构,和Java虚拟机的运行时区域有关。 Java内存模型,和Java的并发编程有关。 Java对象模型,和Java对象在虚拟机中的表现形式有关。
转载:https://blog.csdn.net/Walker_zmc/article/details/101915918