JVM学习笔记(一)

版权声明:欢迎转载,转载请注明原博客网址 https://blog.csdn.net/qq_37497322/article/details/82527967

最近在看《深入理解Java虚拟机》,第一章说是自己编写个JDK,看的云里雾里就跳掉了,两天马马虎虎才看完了第二章,下面是自己写的笔记。

1.Java程序设计语言,Java虚拟机,JavaAPI类库这三部分统称为JDK(Java Development),JDK是用于支持Java程序开发的最小环境
2.可以把JavaAPI类库中的Java SE API子集和Java虚拟机这两部分统称为JRE
(Java Runtime Environment),JRE是支持Java程序运行的标准环境
3.根据各个组成部分的功能来划分,可用分为4个平台
(1)Java Card:支持一些Java小程序运行再小内存设备上的平台
(2)Java ME:支持Java程序运行在移动终端上的平台
(3)Java SE:支持面向桌面级的应用,提供了完整的Java核心API
(4)Java EE:支持使用多层架构的企业引用,除了提供Java SE外,还做了大量
的扩充并提供了相关的部署支持
4.运行时数据区域
Java在运行程序时会将它所管理的内存划分为若干个不同的数据区域
(1)程序计数器
可以看作是当前线程所执行字节码的行号指示器,字节码解释器工作就是通过改变计数器的值
来选取下一条需要执行的字节码命令。一个处理器都只会执行一条线程中的命令,每条线程都
需要有一个独立的程序计数器,各线程的计数器之间互不影响,独立存储。如果线程正在执行
的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令代码。如果正在执行的
是Native方法,这个计数器值为空,此内存区域是唯一一个在Java虚拟机规范中没有任何规
定的OutOfMemoryError情况的区域。
(2)Java虚拟机栈
与计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是
Java方法执行的内存模型:每个方法再执行的同时都会创建一个栈帧,用于储存局部变量表,
操作数栈,动态链接,方法出口等信息,每个方法从调用至执行的过程中,就对应这一个栈帧
在虚拟机栈中入栈到出栈的过程。局部变量所需的内存空间在编译期间完成分配,当进入一个
方法时,所需要在帧中的分配多大的局部空间是完全确定的,方法运行期间不会改变局部变量
表的大小。这个区域规定了两种异常:如果线程请求栈深度大于虚拟机所允许的深度,将抛出
StackOverflowError异常,如果虚拟机栈可用动态扩展,扩展时无法申请到足够的内存,
就会抛出OutOfMemoryError异常。
(3)本地方法栈
本地方法栈和Java虚拟机栈的作用非常相似,区别只是虚拟机栈为执行Java方法服务,本地
方法栈则为虚拟机使用的Native方法服务。在虚拟机规范中对本地方法栈中使用的语言没有
强制规定,因此具体的虚拟机可以自由实现它。与虚拟机栈一样本地方法栈也会抛出
StackOverflowError异常和OutOfMemory异常。
(4)Java堆
Java堆是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一片区域。此内存区域
的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾管理器
管理的主要区域,因此很多时候也被称作“GC堆”。从内存分配的角度来看,Java堆可用划分出
多个线程私有的分配缓冲区,无论怎么划分,都与存放内容无关,存储的仍然是对象实例,进一步
划分是为了更好的回收内存,或者更快得分配内存。Java堆可用处于物理上不连续的内存空间中,
只要逻辑上是连续的即可,如果在堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出
OutOfMemoryError异常。
(5)方法区
与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态
变量等数据。虽然Java虚拟机规范把堆描述为堆的一个逻辑部分,但是它有一个别名叫做"Non-Heap"
即非堆,目的是与Java堆区分开来。Java虚拟机堆方法区的限制非常宽松,除了和Java堆一样不需要
连续的内存即可选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集在
这个区域是比较少出现的,这个区域主要是针对常量池的回收和对类型的卸载,条件相当苛刻。Java
虚拟机规范规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
(6)运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还
有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方
法区的运行时常量池中存放。对于运行常量池,Java虚拟机规范没有做任何细节要求,不同的提供商
可以按照自己的需要来实现这个内存区域。运行时常量池还有一个重要特征是具备动态性,Java语言
常量不一定只有在编译期间才能产生,也就是并非预置入Class文件中常量池的内容才能进入运行时
常量池,运行期间也可能将新的常量放入池中,这种特效被开发人员用的最多的是String类的intern
方法。当常量池无法申请到内存时会抛出OutOfMemoryError异常。
5.直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分
内存也被频繁地使用,在JDK1.4中新引入了NIO(new Input/Output)类,引入了一种基于通道与
缓存区的I/O方式,可用使用Native函数库直接分配堆外内存,然后用过一个存储在Java堆中的
DirectByteBuffer对象作为这块内存的引用进行操作,避免了Java堆和Native堆中来回复制数据,
在一些应用场景中显著提高性能。当直各个内存区域总和大于物理内存限制时会抛出OutOfMemoryError
异常。
6.对象的创建
当虚拟机遇到一条new命令时,先去检查这个命令参数能否在常量池中定位到一个类的符号引用,并且检查
这个符号引用代表的类是否已被加载,解析,初始化过,如果没有,则必须先执行相应的类加载过程。之后,
虚拟机将为新生对象分配内存。对象所需内存在类加载完成后便可完全确定,为对象分配内存空间相当于把一
块确定大小的内存从Java堆中划分出来。划分可用空间一般有两种方法,一种是“指针碰撞”,另一种是
“空闲列表”,选择哪种分配方式由Java堆是否规整决定,而其又由所采用的垃圾收集器是否带有压缩整理
功能决定。对象的创建是虚拟机中非常频繁的行为,即使只是修改一个指针指向的位置,在并发的情况下也
不是线程安全的,可能出现正在给对象分配内存,指针还没来得及修改,另一个对象又使用了原来的指针分配
内存的情况。解决这个问题有两种方案,一是堆分配内存的动作进行同步处理,另一种是把内存分配的动作
划分在不同的空间之中进行,即每个线程在Java堆中预先分配一块小的内存,被称为本地线程分配缓冲(TLAB)
哪个线程需要分配内存,就在哪个线程的TLAB上分配。内存分配完后,虚拟机要将分配的内存空间都初始化为0,
这一操作保证了对象实例再Java代码中可以不赋值就使用。接下来虚拟机要对对象信息进行必要的设置,这些信息
存放在对象头之中。从虚拟机的视角上看,一个对象已经产生了,但从Java程序的视角上看,还需要执行<init>
方法对对象进行初始化,一个真正可用的对象才算完全产生出来。
7.对象的内存布局
对象在内存中的存储布局可以分为3块区域:对象头,实例数据和对齐填充。
(1)对象头
对象头包括两部分信息,第一部分用于存储对象自身运行时数据,如哈希码,GC分代年龄,锁状态标志等。对象头
的另一部分是类型指针,即对象指向它的类元数据指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。不是
所有虚拟机实现都必须在对象数据上保留类型指针,如果对象是数组,对象头中还必须有一块用于记录数组长度的数据。
(2)实例数据
实例数据部分是对象真正存储的有效信息,也是程序代码中定义的各种类型的字段内容。无论是从父类继承下来的,
还是在子类中定义的,都需要记录下来。这部分的存储顺序会受虚拟机分配策略参数和字段在Java中的定义顺序的
影响。
(3)对齐填充
这一部分并不是必然存在的,也没有特别的含义,仅起着占位符的作用,当对象实例数据部分没有对齐时,就需要通
过对齐填充来补全。
8.对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象。对象访问方式取决于虚拟机的实现,目前主流的
访问方式有使用句柄和直接指针两种。
(1)使用句柄
Java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据
与类型数据各自的具体地址信息
(2)直接指针访问
Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址。
两种访问方式各自的优点:
使用句柄访问最大的好处就是reference中存储的是稳定的句柄地址,对象被移动(垃圾收集时移动对象是非常普遍
的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。
9.OutOfMemoryError异常
(1)Java堆溢出
Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制
清楚这些对象,那么对象数量达到最大堆的容量限制后就会产生内存溢出异常。当Java堆内存溢出时,异常堆信息
会进一步提示"Java heap space"。要解决这个区域的异常,一般是先通过内存影响分析工具对Dump出来的转
储快照进行分析,重点是确认内存中的对象是否必要的,先分清楚是发生了内存泄露还是内存溢出。如果是内存泄露,
可进一步通过工具查看泄露对象到GC Roots的引用链,于是就能找到泄露对象是通过怎样的路径与GC Roots相关联
并导致垃圾收集器无法自动回收它们的。掌握了这些信息就可用比较准确地定位出泄露代码的位置。如果不存在泄露,
就是内存中的对象确实都还必须或者,就应当检查虚拟机的堆参数,与机器物理内存对比是否还可用调大,从代码上
检查是否存在某些生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
(2)虚拟机栈和本地方法栈溢出
Java虚拟机中描述了两种异常,如果线程请求栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError
异常,如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。实验结果表明,无论
是栈帧太大还是虚拟机容量太小,当内存无法分配的时候,抛出的都是StackOverflowError异常。如果测试不限
于单线程,通过不断地建立线程的方式倒是可用产生内存溢出异常。建立过多线程导致的内存溢出,在不能减少线程数
或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
(3)方法区和运行时常量池溢出
运行时常量池是方法区的一部分,当前的主流框架如Spring,Hibernate在对类进行增强时,会使用到GCLib这类
字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可用加载入内存,经常会出现这类问题。在
经常动态生成大量Class的应用中,需要特别注意类的回收状况,除了GCLib和动态语言之外,常见的还有JSP或者动态
生成JSP的应用,基于OSGi的应用。
(4)本机直接内存溢出
当代码越过DirectBytesBuffer类,直接通过反射获取实例进行内存分配时会产生该异常,因为虽然使用
DirectBytesBuffer分配内存也会抛出内存溢出异常,但抛出的异常并没有真正向操作系统申请分配内存,
而是手动计算得知内存无法分配,手动抛出异常,由DirectBytesBuffer导致的内存溢出,一个明显的特征是
Heap Dump文件中不会看见明显的异常。


 

猜你喜欢

转载自blog.csdn.net/qq_37497322/article/details/82527967