JVM 进一步了解(1)

什么是JVM

定义:Java Vitrtual Machine(JVM),java运行时环境

好处

  • 一次coding,到处运行
  • 可以自动管理内存
  • 实现方法的多态

劣势

  • 相较于编译型语言来说,性能差
  • 相较于python,js等解释型语言来说,语法较为固定,没有高度自由。当然这种固化的格式也是java适合于大型的项目的原因之一,使其具有很好的代码质量控制能力;
  • 虽然可以自动管理内存,但大型项目的诸多问题都是发生在内存回收阶段。目前在大部分特殊业务场景中,GC部分都需要进行针对性优化。

JVM的构成

JVM Structure

我们先从JVM的内存结构开始分析,然后分析他的execution engine中的GC部分,最后分析其Class Loader部分。

JVM的内存结构

  1. PC register
  2. JVM stack
  3. Heap
  4. Method Area
  5. Native Method Stacks

PC register(程序计数器)

程序计数器,是指向下一条执行命令的指针。它是线程私有的,也就是说一个thread独享一个pc register。在物理上,pc就是一个寄存器。

Tips:pc register是java中唯一一个不需要考虑越界问题的结构。

JVM Language Stack

这和操作系统中运行一个程序的stack很像,也是一个方法作为一个frame。顾名思义,如果在方法内调用另一个方法,就push一个frame;方法结束调用,则返回并pop这个frame。其中需要注意的是,stack大小有限制。特别是在做递归的时候,没有控制好下界,导致方法一直无法回归,那么就会出现stack overflow的问题。
同样的单个frame过大也会导致stack不够大出现越界问题。

stacks

需要注意的是stackpc一样也是线程私有的.也就是说方法内的local variables,不会被其他线程访问到。(除了new的对象,其余基本类型以及这些对象的引用,都是作为local variables保存在本地栈帧中)

Heap

我们在创建一个对象的时候,会使用一个new操作,他会调用对应类的构造函数,将其中的local variable存放在heap中,并且我们会在方法frame中有一个引用来指向heap中对象对应的位置。也就是说一般而言,new对象的时候,内存中会在heap中创建一个真的object,并且在JVM stack中方法frame里面有一个对象的引用。

一般而言Heap内存是由JVM自己控制的,这部分设计GC的功能,将在另一篇文章讲解。
Heap不是线程私有的,由多个线程来共享一个heap。其中,特别是在多线程开发中,对object资源的占有,释放,以及为了占有、释放而发明的锁结构,是非常有趣的话题。这部分也会在接下来的blog中提及。

Method Area

方法区,方法区和heap一样是由多个线程共享的区域,其主要存放类信息常量静态变量以及java即使编译的临时code缓存。其中有一部分为runtime constant pool(运行时常量池),类文件中除了有类的版本,字段,方法,接口等描述文件外,还会有一个constant pool来保存一些编译期产生的字面量和符号引用,这部分将在类加载后被存储到方法区内的运行时常量池中。

方法区在JKD8(HotSpot)以前,常常会被认作是永久代。或者人们将永久代和方法区混为一谈,其实这两者在概念上都不是等价的。仅仅是因为当时hotspot团队将gc收集的设计拓展到方法区,换句话说是使用永久代来实现方法区而已。

直接内存

直接内存不属于java虚拟机内存结构的一部分,但是这部分内存在现代的代码设计中会被频繁的使用。同样地,也会导致outofmemory的问题。
直接内存是指java虚拟机调用系统函数导致的堆外内存,例如niojava虚拟机无需进行二次拷贝,可直接引用堆外内存的数据。java虚拟机性能得到极大的提升,这是直接内存的一大优点。同样地,direct memory也会有内存溢出的问题。

对象的create过程

对象create操作需要check对应的类是否已经加载到method area中。如果还没有,则先执行类加载操作(具体类加载的过程将在以后的深入blog中讲解)。在加载好类对象之后,需要在heap中申请内存,此时申请的内存大小由类对象提供,然后对obejct header进行一些必要的配置,之后执行其中的构造函数。其中我们先分析其中的heap申请内存操作和分析对象在内存中的布局。

heap分配内存

一般而言,在一个具有足够大连续内存空间的heap上,分配内存是很容易的。只需要heap上的指针移动object大小的值,将其中的空间分配给对象即可。但是总会遇到一些意外的情况,如空间碎片化,没有足够大的连续内存分配,并且jvm并不提供内存整理功能时。这种情况直接抛出out of memory error即可。但是由于多线程并发申请内存,使一个线程申请heap内存时,heap中的指针还没有就位,另一个线程又申请内存,导致前一个线程内存分配失败的情况。目前有两种方法来解决这个问题。

  • 方法一,是分配内存操作进行同步处理,使其具有原子性,也就是说(类似串行化处理的方式),通过CAS和失败重试的方法来保证其原子性;
  • 方法二,通过给每一个线程在heap中预分配一块供其使用的空间来避免多个线程空间分配时的抢占情况,称为本地线程分配缓冲(Thread Local Allocation Buffer即TLAB)。

object在内存中的layout

在具体说明布局之前,先考虑这样一个问题,在一个32bit的机器上,一个int值占有多大的空间?一个Integer对象占有多大的空间?

答案可能出乎你的预料,一个int占有4个byte的栈空间,而一个简单的Integer对象居然要占用4个byte大小的空间。如图:

object layout

一般而言,一个对象的内存layout主要包括三个部分:headerinstance datapadding。而Header部分一般又包含三个部分,一个指向类对象的指针,一个lock于一个flagword。

//todo

Reference

(Memory Layout of Objects in Java)[https://www.baeldung.com/java-memory-layout]


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!