jvm内存结构和java内存模型

jvm内存结构

  • 堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。垃圾回收器主要管理的对象。
  • 方法区(元空间)(Method Area,MetaSpace):线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。jdk1.8+使用本地内存
  • 虚拟机栈(JVM Stack):线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针。
  • 本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。
  • 程序计数器(Program Counter Register):线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。

字符串常量池在JDK1.8被放入堆中?具体查看> https://www.cnblogs.com/lyalong/p/14440511.html

  • 堆的作用是存放对象实例和数组。从结构上来分,可以分为新生代老年代

  • 而新生代又可以分为Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1)。默认大小8:1:1. 所有新生成的对象首先都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的

  • 控制参数:-Xms设置堆的最小空间大小。-Xmx设置堆的最大空间大小。-XX:NewSize设置新生代最小空间大小。-XX:MaxNewSize设置新生代最小空间大小。

指针碰撞

假设JVM虚拟机上,堆内存都是规整的。堆内存被一个指针一分为二。指针的左边都被塞满了对象,指针的右变是未使用的区域。每一次有新的对象创建,指针就会向右移动一个对象size的距离。这就被称为指针碰撞。
造成的问题:一个线程正在给A对象分配内存,指针还没有来的及修改,同时为B对象分配内存的线程,仍引用这之前的指针指向。这样就出现毛病了。

TLAB

全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。可以解决指针碰撞的问题
创建对象时,需要在堆上申请指定大小的内存,如果同时有大量线程申请内存的话,可以通过锁机制或者指针碰撞的方式确保不会申请到同一块内存,在JVM运行中,内存分配是一个极其频繁的动作,这种方式势必会降低性能。因此,在Hotspot 1.6的实现中引入了TLAB技术。

  • 如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用,这个申请动作还是需要原子操作的。
  • TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,均摊对GC堆(eden区)里共享的分配指针做更新而带来的同步开销。
  • TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。
  • TLAB的缺点
    • 因为TLAB通常很小,所以放不下大对象。
    • TLAB空间还剩一点点没有用到,有点舍不得。(比如100kb的TLAB,装了80KB,又来了个30KB的对象)
      所以JVM开发人员做了以下处理,设置了最大浪费空间。
    • 当剩余的空间小于最大浪费空间,那该TLAB属于的线程在重新向Eden区申请一个TLAB空间,进行对象创建,如果还是空间不够,那这个对象太大了,只能去Eden区直接创建!
    • Eden空间够的时候,你再次申请TLAB没问题,我不够了,Heap的Eden区要开始GC
    • TLAB允许浪费空间,导致Eden区空间不连续,积少成多。以后还要人帮忙打理。

方法区

方法区(Method Area,jdk1.8+ 改名元空间 MetaSpace)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据。
虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。

  • 控制参数:-XX:PermSize 设置最小空间 -XX:MaxPermSize 设置最大空间。这俩最好设置一样,这样就不会因为动态扩容造成内存震荡

方法栈

  • 每个线程会有一个私有的栈。每个线程中方法的调用又会在本栈中创建一个栈帧。
  • 在方法栈中会存放编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身。)
  • 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小

程序计数器

  • 它的作用可以看做是当前线程所执行的字节码的行号指示器。
  • 此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

Java内存模型

Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:

  • 线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
    • 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
    • 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

指令重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序

  • 可以发生在好几个地方:编译器、运行时、JIT等,比如编译器会觉得把一个变量的写操作放在最后会更有效率,编译后,这个指令就在最后了(前提是只要不改变程序的语义,编译器、执行器就可以这样自由的随意优化),一旦编译器对某个变量的写操作进行优化(放到最后),那么在执行之前,另一个线程将不会看到这个执行结果。
  • Java中包含了几个关键字:volatile、final和synchronized,帮助程序员把代码中的并发需求描述给编译器。JMM中定义了它们的行为,确保正确同步的Java代码在所有的处理器架构上都能正确执行。

逃逸分析、栈上分配、标量替换、同步消除

逃逸分析是编译语言中的一种优化分析,而不是一种优化的手段。通过对象的作用范围的分析,为其他优化手段提供分析数据从而进行优化。

  • 逃逸分析包括:

    • 全局变量赋值逃逸
    • 方法返回值逃逸
    • 实例引用发生逃逸
    • 线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量.
  • 标量替换

    • 标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
    • 通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而会将该对象的成员变量分解成若干个变量来代替。这些代替的成员变量在栈帧或寄存器上分配空间。
    • 通过-XX:+EliminateAllocations可以开启标量替换, -XX:+PrintEliminateAllocations查看标量替换情况(Server VM 非Product版本支持)
  • 栈上分配

    • 我们通过JVM内存分配可以知道JAVA中的对象几乎都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问.那就通过标量替换将该对象分解在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力
    • 通过-XX:-DoEscapeAnalysis关闭逃逸分析,JDK1.8是默认开启逃逸分析
  • 同步消除

    • 同步消除是java虚拟机提供的一种优化技术。通过逃逸分析,可以确定一个对象是否会被其他线程进行访问
    • 如果对象没有出现线程逃逸,那该对象的读写就不会存在资源的竞争,不存在资源的竞争,则可以消除对该对象的同步锁
    • 通过-XX:+EliminateLocks可以开启同步消除,进行测试执行的效率.
posted @ 2021-03-11 16:50  rm-rf*  阅读(159)  评论(0编辑  收藏  举报