JVM从入门到精通(一)

走进Java

JDK、JRE与JVM

JDK:Java程序设计语言、Java虚拟机、Java类库。支持Java程序开发的最小环境
JRE:Java类库API中的Java SE API子集、Java虚拟机
JVM:JRE的子集

Java技术体系

JVM从入门到精通(一)_第1张图片

Java发展史

1995.5.23:Oak改名Java,正式发布Java1.0
1996.1.23:JDK1.0,代表技术包括:Java虚拟机、Applet、AWT
1997.2.19:Sun公司发布JDK1.1
1998.12.4:JDK迎来了一个里程碑式的重要版本,Sun在这个版本中将Java技术体系拆分为3个方向:J2SE、J2EE、J2ME
1999.4.27:HotSpot虚拟机诞生,JDK1.2.1和1.2.2发布
2000.5.8:JDK1.3发布
2002.2.13:JDK1.4发布。同年,微软的.NET Framework发布
2004.9.30:JDK5发布,开始放弃1.x的命名方式
2006.12.11:JDK6发布,放弃类如J2EE的命名方式,启用Java EE 6的新命名方式
JDK6发布以后,由于代码复杂性的增加,Java开源
JDK8彻底移除HotSpot的永久代

Java虚拟机家族

虚拟机始祖:Sun Classic/Exact VM

武林盟主:HotSpot VM(重点)

小家碧玉:Mobile/Embedded VM

天下第二:BEA JRockit/IBM J9 VM

挑战者:Apache Harmony/Google Android Dalvik VM

没有成功,但并非失败:Microsoft VM及其他

百家争鸣

Java内存区域与内存溢出

运行时数据区域

JVM从入门到精通(一)_第2张图片

程序计数器

当前线程所执行的字节码的行号指示器,每个线程都有自己的程序计数器,流程控制流的指示器,分支、循环、跳转、异常处理等基本功能都需要以来这个计数器来完成

  1. Java方法:正在执行的虚拟机字节码执行的地址
  2. 本地方法:null

注意:此内存区域是唯一一个没有规定任何内存溢出的区域

虚拟机栈

生命周期与线程相同,也成为线程栈
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈通常指的就是虚拟机栈,或者更多情况下只是虚拟机栈中局部变量表部分

  1. 栈的深度达到虚拟机所允许的深度抛出StackOverflowErro异常,一般是递归不闭合
  2. 栈扩展时无法申请到足够的内存抛出OutOfMemoryError异常,一般是线程启动太多
    https://blog.csdn.net/u014296316/article/details/82668670

本地方法栈

为虚拟机使用的本地Native方法服务。使用语言、方式与数据库并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一

也称为GC堆,内存分配和回收的主要区域,也是java虚拟机管理内存中最大的一块。唯一的目的就是存放对象实例
Java世界里"几乎"所有对象实例都是在堆区分配的,但也不是绝对的,例如:随着即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换手段导致了一些微妙的变化
如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,以提升对象分配时的效率。将堆区细化是为了更好地回收/分配内存
既可以实现为固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms设定)
如果堆区没有足够的内存来完成实例的分配,并且堆区无法再扩展时抛出OutOfMemoryError异常

方法区

物理部分属于堆,逻辑部分上别名非堆,目的是为了和堆区分开来,因为一般不对方法区进行垃圾回收。
HotSpot虚拟机也将永久代和方法区混为一谈,永久代是HotSpot的专有名词,可能导致内存溢出等问题。HotSpot团队JDK6中有放弃永久代,逐步使用本地内存来实现方法区的计划了,JDK7将原本在永久代的字符串常量池、静态变量等移除到堆区,而到了JDK8,终于完全废除了永久区的概念,改在本地内存中实现元空间来代理,把JDK7中永久代还剩余的内容全部内容(主要是类型信息)全部移到元空间中
《Java虚拟机规范》对方法区的约束是非常宽松的,垃圾收集行为在这个区域很少出现,以至于Sun公司的BUG列表中就出现过的若干个严重的bug就是由于低版本的HotSpot虚拟机对方法区未进行完全回收而导致内存泄漏

运行时常量池

方法区的一部分,用来存放编译期间生成的各种字面量与符号引用,在类加载后存放进来
特点:具有动态性,Java语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,例如String.intern()方法
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请内存时会抛出OutOfMemoryError异常

直接内存

其实就是Java虚拟机申请内存之外的内存,元空间就在这个部分,也有可能忽略直接内存,从而导致OutOfMemoryError异常出现(指的本机总内存,而不是Java虚拟机分配的内存)

HotSpot虚拟机对象探秘

  1. 对象的创建:Java虚拟机遇到一条字节码new指令
    a. 检查这个这条指令的参数是否能在常量池中定位到一个符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有则先执行相应的类加载过程
    b. 类加载检查通过后,接下来虚拟机为新生对象分配内存。对象所需内存大小在类加载完成时便可完全确定,分配内存相当于在堆区划分出一块确定大小空间的内存块

  2. 分配方式:由堆区是否规整决定

i. 指针碰撞:堆内存绝对工整(使用过的内存放在一边,空闲内存放在另一边,中间放一个指针作为分界点的指示器),分配内存就是将中间指针向空闲空间挪动一段与内存大小相等的距离
ii. 空闲列表:堆内存不规整(已被使用的内存和空闲内存放在一块),则维护一个列表,记录哪些内存是可用的,进行分配的时候找一块足够大的空间分给对象实例,并更新列表上的记录
而堆区是否规整又取决于采用的垃圾收集器是否带有空间压缩整理的能力决定

  1. 使用Serial、ParNew等压缩整理过程的收集器 --> 指针碰撞算法
  2. 使用CMS这种基于清除算法的收集器 --> 较为复杂的空闲列表

对象的创建十分频繁,并且容易导致线程不安全的问题

解决方案

  1. 对分配内存空间的动作进行同步处理,实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性
  2. 本地线程分配缓存TLAB:线程私有,每个线程在Java堆中预先分配的一小块内存。哪个线程需要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓冲区才需要同步锁定

对象的内存分布

在HotSpot中,对象在堆内存的存储布局可以划分为三个部分

  1. 对象头:存储对象自身的运行时数据Mark Word + 类型指针(指向对象本身的类型元数据的指针)
  2. 实例数据:对象真正存储的有效信息
  3. 对齐填充:仅仅起到占位的作用

原因:HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是,任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数。因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全

注意:如果是数组对象,则对象头中还存储数组长度

JVM从入门到精通(一)_第3张图片

对象的访问定位

Java程序会通过栈上的reference数据来操作堆上的具体对象

  1. 句柄
    可能会在堆区划分出一块内存来作为句柄池。reference指向对象的句柄地址,句柄中包含了对象实例数据和类型数据各自具体的地址信息
    JVM从入门到精通(一)_第4张图片
    优点:reference中存储的是稳定的句柄地址,在对象移动(eg. 垃圾收集时)时只会改变句柄中的实例数据指针
  2. 直接指针
    JVM从入门到精通(一)_第5张图片
    优点:速度快,节省了一次指针定位的时间开销

HotSpot主要采用直接指针的方式

参考:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) 周志明》

你可能感兴趣的:(JVM,java,后端,mysql,数据库,jvm)