目录
2.2.1.对象头(Header):用于存储对象的元数据信息
2.2.2.实例数据(Instance Data):存储真正有效数据
2.2.3.对齐填充(Padding):仅仅起到占位符的作用
1.JVM运行时数据区
1.0.JVM运行过程
Java 源文件通过编译器编译后,能产生相应的 .Class 文件,也就是字节码文件。而字节码文件通过 Java 虚拟机中的解释器,编译成特定机器上的机器码。
1.0.1.跨平台
不同的平台都有JVM的版本,一个 Java 源文件被编译成字节码文件,被不同平台的 JVM 翻译成特定平台下的机器码从而运行。
见下图:
1.0.2.Java虚拟机组成
1.1.程序计数器(线程私有)
1.1.1.是什么?
1)程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器;
2)线程是一个独立的执行单元,是由CPU控制执行的;
3)字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
注意:为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们称这类内存区域为“线程私有”的内存。
1.1.2.特点
内存区域中唯一没有规定任何OutOfMemoryError情况的区域。
1.2.栈(Stack)(线程私有)
1.2.1.Java虚拟机栈
是什么?
每个方法在执行的同时会创建一个栈帧(Stack Framel)用于存储局部变量表、操作数栈(就是对局部变量的操作,如加减乘除)、动态链接(new 的对象)、方法出口(return 的东西)等信息。每个方法从调用直至执行完成的过程,就是对应一个栈帧在虚拟机中入栈到出栈的过程。
特点
1)局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)以及对象引用(reference类型)。
2)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
1.2.2.本地方法栈
是什么?
与java虚拟机栈相同,每个方法在执行的同时会创建一个栈帧(Stack Framel)用于存储局部变量表、操作数栈(就是对局部变量的操作,如加减乘除)、动态链接(new 的对象)、方法出口(return 的东西)等信息。每个方法从调用直至执行完成的过程,就是对应一个栈帧在虚拟机中入栈到出栈的过程。
特点:
1)用于作用于本地方法执行的一块java内存区域。
2)加了native的方法即为本地方法。
1.3.堆(Heap)(线程共享)
是Java内存区域中一块用来存放对象实例(私有的属性)的区域,几乎所有的对象实例都在这里分配内存。
特点:
1)此内存区域的唯一目的就是存放对象实例。
2)Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域。
3)Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。
4)Java堆可以分为:新生代和老年代;新生代分为:To Space、From Space、Eden。
1.4.方法区(线程共享,JD8之后已经被元空间取代)
是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(类版本号、方法、接口)、常量、静态变量、即时编译器编译后的代码等数据。
特点
1)内存中存放类信息、静态变量等数据,属于线程共享的一块区域。
2)HotSpot使用永久代来实现方法区。
3)方法区是一片连续的堆空间,通过-XX:MaxPermSize来设定永久代最大可分配空间,当JVM加载的类信息容量超过了这个值,会报OOM:PermGen错误。
4)永久代的GC是和老年代(old generation)捆绑在一起的,无论谁满了,都会触发永久代和老年代的垃圾收集。
5)JDK1.7开始了方法区的部分移除:符号引用(Symbols)移至native heap,字面量(interned strings)和静态变量(class statics)移至java heap。
为什么要用Metaspace替代方法区
随着动态类加载的情况越来越多,这块内存变得不太可控,如果设置小了,系统运行过程中就容易出现内存溢出,设置大了又浪费内存。
注意:
1)并非数据进入了方法区就如永久代的名字一样“永久”存在了,这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
2)当它无法内存分配需求时,方法区会抛出OutOfMemoryError。
1.4.1.运行时常量池
运行时常量池是方法区的一部分,class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池。用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
特点:运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池再申请到内存时会抛出OutOfMemoryError异常。
1.5.元空间(Metaspace)
Metaspace由两大部分组成:Klass Metaspace和 NoKlass Metaspace。
1.5.1.Klass Metaspace
1)Klass Metaspace就是用来存klass的,就是class文件在jvm里的运行时数据结构(不过我们看到的类似A.class其实是存在heap里的,是java.lang.Class的对象实例)。
2)这部分默认放在Compressed Class Pointer Space中,是一块连续的内存区域,紧接着Heap和之前的perm一样;通过-XX:CompressedClassSpaceSize来控制这块内存的大小,默认是1G。
下图展示了对象的存储模型,_mark是对象的Mark Word,_klass是元数据指针。
3)Compressed Class Pointer Space不是必须有的,如果设置了-XX:-UseCompressedClassPointers,或者-Xmx设置大于32G,就不会有这块内存,这种情况下klass都会存在NoKlass Metespace里。
1.5.2.NoKlass Metaspace
1)NoKlass Metaspace专门来存klass相关的其他的内容,比如method,constantPool等,可以由多块不连续的内存组成。
2)这块内存是必须的,虽然叫做NoKlass Metaspace,但是也其实可以存klass的内容,上面已经提到了对应场景。
3)NoKlass Metaspace在本地内存中分配。
1.5.3.优点
1)运行时常量池和静态变量都存储到堆中,Metaspace存储类的元数据,MetaSpace直接申请在本地内存(Native memory),这样类的元数据分配只受本地内存带下的限制,OOM问题就不存在了。
2)JDK8 HotSpot JVM 移除永久区,使用本地内存来存储元数据信息,并称之为:元空间(Metaspace)。这以为着不会再有java.lang.OutOfMemoryError:Perm Gen问题,也不需要在进行调优及监控内存空间的使用。
1.5.4.元空间与永久代区别
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize
初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。-XX:MaxMetaspaceSize
最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio
在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集-XX:MaxMetaspaceFreeRatio
在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
1.5.5.总结
1)Metaspace 容量:默认情况下,类元数据只受可用的本地内存限制(容量取决于是32位或是64位操作系统的可用虚拟内存大小)。新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整。
2)之前不管是不是需要,JVM都会吃掉那块空间……如果设置得太小,JVM会死掉;如果设置得太大,这块内存就被JVM浪费了。理论上说,现在你完全可以不关注这个,因为JVM会在运行时自动调校为“合适的大小”;
3)提高Full GC的性能,在Full GC期间,Metadata到Metadata pointers之间不需要扫描了,别小看这几纳秒时间;
4)隐患就是如果程序存在内存泄露,像OOMTest那样,不停的扩展metaspace的空间,会导致机器的内存不足,所以还是要有必要的调试和监控。
1.6.总结
1)栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。
2)对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中可能有多份。
2.Java对象
2.1.java对象创建流程
如上图:
1)虚拟机遇到一条new指令时,首先检查这个对应的类是否在常量池中定位到一个的符号引用。
2)判断这个类是否已被加载、解析和初始化。
3)为这个新生对象在Java堆中分配内存空间,其中Java堆分配内存空间的方式主要有两种(指针碰撞、空闲列表):
a.指针碰撞:
· 分配内存空间包括开辟一块内存和移动指针两个步骤。
· 非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
b.空闲列表
· 分配内存空间包括开辟一块内存和修改空闲列表两个步骤。
· 非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
4)将分配到的内存空间都初始化为初始值。
5)设置对象头相关数据:1、GC分代年龄;2、对象的哈希码 hashCode;3、元数据信息。
6)执行对象init方法。
2.2.Java对象内存布局
2.2.1.对象头(Header):用于存储对象的元数据信息
对象运行时数据(Mark Word):部分数据的长度在32位和64位迅疾(未开启压缩指针)中分贝为32bit和64bit,存储对象自身的运行时数据,如:哈希值、GC分代年龄、锁状态标识、线程持有的锁、偏向锁ID、偏向锁时间戳等。
Mark Word一般被设计为非固定的数据接口,以便于存储更多的数据信息和服用自己的存储空间。
类型指针:只想它的类元数据的指针,用于判断对象数据那个类的实例。
2.2.2.实例数据(Instance Data):存储真正有效数据
如各种字段内容,各字段的分配策略为longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类定义的变量的前面。
2.2.3.对齐填充(Padding):仅仅起到占位符的作用
2.3.对象访问定位方式
当我们在对上创建一个对象实例后,就要通过虚拟机栈中的reference类型来操作堆上的对象。现在主流的访问方式有两种(HotSpot就是采用的第二种)
2.3.1.句柄访问对象
即reference中存储的是对象句柄的地址,而句柄中包含了对象实例数据与类型数据的具体位置,相当于二级指针。
2.3.2.直接指针访问对象
即reference中存储的就是对象地址,相当于一级指针。
2.3.3.对比
2.3.3.1.垃圾回收分析
1)句柄访问当垃圾回收移动对象时,reference中存储的地址是稳定的地址,不需要修改,仅仅需要修改对象句柄的地址;
2)指针访问垃圾回收时,需要修改reference中存储的地址。
2.3.3.2.访问效率分析
指针访问由于句柄访问,因为指针访问只能进行一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。