运行时数据区域
JVM再执行Java程序的过程中会把它锁管理的内存划分为若干个不同的数据区域。
这些区域都有自己的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而创建和销毁。
程序计数器:
1·它是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在JVM中字节码解释器的工作就是改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖这个计数器来完成。
2·在JVM的多线程机制中,为了线程切换后能恢复到正确的执行位置,每一条线程都需要有一个独立的程序计数器,每条线程的程序计数器互不影响,独立存储,是线程私有的内存。
3·如果线程正在执行一个Java方法,那么这个计数器记录的就是正在执行的虚拟机字节码指令地址,如果正在执行的是Native方法,这个计数器则为空。
4·这个内存区域是唯一一个在虚拟机规范中没有规定任何OutOfMemoryError的区域。
Java虚拟机栈:
1·与程序计数器一样,虚拟机栈也是线程私有的,它的生命周期与线程相同。
2·虚拟机栈描述的是Java方法执行的内存模型,每个方法执行的时候都会创建一个帧栈(Stack Frame)用于存放局部变量表、操作数栈、动态链接、方法出口等信息。(每一个方法从调用直至执行完成的过程,就对应着一个帧栈在虚拟机栈中入栈到出栈的过程)。
3·大多数人把Java内存分为堆和栈,但是其实他们口中的栈指的是局部变量表。
4·局部变量表存放了在编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型。
5·如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈:
1·它与虚拟机栈的区别:虚拟机栈为虚拟机执行Java方法服务,而本地方法则为虚拟机使用到的Natice方法服务。
2·和虚拟机栈一样,本地方法栈也会抛出StackOverfloError和OutOfMemoryError异常。
Java堆:
1·Java堆是被所有线程共享的一块区域,在虚拟机启动时创建
2·该内存区域唯一的作用就是存放对象的实例,几乎所有的对象实例都在这里分配内存,在JVM的规范中是这么描述的:所有对象的实例以及数组都要在堆上分配。但是,随着JIT编译器、逃逸分析、栈上分配、标量替换优化技术的发展,上述的JVM规范也就没有那么绝对了。
拓展:
JIT编译器:即时编译器,一句一句边运行边翻译执行
逃逸分析:逃逸:就是当我们创建出来一个对象时,该对象除了在本线程上使用,还要再别的线程上使用,那我们就不考虑使用栈上分配了
栈上分配:对于那些线程私有的对象,将他们打散分配再栈上,而不是在堆上。优点:函数在调用结束之后可以自己销毁,不需要GC的介入,提高性能。
标量替换:如果把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换,那么程序真正在执行的时候,可能不创建这个对象,直接创建它的若干个被这个方法使用到的成员变量。
1·Java堆是垃圾收集器管理的主要区域
2·Java堆不需要连续的内存、可以选择固定大小或者可扩展大小
3·所有新生成的对象都是放在年轻代。年轻代的目的就是尽可能快速的收集掉那些生命周期短的对象。
4·堆又可以分为新生代和老年代:新生代用于存放刚创建的对象已经年轻的对象,如果对象一直没有被回收,生存的足够长久就会被移入老年代。
5·新生代又进一步可以细分为eden,survivorSpace0(s0, from space)、survivorSpace1(s1, to space)。
方法区:
1·方法区是被所有线程都共享的一个区域
2·它用于存放虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
3·它和堆一样不需要连续的内存、可以选择固定大小或者可扩展大小
运行时常量池:
1·运行时常量池是方法区的一部分
2·Class文件中除了有类的版本、字段、方法、接口等描述信息之外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用–>Class的常量池
3·运行时常量池相比于Class文件常量池的另外一个重要特性是具备动态性,Java语言不要求常量一定只有在编译的时候才能产生,也就是说并非预置入Class文件中的常量池的内容才能才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中
直接内存:
·并非JVM运行时数据区的一部分,但是这部分内存频繁被使用
虚拟机对象:
对象的创建:
对象的创建过程图解:
在上图中的在堆区分配内存的时候一般会有两种方案:
1. 指针碰撞:首先Java堆中的内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器,那么锁分配的内存就是把哪个指针向空闲空间那边挪一段与对象大小等 大的距离。
2. 空闲列表:Java堆不是规整的,已使用的内存和空闲的内存相互交错,虚拟机维护着一个列表,记录哪些内存块是可用的,在分配的时候就从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
选择哪种分配方式是由Java堆是否规整决定的,而Java堆是否规整又是由所采用的垃圾收集器是否带有压缩整理功能决定的。所以,在使用 Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而 使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
由于对象的创建在虚拟机中式非常频繁的行为,所以在并发的情况下也并不是线程安全的,为了解决这个问题,由两种方案:
1. 对分配内存空间的动作进行同步处理—实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
2. 把内存分配的动作按照线程划分规划在不同的空间之中进行内存分配完成之后,虚拟机将分配到的内存空间都初始化为零值,这就是我们为什么在一个类中定义全局变量的时候可以不给它赋初值就直接使用。
在内存分配完成且内存初始化完成后,从虚拟机的角度看,一个新的对象已经产生,但是,从Java的角度看,对象的创建才刚刚开始,在执行完new指令之后,一般会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算创建完成。
对象的内存布局:
1·对象在内存中的布局可以分为3个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
2·对象头包括两部分信息,第一部分用于存储对象自身运行时数据,比如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
3·对象头的另外一部分信息是类型指针,对象指向它元数据类型的指针,虚拟机通过这个来确定这个对象是哪个类的实例。
对象访问定位:
1·通过栈上的本地变量表中的reference数据来操作堆上的具体对象。
2·reference数据类型只是规定了一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问堆中的对象具体位置,对象访问方式噎死取决于虚拟机实现而定的。
3·目前主流的两种访问方式有:1.句柄访问 2.指针访问
拓展:
两种访问方式的区别:
1.句柄访问时直接在堆内存中维护着两个指针,一个执行指向对象实例数据,另外一个指向对象类型数据
2.而指针访问呢?在堆内存的对象实例数据中维护着一个指针,指向对象类型数据
两种定位方式的优点和缺点:
使用句柄最大的好处就是reference中存储的时稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而不需要改变reference
使用直接指针的优点时访问的速度更快,它节省了一次指针定位的时间开销。