距上次写了流水账已有两年半有余了,每次想写一篇冒冒泡时便觉得肚子没有墨水。正巧最近重新看了下JVM相关的书籍,就当作记个读书笔记吧。
JVM的介绍
内存管理如围城,里面的人想出去,外面的人想进来(看到封面这张图并不想)。
之所以Java语言能一次编译而又能处处运行,是因为在操作系统层面上再加了一层虚拟机,用以屏蔽系统和硬件之间的差异。让你仿佛运行在一个为你量身定做的电脑上。像C/C++ 这种是直接编译成某个操作系统能了解的文件,能够直接执行,但换到另一个系统却跑不起来。这是因为其他系统对于你编译出来的文件并不认识,当然就无法运行。所以只能将写好的代码在各个平台上重新编译一下。
我们所说的跨平台运行,并不是说写好的代码直接去运行(程序是静态的,相当于只是个模板),而是将写好的代码编译成操作系统和硬件相关的能够支持的文件。
有人说过一句名言:
“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”
“Any problem in computer science can be solved by anther layer of indirection.”
为了能让编译好的Class文件能够在任何平台运行,便创造了JVM这个巨人。只要在每个平台上安装个JVM,便能直接运行Class文件而不用管系统的差异,一切都给你屏蔽掉了。毕竟每个成功的Class背后都有一个默默付出的JVM。
JVM的另一个特点就是内存管理,写过C/C++的都知道内存管理是多繁琐,一不小心就出现内存泄漏等等致命的问题。虽然在这个世界里拥有至高无上的权力,但又要为每个公民负责出生 (生成对象) 到死亡 (回收对象) 的事情,还是很累的。所以JVM出来替你管理这些对象的出生以及死亡的事情。
除了平台无关性外,JVM还有语言无关性。无论是什么语言,只要能编译成字节码(Class文件)都能帮你运行,比如Kotlin。
运行时数据区域
在执行程序时,JVM会将所管理的内存划分成若干个区域,每个区域都有自己的功能。
-
方法区:用于存放被虚拟机加载的类型信息、常量、静态变量以及被即时编译后的代码等信息。它的前世有个名字叫“永久代”。属于线程共有的区域。
-
运行时常量池:该部分是方法区的一部分,用于存放编译期间生成的各种符号和符号引用,以及存放在运行期间产生的常量
-
虚拟机栈:执行Java方法时,会为每个方法创建栈帧,用以存放局部变量表、操作数栈、动态链接、方法出口等信息。一个方法执行就是入栈,执行完毕并返回就是出栈。也是线程私有的区域。
-
本地方法栈:与虚拟机栈类似,只不过服务的对象是本地方法而已。
-
程序计数器:执行Java方法时存放下一条要执行的字节码指令的地址,线程私有、互不干扰。当执行本地方法时为Undefined
-
堆:堆是JVM管理内存中最大的一块,也是被所有线程共享的区域。JVM世界中,几乎所有的对象实例都是在此生成的
-
直接内存
对象的创建方式
-
指针碰撞:我们可以假设Java堆为一个一维数组,所有被使用的放在数组的左边,未被使用的放在数组右边,用一个指针指着已使用和未使用的分界处。当为对象分配内存时,只要将指针向右移动所需要的空间即可。
-
空闲列表:如果这个一维数组中,已使用的空间和未使用的空间依次交错。那就只能使用一张列表来记录哪些位置被使用了、哪些位置未被使用。
-
如何选择:对于这两种分配方式的选择,具体要看Java堆中的分配是否整齐,而是否整齐,就取决于所采用的垃圾收集器是否带有空间压缩功能(就是将已使用的全部推到一侧)。
对象的内存布局
-
对象头:对象头分为两部分,一部分是存储自身运行时数据(Mark Word),另一部分是存储类型指针。
-
实例数据
-
对齐填充:HotSpot的自动内存管理系统要求任何对象的大小必须8字节的整数倍,当不够时则填充。
对象的访问方式
-
句柄访问:在Java堆中划分一个区域用来存放句柄池。当访问时需先到句柄池中定位到对象的位置再去访问。需要有一次间接访问,这对于对象发生移动时只需修改句柄池中该对象的位置即可。
-
直接访问:顾名思义,直接通过reference(里面存储着对象的地址)进行访问对象,省了一次访问句柄池的时间。
几种清理死亡对象的方法
下次再写了
常见的几种垃圾回收器
下次再写了