第一章的内容直接跳过了。背景故事有时间再看,虽然都是理论,但是对于理解而言帮助非常大。
在学习过程中,也算是有了种体会。JVM出现的各种各样的问题,其实绝大多数都是内存造成的。所以书上的学习也是从内存开始讲的。话不多说,开始吧。
在C语言,C++两种语言中,创建对象时,也意味着我们需要手动销毁这个对象,也就是构造函数与析构函数。但在java中,开发却并没有涉及到关于对象销毁的这个操作。因为java底层封装了垃圾回收器,来保证对无用对象的回收。JVM中存在哪些内存区域,垃圾回收器又怎么回收,从什么地方开始回收,都是需要了解的,否则,出现问题,没法排查是个硬伤。先从底层结构开始了解。
我们平时所说的JVM内存空间,指的其实就是运行时数据区部分。该部分与执行引擎,本地库接口,本地方法协作,来实现指令的执行,这个先不讨论。运行时数据区包含我们平时所说的堆,栈等一系列的信息,下面统一来进行介绍。
程序计数器,代表当前线程具体执行到哪行指令的一个行号指示器。在JVM概念模型中,字节码解释器其实就是通过改变程序计数器的值来得到下一条需要执行的指令。分支,循环,跳转,异常处理,线程恢复全部都需要依赖它。
假设在两个线程在同时执行,线程1执行到半路上,此时CPU所分配的时间片消耗完了,切换到线程2,线程2执行完后,肯定是要切换到线程1来继续执行的。而线程1,此时就是从程序记录器得到下一步要执行的指令行号,在原先的基础上继续执行。
程序计数器是线程私有的。在线程执行一个方法中,如果线程执行的是java方法,程序计数器记录的是正在执行指令的地址。而如果在native方法中,程序计数器记录的是undeified。此外,程序计数器在JVM虚拟机规范中,是唯一一个不会发生OutOfMemoryError的区域。
总结:程序计数器,线程私有,在执行java方法时记录当前线程执行到字节码指令的地址,在native方法中,内部值为undeified。在JVM虚拟机规范中,不会发生OOM异常。
在传统的C,C++语言来说,一般对内存划分为堆以及栈。虽然在java中不是完全这么区分,但也可以看不到栈的重要性。
虚拟机栈,当java每调用一个方法时,都会同步创建一个栈帧来用以存放局部变量表,操作数栈,动态链接,方法出口等信息。方法从调用到结束的过程,也就意味着栈帧从虚拟机栈入栈到出栈的过程。
虚拟机栈,也是线程私有的。随线程的创建而创建,随线程的消亡而消亡。
虚拟机栈,局部变量表,内部存储了基本数据类型,reference数据类型(hotspot虚拟机中代表了引用指针),returnAddress类型(指向了字节码的地址)。
在虚拟机栈中,数据的基本类型在局部变量表中是按变量槽的格式来进行存储,除了double,以及long这两个占据了8字节空间的类型按一个变量槽来进行存储,其余的都是一个变量槽。局部变量表所需内存空间在编译期便分配完毕,空间是完全固定的,不会随着方法运行而改变。
虚拟机栈,如果设定了栈的深度,在超出标准时会报StackOverFlowError。如果没有设定的话,直到内存无法给它分配空间,报OutOfMemeryError。(递归时不设置终止条件,直接挂)。
总结:虚拟机栈,是线程私有的,内部包含了局部变量表,操作数栈,动态链接,方法出口等。每调用一个方法会生成一个栈帧,在虚拟机栈中入栈。方法结束,则从虚拟机栈中的出栈。局部变量表存储了基本数据类型,reference类型,returnAddress类型。可能会出现stackOverFlowError或OutOfMemeryError。
调用java方法时产生栈帧并入栈虚拟机栈,那么调用native方法时也会产生栈帧,并且录入至本地方法栈。虚拟机栈和本地方法栈是类似的。
总结:本地方法栈,线程私有,内部同样包含局部变量表,操作数栈,动态链接,以及方法出口。在调用方法时也是,随着方法调用创建栈帧入栈本地方法栈,随着方法结束出栈本地方法栈。局部变量表中也是存储了基本数据类型,reference类型以及returnAddress类型。同样会出现StackOverFlowError以及OOM。
堆,其实就是存储创建出来的对象的内存空间。就目前的Java,所有的对象都是存储在堆中的(未来版本说不准改变)。堆,平时我们所说的垃圾回收,几乎说的都是回收堆中内存。
堆,在内存中是线程共享的。可以在保证线程安全的情况下做线程通讯。
堆的大小,可以通过-Xmx(最大堆内存),-Xms(最小堆内存)来设定,一般设定的一样,防止内存抖动。GC之类的后续再说。
内存泄漏用不好肯定会发生的,OOM不可避免。
方法区,也是线程共享的。主要是用于存储类型信息,静态变量,常量,以及JNI即时编译器存放的一些代码缓存。注意,方法区有一个名称,叫非堆(Non-Heap)。我们平时所说的非堆内存,从官方术语的角度来说,我理解的是方法区这块的内存。先这么理解着。
方法区,和永久代不是一回事。准确说,老版本的方法区的实现,是利用永久代来进行实现的。因为HotSpot虚拟机将垃圾收集器分代设计扩展到方法区,所以才被称呼为永久代。永久代也会存在OutOfMemeryError,因为永久代默认有-XX:MaxPerm的上限。到最后,使用元空间来替代。
方法区也会 存在垃圾收集,不过很少。就像常量池的回收,类型的卸载等等,虽然少,也是存在的。
注意,运行时常量池在方法区。
运行时常量池和常量池的区别是什么呢?
运行时常量池在方法区,而常量池则是.class编译文件中一部分,存储编译期间生成的字面量和引用,常量池会随着类的加载器存入到方法区中。
所以,每一个class文件都有一个常量池,常量池中的各种字符量以及符号引用,在类加载过程中存放到静态方法区的运行时常量池中。
运行时常量池是可以进行动态扩展的,例如String的intern()方法,就可以做到在运行池常量池中添加数据。
运行时常量池在方法区中,随方法区限制,自然也会出现OOM异常。
直接内存,Direct Memory不是运行时数据区的一部分。但是在调用NIO时,虽然在直接分配了堆外内存(这个概念我不太明确),但在java堆中存在一块DirectByteBuffer对这块内存存在引用,来提升性能。
但直接内存不会收到JVM堆大小限制,所以,在-Xmx等设置内存时,忽略掉直接内存,导致动态扩展出现OutOfMemoryError异常(这块不太懂,等整本书下来再回味)。