在JVM规范中,java虚拟机在在运行时用到的存储不同类型数据的区域统称为-Java运行时数据区
在java运行时数据区包括一些部分:
1、程序计数器
2、方法区
3、栈(虚拟机栈、本地方法栈)
4、堆(永久代、老年代、新生代)
如图(灰色部分是线程私有的部分、黄色部分为线程共享的部分)
我们先编写一段简单的代码,在后面能帮助我们理解java虚拟机内存模型
package com.lhh.jvm;
/**
* @author liuhang
*
*/
public class LocalCalTest {
private static final String s = "hehe";
public static void main(String[] args) {
int a = 1;
double s = 1.1;
double d = a + s;
System.out.println(d);
String string = "wocao";
string.intern();
}
}
使用javac编译成class文件,注意要加上-g参数,然后使用javap反编译查看,如下图:
Last modified 2016-8-26; size 824 bytes
MD5 checksum 50ef33295e6eb4b3826622024ca4a040
Compiled from "LocalCalTest.java"
public class com.lhh.jvm.LocalCalTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#34 // java/lang/Object."<init>":()V
#2 = Double 1.1d
#4 = Fieldref #35.#36 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #37.#38 // java/io/PrintStream.println:(D)V
#6 = String #39 // www
#7 = String #40 // wocao
#8 = Methodref #41.#42 // java/lang/String.intern:()Ljava/lang/String;
#9 = Class #43 // com/lhh/jvm/LocalCalTest
#10 = Class #44 // java/lang/Object
#11 = Utf8 s
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 ConstantValue
#14 = String #45 // hehe
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lcom/lhh/jvm/LocalCalTest;
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 args
#25 = Utf8 [Ljava/lang/String;
#26 = Utf8 a
#27 = Utf8 I
#28 = Utf8 D
#29 = Utf8 d
#30 = Utf8 string
#31 = Utf8 string2
#32 = Utf8 SourceFile
#33 = Utf8 LocalCalTest.java
#34 = NameAndType #15:#16 // "<init>":()V
#35 = Class #46 // java/lang/System
#36 = NameAndType #47:#48 // out:Ljava/io/PrintStream;
#37 = Class #49 // java/io/PrintStream
#38 = NameAndType #50:#51 // println:(D)V
#39 = Utf8 www
#40 = Utf8 wocao
#41 = Class #52 // java/lang/String
#42 = NameAndType #53:#54 // intern:()Ljava/lang/String;
#43 = Utf8 com/lhh/jvm/LocalCalTest
#44 = Utf8 java/lang/Object
#45 = Utf8 hehe
#46 = Utf8 java/lang/System
#47 = Utf8 out
#48 = Utf8 Ljava/io/PrintStream;
#49 = Utf8 java/io/PrintStream
#50 = Utf8 println
#51 = Utf8 (D)V
#52 = Utf8 java/lang/String
#53 = Utf8 intern
#54 = Utf8 ()Ljava/lang/String;
{
public com.lhh.jvm.LocalCalTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lhh/jvm/LocalCalTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=8, args_size=1
0: iconst_1
1: istore_1
2: ldc2_w #2 // double 1.1d
5: dstore_2
6: iload_1
7: i2d
8: dload_2
9: dadd
10: dstore 4
12: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
15: dload 4
17: invokevirtual #5 // Method java/io/PrintStream.println:(D)V
20: ldc #6 // String www
22: astore 6
24: ldc #7 // String wocao
26: astore 7
28: aload 6
30: invokevirtual #8 // Method java/lang/String.intern:()Ljava/lang/String;
33: pop
34: return
LineNumberTable:
line 10: 0
line 11: 2
line 12: 6
line 13: 12
line 14: 20
line 15: 24
line 16: 28
line 17: 34
LocalVariableTable:
Start Length Slot Name Signature
0 35 0 args [Ljava/lang/String;
2 33 1 a I
6 29 2 s D
12 23 4 d D
24 11 6 string Ljava/lang/String;
28 7 7 string2 Ljava/lang/String;
}
1、程序计数器
程序计数器是线程私有的区域,每个线程的启动都会给这个线程创建一个程序计数器,当线程执行完后被回收掉,程序计数器记录当前线程执行的字节码文件的行号,通常认为程序计数器是一个指针,他代表着当前线程执行的行号(内存地址),通俗的讲,就是虚拟机执行一段代码时,程序计数器会告诉虚拟机当前执行到第几行代码,当这一行执行执行完成时虚拟机会接着执行下一行代码,如果当前执行的是一个native(java调用其他语言的方法)方法,它的值就为空,它占用的内存较少,也是java数据区中唯一一块没有任何内存溢出异常的数据区。
2、方法区
方法区是线程共享的区域,方法区物理上存在与堆上,准确的说存在与堆上的持久代(只在Hotpot虚拟机有持久代这个概念,在jdk1.8后永久代被移除了,后续会写一个博客专门简述jdk版本变迁对常量池造成的影响)中,但是方法区和堆在逻辑上又是相互独立的。方法区主要存储了java的类信息主要包括:运行式常量池、类加载器、类的常量、静态变量、以及各种方法、字段、构造方法、以及编译产生的classcode(LineNumberTable、LocalVariableTable)等。
2.1、运行时常量池(Constant pool)
首先常量池不是像它的名字那样,只是存储了一些对象内部声明的一些常量,我记得以前学习是对一段代码过疑问
Integer i1=127;后来经过查阅资料发现当Byte,Short,Integer,Long,Character这五种类型的值在-127-127之间时的赋值会直接指向常量池的对象,这对我造成类误解,一直以为java的常量池就是存储了一些特定的对象,后来经过学习才明白,这些只是java常量池的一部分,我们可以抽象出来为对象池,从我们生成的classcode文件中的Constant Pool模块我们可以清晰的看到,常量池还存储了一些java对象的类型信息等。
Integer i2=127;
System.out.println(i1==i2)//输出true
//值大于127时,不会从常量池中取对象
Integer i3=128;
Integer i4=128;
System.out.println(i3==i4)//输出false
随着jdk版本的不同,会对调用string的intern方法产生不用的结果。
2.2、this关键字的初始化
在上述例子的java文件中我们并没有创建LocalCalTest的构造方法,java虚拟机会默认给我们添加这个构造方法,并且在这个构造方法内初始化了一个this对象,并放到了常量池中,这也是为什么this关键字可以代表当前对象。
由于方法区存储是一些类型信息和常量信息,所有方法区的内存回收主要是类型的卸载和常量的回收,但是类型卸载的条件是非常苛刻的,所以该区域的垃圾收集的行为是非常少的。
3、栈
栈是线程私有的区域,栈的生命周期和线程息息相关,它随着线程的启动而创建,随这线程的关闭而回收。JVM中的栈主要有两种类型,一是JVM虚拟机栈,也就是我们平常说的栈内存,另一个是本地方法栈。
3.1、虚拟机栈
java虚拟机栈的主要功能就是描述java方法执行过程中的生命周期,每个方法在执行过程中都会创建一个栈帧,栈帧的作用是支持虚拟机进行方法的调用和方法的执行的数据结构,栈帧中包含局部变量表,操作数栈,动态链接、方法返回地址、异常分派以及其他信息等。一个方法的执行就是栈帧由入栈到出栈的过程,在线程执行过程中,方法的调用链会很长,所以只用处在栈顶的元素才是有效的。
程序比较简单,下面我们结合这段程序来讲解一下栈帧中的各个元素:
3.1.1、局部变量表(LocalVariableTable)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量由若干个Slot组成,一个slot占用一个字节,它通常以索引的形式访问,例如slot为0的代表变量名为args的数组,SIgnature以[开头代表该变量为一个数组,如果是基本类型的数据,会取基本类型的首字母代表,例如I代表整形、D代表浮点型等。
从上图我们可以看到LocalVariableTable中有四个局部变量,name分别为args、a、s、d等,我们可以看到,slot的偏移量分别为0,1,2,4,变量s占用了两个slot,实际上变量d也占用两个slot,只不过后面没有变量了,我们看不到它的偏移量罢了。关于s为什么会占用两个偏移量呢?我们都知道,在java的数据类型中long和double是64位的数据类型,所有double和long会占用两个slot,java虚拟机不允许用户操作这两个slot中的单独一个,所以对于long和double类型的两个slot操作可以看成是一个原子操作。
3.1.2、操作数栈
操作数栈也称为操作栈,和局部变量区一样,它存储的是局部变量及中间操作结果的值,操作数栈也是被组织成一个以字长为单位的数组,long和double的深度为2,操作数栈是按标准栈的形式把数据弹入弹出,例如在我们的示例程序中,在classcode中的Code区域中我们可以看到,istore_1、dload_2、dadd等命令,下面我们做一下解释:
1、istore_1命令。i代表int类型,store代表入栈、_1代表存储一个1哥字节,整合就是存储占用一个字节的int整数
2、dload_2命令,d代表为double类型,load为出栈,_2代表两个字节数据,整合起来就是取出一个占用两个字节的double浮点数
3、dadd命令,d代表结果类型为double ,add命令会取出最接近栈定的两个元素执行相加命令。
操作数栈只能存放基本数据类型的数据,其他类型的数据栈上只保存对象的引用(内存地址),我会在堆那一章讲解一下栈和堆之间的引用。
3.2、本地方法栈
本地方法栈和java虚拟机栈很多地方都是非常相似的,java本地方法栈就是为了java执行动态方法准备的,有一些虚拟机(如Hotspot)等将java虚拟机栈和本地方法栈合并实现
4、在介绍堆之前,我们先简单了解一下java的gc回收算法
4.1、标记-清除 算法 (Mark-Sweep算法),标记清除算法比较简单,在GC时,jvm会标记那些对象是可用的,那些对象是无用的,然后对无用的对象进行标记,并清除掉,标记清楚算法实现比较简单,但是缺点是会产生内存碎片,当我们如果需要分配一个大对象时会因找不到足够的连续区域去容纳它。
4.2、复制算法,该方法需要分配两块相同的内存区域,一块用于使用,一块当作buffer,当执行gc时,jvm会将扫描使用的那块内存,将其中的存活对象放到buffer块中,然后直接对在使用内存进行全部删除操作,这种方法适合存活对象比较少的情况下使用,因此在堆的新生代中是采用这种算法的改进版进行GC,缺点是有一半内存不能使用。
4.3、标记-清楚-整理算法:标记清除整理算法就是比标记清楚算法多了一步整理的动作,它将清理后存活的对象集中到一起,它解决了标记清楚算法产生内存碎片的问题。
4.4、相关JVM参数
-XX:+PrintHeapAtGC GC的heap详情
-XX:+PrintGCDetails GC详情
-XX:+PrintGCTimeStamps 打印GC时间信息
-XX:+PrintTenuringDistribution 打印年龄信息等
-XX:+HandlePromotionFailure 老年代分配担保(true or false)
-XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间
-XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况
-XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数
5、堆
java是全局共享的内存区域,也是内存管理中最大的一块区域,也是javaGC主要的管理区域,几乎所有的java对象都是在此上面分配内存。由于java对象的存活时间不一样,如果整个堆内存都按一种GC回收算法实现,势必会影响GC的效率,正是出于这种问题出发,出现了堆中分代管理的现象。
堆内存的组成:永久代、老年代、持久代(只在HotSpot中有这个概念,且在1.8之后被去掉),线程缓存区
堆内存回收算法:分代回收算法,分代回收算法不是新的算法,它只是针对堆中的对象的存活特点将堆内存分代管理,在不同的代上采取不同的GC收集算法。
5.1、新生代
和名字一样,新生代主要存放的是我们新创建的对象,据Oracle的官方调查,新new出来的对象有98%都是朝生夕死的,所以这个内存区域的存活对象比较少,在存活对象比较少的情况下的垃圾回收适合采用复制算法,但是采用赋值算法会浪费掉一般的内存,由于新生代98%的对象都是朝生夕死的,所有不必要采用1:1的比例来进行分配使用空间和缓存空间。在JVM中,把新生代分配为一块较大的使用空间和两块较小的缓存空间,通常比例为8:1:1,我们可以通过修改启动参数来修改这一比例。jvm回使用使用空间和其中一块缓存空间,当进行GC时会将存活对象复制到另一块缓存空间,GC之后jvm会使用另一卡缓存空间和使用空间。GC之后的存活对象的年龄+1,当达到设置的值时会将该对象移到老年代。
5.2、老年代
主要存放应用程序中生命周期长的内存对象,另一个作用是当新生代的内存不够使用时新创建的对象便在此分配。在此空间上的对象一般是经过几次垃圾回收依然存活的对象,生命力比较顽强,每次GC时挂掉的对象并不过,所以在此空间上垃圾回收我们一般会采用标记算法。
5.3、持久代
用于存放静态文件,例如在JDK1.6之前,方法区一直存在与持久代中,在JDK1.8之后持久代已经不存在,它完全被元空间所代替。
5.4、从栈到堆的过程
例如: Object obj = new Object();
在栈上的局部变量表那一章,我们知道,栈上不报存Object对象,只是保存了该对象的引用,那么我们如何通过这个引用来找到这个对象数据呢,一种是在栈上存储该实例对象的内存地址,我们可以通过这个地址来找到这个示例对象,在这个示例对象的对象头中有一个指针,这个指针指向方法区中这个对象所属的类型信息。另一种方法是栈上不保存这个示例对象的内存地址,而是保留一个对象句柄,这个句柄不仅有该实例对象的内存地址,还有指向方法区的对象类型信息。这两种访问方式各有优势,当使用句柄是,我们移动对象(参考GC回收时对象会来会移动,每次GC后,对象的引用地址都会改变)时只需要修改该对象的句柄中实例数据的地址就行,而句柄对象的地址并没有改变,当使用第二种时,由于存储的是示例对象的地址,我们可以直接通过该地址找到该对象,可以节省我们的资源开销,在HotSpot虚拟机中就是采用第一种类型。
5.5、相关设置参数
-Xms :初始堆大小
-Xmx :最大堆大小
-XX:NewSize=n :设置年轻代大小
-XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n :设置持久代大小
-xx:maxtenuringThreshold:新生代年龄,当新生代中的对象经过GC的次数达到这个值时会放到老年代