深刻理解Java虚拟机--我的总结(持续更新)

深刻理解Java虚拟机--我的总结(持续更新)

天天按照书本学一点,会把本身的总结思考写下来,造成输出,持续更新,立帖为证java

-- 2020年7月7日 开始第一次学习
-- 2020年7月8日 今天在百忙Rush B中抽出时间,学了点习,计划明天把本地方法栈和Java堆看完总结完
-- 2020年7月10日 第一次周五学习,也算是有进步,翻了一下书感受好多啊,不知道何时能看完
-- 2020年7月15日 冲鸭!!!!

第二部分、自动内存管理

1、Java内存区域与内存溢出异常

Java与C++在内存控制方面大相径庭,由于Java虚拟机有自动内存管理机制,因此Java程序员就牺牲部份内存控制权,来换取编写程序时的便利。虽然不容易出现内存泄漏和内存溢出问题,但仍是有必要学习点Java虚拟机相关知识,除了在遇到虚拟机问题时能够快速解决以外,还能够和别人装逼(最大的快乐!)
Java虚拟机在运行Java程序的时候,会将内存自动划分为不一样区域,不一样区域对应的功能、建立销毁时间也不一样,有些区域会随着虚拟机启动而一直存在,有些区域以来用户的线程启动结束而建立销毁。程序员

内存区域分为如下几个区域:数组

  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈
  • Java堆
  • 方法区
  • 运行时常量池
  • 直接内存

程序计数器

  • 程序计数器是:一小块内存空间,记录当前线程执行字节码指令的地址,字节码解释器就是经过改变计数器里面的值来肯定下一条须要执行的字节码指令
  • “线程私有”的内存空间:在任何肯定时刻,处理器都只会执行一个线程中的指令(注:Java的多线程是经过切换线程,分配处理器执行时间来实现的),每一个线程都须要记录执行到那条指令了,接下来该执行哪条,因此每一个线程都会有一个线程计数器,并且独立存储互不干扰
  • 存储内容:
    • 若是线程正在执行Java方法,则计数器记录的是正在执行虚拟机字节码的指令地址
    • 若是正在执行本地方法(Native),则计数器为空
  • 此内存区域是惟一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError状况的区域

Java虚拟机栈

  • Java虚拟机栈:描述的就是在执行Java方法时线程内存模型。每一个方法在被调用的时候都会同步建立一个"栈帧",存储到Java虚拟机栈中,这个栈帧里面包含:局部变量表操做数帧动态链接方法出口等信息,一个方法从被调用开始执行到执行完毕,就对应这栈帧从入栈到出栈过程。
  • 线程私有内存空间:和程序计数器同样时线程私有的,生命周期和线程同步。

思考:了下为何也是线程私有的?应该时每一个线程执行方法不一样,里面的一些临时变量等也不会相同,为了在切换线程时不会发生混乱互相干扰,因此须要和程序计数器同样,也是线程私有的内存空间缓存

  • 会有人把Java虚拟机内存空间笼统的划分为"栈空间","堆空间",这里说的"栈空间"一般就是指Java虚拟机栈,在笼统一点一般指的是Java虚拟机栈里面的局部变量表这部分
  • 局部变量表:
    • 存放内容:存放了编译器基本数据类型对象引用(并非对象自己,多是只想对象起始地址的指针,也多是指向一个表明对象的句柄???,或者其余于此对象相关的位置),returnAddress类型(返回地址类型,指向一条字节码指令的地址)
    • 局部变量表中的存储空间:都是以局部变量槽(Slot)来表示,其中64位长度的long和double类型数据占两个变量槽,其他数据类型占一个。
    • 在程序编译期间就已经肯定好了局部变量表的大小并完成分配。当进入一个方法时,该方法须要在局部变量表中分配多大的空间都是肯定好的,在运行期间不会改变局部变量表的大小。
    • 上面所说的”大小“是指局部变量槽的数量,不一样虚拟机的一个变量槽可能会占不一样大小内存空间(一个变量槽占32比特或64比特)
  • 异常:《Java虚拟机规范》对该内存区域规定了两个异常:
    • 若是线程请求栈的深度大于虚拟机所容许的深度,则会抛出StackOverflowError异常;
    • 若是Java虚拟机栈的容量能够动态扩展,当栈扩展时没法申请到足够的内存就会抛出OutOfMemoryError异常

本地方法栈

本地方法栈与Java虚拟机栈做用类似,但也稍有区别。Java虚拟机栈是为虚拟机执行Java方法(字节码)服务的,而本地方法栈是为虚拟机执行本地方法(Native)服务的。安全

由于在《Java虚拟机规范》中,并无对本地方法栈作强制规定,因此不一样虚拟机实现的方式可能不一样,有些虚拟机(HotSpot虚拟机)直接将本地方法栈和Java虚拟机栈合二为一多线程

与Java虚拟机栈同样,当本地方法栈深度超出规定(溢出)和栈扩展失败的时候,也会报StackOverflowError和OutOfMemoryError异常函数

Java堆(Java Heap)

Java堆是虚拟机管理内存中最大的一块,被全部线程共享,在虚拟机启动时建立。布局

主要是负责存放对象实例,按照《Java虚拟机规范》描述是:全部对象实例以及数组都应当在堆上分配。考虑到Java语言的发展,和即时编译技术的出现,将来可能会出现对象实例不在堆上分配的状况。性能

Java堆是垃圾收集器管理的内存区域,所以也被称为"GC堆"。垃圾收集器大部分是基于分代收集理论设计的,因此会出现新生代、老年代、永久代,Eden空间、Form Survivor空间、To Survivor空间等名词,这些划分的区域仅仅是垃圾收集器共同特性或设计风格,并不能说是Java堆是由这些区域组成的。学习

从分配内存角度说,线程共享的Java堆能够划分多个线程私有的分配缓冲区(TLAB),划分出来的惟一做用仍是存放对象实例,目的是为了更快更好的分配和回收内存。

Java堆在逻辑上是连续的,但在物理上并不要求连续。若是存放的是大对象,例如:数组对象,大多数虚拟机为了实现简单、存储高效,可能会要求连续的存储空间。

Java堆既能够是固定大小,也能够是可扩展的。目前主流虚拟机都是可扩展的,经过参数-Xmx和-Xms设定。若是在Java堆中没有内存给对象实例分配,而且没法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

方法区

在《Java虚拟机规范》中对方法区的约束是十分宽松的,许多部分和Java堆相同,例如:

  • 都是线程共享
  • 物理上不须要连续的存储空间
  • 能够选择固定大小或可扩展

并把方法区描述为堆的一个逻辑部分,可是方法区和堆仍是有区别的,方法区的另外一个别名叫"非堆(Non-Heap)",方法区用来存放已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

方法区与永久代关系

本质上二者并非等价的,但不少人将二者混为一谈,这是由于当初HotSpot虚拟机在设计的时候,为了简单方即可以像管理Java堆同样管理这部份内存,将垃圾收集器的分代设计扩展至方法区,即便用永久代来实现方法区。但Java虚拟机规范中并无对方法区的实现作具体要求,因此其余虚拟机(如:BEA的JRockit、IBM的J9)都没有永久代这个概念。

使用永久代实现方法区好处:

能够像管理Java堆同样管理一部份内存,省去了专门为方法区编写管理代码的工做

使用永久代实现方法区坏处:

会致使Java应用更容易遇到内存溢出的问题,永久代有-XX:MaxPermSize的上限,即便没有设置也有默认值,而J9和JRockit只要没有触碰到进程可用内存的上限,例如32位系统中4GB限制,就不会出现问题。

有极少数方法(String::intern())会因永久代的缘由而致使不一样虚拟机下有不一样表现

永久代介绍

垃圾收集行为在永久代不多出现,但并非数据进入永久代以后就永久存在了,这一区域内存回收目的主要是针对常量池回收和对类型的卸载,可是由于回收条件严格,因此回收效果总不能使人满意。

  • JDK6的时候,HotSpot开发团队计划放弃使用永久代,逐步改成采用本地内存(Native Memory)来实现方法区
  • JDK7的时候,把本来放在永久代的字符串常量池、静态变量移出
  • JDK8的时候,放弃使用永久代,在本地内存中实现元空间(MetaSpace)来代替,并把JDK7中还保留在永久代中的内容所有移出。

当方法区没法知足新内容内存分配的时候,就会抛出OutOfMemoryError异常。

运行时常量池

运行时常量池是方法区的一部分,用来存放编辑时生成的各类字面值和符号引用,由于《Java虚拟机规范》并无对这部分作详细要求,因此虚拟机开发者能够按照本身需求去实现这部份内存。除了上面戳的符号引用外,通常还会将符号引用翻译出来的直接引用也存到运行时常量池中。

具有动态性。并不必定是预置入Class文件中常量池才能进入方法区的运行时常量池,运行期间能够将新的常量放入。

当没法申请到足够内存时,会抛出OutOfMemoryError异常。

直接内存

直接内存并非虚拟机运行时数据区(上面写的都是)的一部分,也不是《Java虚拟机规范》中定义的内存。

用力提升性能,避免在Java堆和Native堆中来回复制数据。

直接内存并不受Java堆内存大小的限制,可是受本机总内存的限制。根据实际内存设置-Xmx等参数时,若是忽略直接内存,可能会致使抛出OutOfMemoryError异常。

2、HotSpot虚拟机对象探秘

一、对象建立

  1. 类加载:Java虚拟机遇到new指令的时候,首先会去常量池定位一个类的符号引用,并检查这个类是否已经被加载,解析和初始化过。若是没有则进行类加载过程

  2. 分配内存:在类加载以后,就知道对象所须要的内存大小,接下来开始为对象分配内存。对象分配内存是在堆上完成的,划出一块未使用的内存给对象,分配的方式有两种:"指针碰撞","空闲列表"。到底采用哪一种分配方式取决于Java堆是否规整,Java堆是否规整又取决于垃圾收集器是否带有空间压缩整理(Compact)能力。

    分配内存中为了解决线程安全问题有两种方案:1、对分配内存空间的动做进行同步处理,即虚拟机采用CAS配上失败重试的方式保证更新操做的原子性。2、使用本地线程分配缓冲区(TLAB),哪一个线程要分配内存,就在哪一个线程的本地缓冲区进行分配,只有缓冲区用完了,在分配新的缓冲区的时候才须要同步锁定。

  3. 赋初始值:保证对象的实例字段不赋初始值就能够直接使用,能够直接访问这些字段的初始值。

  4. 虚拟机对对象设置:虚拟机会将一些必要信息保存在对象头中,如:这个对象是哪一个类的实例,如何才能找到类的元数据信息,对象的哈希码等等。

  5. 执行构造函数:此时站在虚拟机角度看对象已经建立好了,可是此时对象中字段仍是默认零值,须要执行构造函数,按照设计意图构造好。

二、对象的内存布局

三、对象的访问定位

相关文章
相关标签/搜索