JVM 第一篇

1.运行时数据区域


1.1程序计数器

    程序计数器是每一个线程都拥有的,里面保存的是当前线程执行的字节码的行号指示器。简单的说就是当前线程执行到代码的哪一行。在多线程的环境中,线程是轮流使用CPU的,所以线程需要一个保存当前执行的状态,当再次拥有CPU时,当保存状态往下执行。

    如果线程正在执行java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的Native(本地)方法,那么计数器内为空。程序计数器是JVM中唯一一个没有OOM的区域。

1.2虚拟机栈

线程在执行每一个方法的时候都会创建一个栈帧,栈帧中主要保存3类数据:

   局部变量表:输入参数和输出参数以及方法中的变量。

   操作数栈(栈操作):主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,先进后出。保存的方式和局部变量表中是一样的。

    栈帧数据:里面保存着访问常量池的指针,异常处理表等等。

还有一些其他的数据,如:动态链接,方法出口等。每一个方法的开始和结束都对应着一个栈帧的入栈和出栈的过程。先进后出。

    局部变量表存放了编译器可知的各种基本数据类型,对象的引用和对象的类型(指向了一条字节码指令的地址)。其中64位的Long和double类型的数据会占用两个局部变量空间(Slot),其他的占用一个。局部变量表所需的内存空间是在编译期完成的,也就是说当进入方法的时候,这个方法需要在帧中分配多大的局部变量空间是完全确认的,在运行期间是不会改变的。

    虚拟机栈会有两种抛出异常的情况,如果线程请求的栈深度大于虚拟机允许的深度,将抛出Stack Overflow Error异常;当时用可扩展的虚拟机栈时,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

1.3 本地方法栈

    本地方法栈和 虚拟机栈类似,只不过虚拟机栈是用来执行java方法(也就是字节码),而本地方法栈它是用来表示执行本地方法的,本地方法栈存放的方法调用本地方法接口,最终调用本地方法库,实现与操作系统、硬件交互的目的。和虚拟机栈一样也会抛出Stack Overflow Error异常和OutOfMemoryError异常。

1.4 java堆

    java堆是所有线程共享的一块区域,也是java虚拟机中所管理的内存中最大的一个区域。在之前几乎所有的对象实例都在这里分配内存,但是随着JIT编译器的发展和逃逸技术的成熟,栈上分配,标量替换优化技术,导致不再是几乎所有对象实例都需要在堆中分配内存了。

逃逸分析:

    逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它有可能被外部的方法引用,例如作为参数传递到其他的方法中,称为方法逃逸。甚至还可以被外界的线程访问到,例如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或者线程无法通过任何途径访问这个对象,那么可以为这个对象做一些高效的优化。

栈上分配:为这个对象在栈中分配内存,这样就避免了再堆中分配内存,那么大量的对象就会随着方法的结束而自动,减少了GC的压力。

同步消除:因为这个对象是无法被别的线程所访问到的,那么这个对象的读写就没有线程竞争,那么就不在需要对这个变量实施同步措施。增加了线程的效率。

标量替换:标量是指一个数据无法在分解为更小的数据,如基本类型和reference类型。如果一个数据可以分解为更小的数据,那么就称为聚合量,例如:对象。如果逃逸分析证明这个对象不会被外界访问,那么可以不真正的创建这个对象,而是直接创建这个对象的若干个被这个方法使用到的成员变量来代替。将这个对象拆分后,除了可以让这个成员变量在栈上(栈上储存的数据,很大的概率会被虚拟机分配至物理机器的高速寄存器中储存)分配和读写外,还可以为后续进一步做优化做准备。

    但是要完全准确地判断一个对象是否为逃逸对象,需要对数据进行一系列复杂的操作,这是一个相对耗时的过程,而且如果分析完成后发现并没有几个逃逸对象,那么付出的代价就比较高了。如果有需要可以手动打开。

    java堆是垃圾收集器管理的主要区域,也称为GC堆。java堆可以处于物理上不连续的内存空间,只要逻辑上连续的就可以了。在实际中,可以实现为固定大小的,也可以实现为动态大小的,现在一般是动态大小的。如果堆中没有内存完成实例分配,并且无法在扩展的时候就会抛出OutOfMemoryError异常。

堆内存的设置:


1.5 方法区

    方法区是共享区,用于存储已被虚拟机加载的类信息,常量,静态变量,即使编译器后的代码等数据。当方法区无法满足内存分配需求的时候就会抛出OutOfMemoryError异常。

1.6 运行时常量池

    运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号的引用,这部分内容在类加载后进入方法区的运行时常量池中存放。当常量池无法申请到内存时就会抛出OutOfMemoryError异常。

1.7 直接内存

    直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但是这部分也被频繁地使用,所以也会抛出OutOfMemoryError异常。

    在java1.4中通过NIO,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在家吧堆中DirectByteBuffer对象作为这块内存的引用进行操作。这样能在场景中显著提高性能,因为避免了在java堆和Native堆中来回的切换数据。

    本机直接内存的分配不会影响到java堆的大小,但是还是会占用物理内存大小,所以在使用堆外内存的时候,要注意不要让各个储存区域总和大于物理内存限制,否则会抛出OutOfMemoryError异常。

2.对象的创建

对象的创建首先是类的加载,类加载的有几个阶段。加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

      加载 -> 验证 -> 准备 -> 初始化 -> 卸载  这5个阶段的顺序在某种意义是来说是一定的。解析则不一定,解析可能会在初始化的时候开始。

    当碰见new指令的时候,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,如果没有则抛出ClassNotFoundException,然后检查这个符号引用是否已经被加载、解析、初始化,如果没有则先进行相应的操作。

    在检查通过后则为新对象分配内存,对象所需的内存大小在类加载完成后是可以完全确认的(根据方法区中该类的信息确定)。在分配内存的时候如果内存空间是绝对规整的,则一般使用“指针碰撞",通过移动指针的位置为对象分配内存,但是这样会有并发问题,所以在使用指针分配的时候会先为每一个线程分配一个默认大小的内存空间,称为线程缓冲(TLAB),用于存在线程的对象。当线程的默认内存使用完时,就会进入同步锁定状态(将指针进行锁定),分配新的内存空间。通过-XX:+/-UseTLAB来设置是否使用TLAB。如果内存空间是不规整的则会使用“空闲列表”的分配方式,为对象分配内存。内存是否规整取决于所采用的垃圾收集器是否带有压缩整理功能决定。

    当内存内存完成后,会先将对象的实例字段初始化为零值,保证对象的实例字段在代码中可以不赋初始化的值直接使用。在对对象的对象头进行设置,设置对象是哪个类的实例、如何找到类的元数据信息、对象的hash码、对象的GC分代年龄等信息,如果对象是一个数组对象,则还需要设置对象的数组的长度。最后进行对象的赋值初始化。

努力吧,皮卡丘





猜你喜欢

转载自blog.csdn.net/yidan7063/article/details/79625004