JVM(复习笔记一)

JVM

JVM可分成五个主要部分:
1、类加载器:加载字节码文件到内存
2、运行时数据区:JVM核心内存空间结构模型
3、执行引擎:对JVM指令进行解析,翻译为机器码,解析完成后提交到操作系统
4、本地库接口:供java调用的融合了不同开发语言的原生库
5、本地方法库:java本地方法的具体实现

其中JVM的内存根据线程的占用方式主要分为两部分:
一、线程独占区:每一个线程在创建的同时,JVM会为其分配一块内存区域,用于存储该线程的数据,主要包括栈(虚拟机栈、本地方法栈)和程序计数器。
二、线程共享区:该区域是对所有现场共享的区域,用存储加载的类信息和对象数据,主要分为堆和方法区。

①、程序计数器:
  在多线程下CPU的处理机制属于轮流切片机制,由CPU分配每个线程的切片时间,在任意时刻,一个处理器只会执行一个线程中的一条指令,当切片时间结束,CPU会保存线程,记录线程状态,和指令的执行进度。所以,为了保证线程切换后可以恢复到正确的位置,所以每一个线程都有一个独立的程序计数器,使得进程之间的程序计数器互不影响,所以程序计数器位于线程独占区域。
  当线程正在执行一个Java方法时,程序计数器记录的是虚拟机正在执行的指令地址。如果正在执行的Native方法时,计数器的内容为空。
  程序计数器是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError的区域。
②、虚拟机栈:
  Java虚拟机栈与程序计数器相似,同属于线程私有区。
  虚拟机栈描述的是Java方法执行期间的内存模型。当一个方法被一个线程调用时,该线程就会向自己的栈中压入一个栈帧,方法结束时,栈帧会被弹出栈。栈帧主要用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  局部变量表存放了编译器可知的各种基本数据类型和引用类型。根据虚拟机栈的类型栈的深度可能会有不同的限制,当栈中栈帧数超过规定的长度后,会抛出StackOverflowError异常。当前大部分的虚拟机栈允许动态扩展,当动态扩展时申请不到足够内存时会抛出OutOfMemoryError异常
③、本地方法栈:
  为虚拟机使用到的native方法服务,虚拟机规范中对本地方法栈种方法使用的语言,使用方式和数据结构没有强制规定。
④、堆:
  java堆是虚拟机管理的最大一块内存,同时也是对所有线程共享的内存区域,几乎所有的对象实例都分配在这一块区域,因此这一区域也是垃圾收集线程的主要目标,为了提高垃圾收集的效率,根据GC的回收算法将Java对分成几个区域,包括:Eden区域,From Survivor区域,To Survivor区域,老年代区域等几个主要区域。
  Eden区:对象刚被创建的时候,存放在 Eden 区,如果 Eden 区放不下,则放在 Survivor 区,甚至老年代中。 Survivor 区:Survivor 又可分为 Survivor From 和 Survivor To,GC 回收时使用,将 Eden 中存活的对象存入 Survior From 中,下一次回收时,将 Survior From 中的对象存入 Survior To 中,清除 Survior From ,下一次回收时重复次步骤,Survior From 变成 Survior To,Survivor To 变成 Survivor From,依次循环,同时每次回收,对象的年龄都 +1,年龄达到了 阈值(默认是15) 的对象,移动到老年代中。
  堆空间分为年轻代和老年代,通常比例是3:7或者2:8,而年轻代又分为Eden区和幸存代,一般比例是s0:s1:Eden=1:1:8
  Ps: 从内存分配的角度看,线程共享的java堆可能划分出多个线程私有的分配缓冲区TLAB,无论如何划分,都与存放的内容无关,无论哪个区域,存储的仍是对象实例,进一步划分的目的是为了更好的回收内存或者更快的分配内存。依据java虚拟机规范,java堆可以处理于物理上不连续的内存空间,只要逻辑上是连续的即可,就像我们的磁盘空间一样,在实现时,即可实现成固定大小,也可以通过(-Xms,-Xmx)来控制。
⑤、方法区:
  方法区与堆相似,同样是线程共享区,主要用于存储虚拟机加载的类信息、常亮、静态变量等数据。由于HotSpot虚拟机选择将方法区纳入垃圾回收的范围里,所以方法区有时候也被称为永久带,对于方法区的回收效果是很差的,因为加载的类型信息和常量池的卸载条件较为苛刻,但是由于方法区的内存有限,必然存在内存溢出的问题,所以对于永久带依然会进行垃圾回收,只不过频率相对较低。
⑥、元空间:
  JDK1.8之后,Java虚拟机弃用永久带,使用元空间来存储数据。在 JDK1. 7 及以前,元空间是放在永久代中的,JDK1.8 之后分离出来了。 元空间和永久代是方法区的实现,方法区只是一种规范,在 JDK1. 7 之后,原先位于方法区永久代里的字符串常量池已被移动到了 Java 堆中,因为永久代的内存空间极为有限,如果频繁调用 inter 方法,内存无法存储这么多数据。在JDK1. 8 之后将永久代完全删除了,使用元空间替代了永久代。 元空间使用本地内存,永久代使用 JVM 内存,所以使用元空间的好处在于程序的内存不在受限于 JVM 内存,本地内存剩余多少空间,元空间就可以有多大,解决了空间不足的问题。
⑦、运行时常量池:
  JDK1.7时从永久代移动到元空间当中,存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放,运行时常量池相对于class文件常量池的另外一个重要特征就是具备动态性,java并不一定要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
⑧、直接内存:
  直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是这部分内存也被频繁的使用,而且也可能导致内存溢出。
  在JDK1.4中加入了NIO类,一种给予channel和buffer的IO方式,它可以直接使用native函数直接分配堆外内存,通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用并进行操作。
⑨、对象访问定位:
  对象的存储主要在两部分,一个是对象的实例数据,即对应参数的值,一个是对象的类型参数。实例数据存储在堆中,类型数据存储在方法区中。JVM主流的访问方式有两种:

句柄访问
  在堆中划分出一块单独的区域,我们称之为句柄池,栈中对象的引用地址指向句柄池的一个句柄,一个句柄包含实例数据的地址和对象类型数据的地址。使用句柄的好处是栈中存储的是稳定的句柄地址,在GC垃圾回收或者其他状况下对象需要被移动时,只需改变句柄的地址,栈中的地址不用变动。

直接访问
  当采用直接访问的方式时,栈中存储的是对象实例数据的地址,在实例数据中有包含指向对象类型数据的地址。直接访问相对于句柄访问少了一次指针定位的开销。
  
使用直接指针访问方式的最大好处就是速度更快,他节省了一次指针定位的时间开销。
对于Hotspot虚拟机而言,他是使用第二种方式进行对象访问。

⑩、对象的创建:
  当我们创建一个对象时,虚拟机会在堆中开辟一块空间,用于存储数据。
  JVM内存的分配策略主要有两种:
  (1) 指针碰撞
   当堆内存的分布特点是规整的,即所有用过的内存在一侧,未用过的在一侧,同时设置一个分界点指示器,当分配内存时,就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种方式称为指针碰撞。
   规整的内存结构需要虚拟机具有压缩整理的功能,当有内存对象被回收后,虚拟机需要对现有的内存进行整理,重新生成完整的内存。
  (2)空闲链表
  当堆内存的分布不是规整的时候,堆内存的分布是不规整的,这时就需要虚拟机维护一个空闲列表,用于记录空闲的内存区域的大小和地址,当需要分匹配内存时按照一些分配策略分配合适的内存块,当对象被回收后,对应的内存区域应当重新加入空闲列表。这种方式称为空闲链表法。
  Serial、ParNew等收集器带有Compact过程,所以采用指针碰撞的算法,而CMS这种基于Maek-Sweep算法的收集器,一般采用空闲链表法。
  而java堆是否规整则是依据于堆使用的垃圾回收机制决定的。
  但对象的创建在JVM中是十分频繁的行为,在并发情况下内存的分配存在线程安全问题,即使是仅仅修改一个指针所指向的位置,在并发情况下也许并不安全,也许一个对象没有分配完毕,另一个创建请求就进来的。
  解决此类问题一般有两种解决方案:
  方案一、 保证分配动作的原子性(CAS)
  对分配内存空间的动作进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
  方案二、 尽量将内存分配的动作划分到不同的区域进行
  虚拟机会将不同线程的分配划分到不同的内存区域中去,在线程创建的过程中会对应的在内存区域划分一块专属于该线程的区域,通常我们称之为本地线程分配缓冲(TLAB)。首先线程会在本线程的缓冲区域分配内存,只有当TLAB的内存用完才会分配新的TLAB,此时才需要考虑同步锁定。
  是否使用TLAB可以通过 -XX :+/-UseTLAB参数来设定。
  
  在虚拟机执行完new指令后,会接着执行init方法,把对象按照用户的意愿进行初始化。

⑪、对象的内存布局:
  对象在内存中的存储结构可分为:对象头,实例数据,对齐填充。
  对象头包括两部分,一部分用于存储对象自身运行时数据,如hashcode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程的ID,偏向时间戳;另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,如果对象是一个java数组,那么对象头中还必须有一块用于记录数组长度的数据。
  实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是父类继承下来的,还是在子类中定义的。
  对其填充并不是一定存在的,仅仅起着占位符的作用,由于Hotspot自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数,当实例部分没有补齐时,就需要通过对其填充来补全。

你可能感兴趣的:(java)