1,引言
写这篇文章的时间是2023.3.4,16点半。也没啥事情,就写写虚拟机的读书笔记,总感觉,只会编程语言远远不够,就想黑箱一样,确实,编程语言的底层,似乎更令人着迷。虽然,我也看不懂这本书在写啥。但我想起来,高中时,我的一位数学老师,在期中考试之后,因为我们班的数学不行,许多同学去问数学老师,数学题看不懂,答案解析也不懂,她说的一句话,至今让我受益匪浅,抄解析,一遍不够就多抄几遍,直到会了为止。我当时大为震惊,放到现在,当我跟同学提起不会数学就抄,难免会引来一阵不理解甚至嘲笑。没办法,聪明的人太多了,对于我这种数学次次不超过60分的人来说,抄解析,对我来说,反而作用是最大。努力吧,少年,读书破万卷,下笔如有神!突然想到,看书的话,分段来看,效果也不错。
2,运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口的描述信息外,还有一项信息是常量池,用于存放编译时生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
java虚拟机对class文件每一部分的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可,装载和执行,但对于运行时常量池,java虚拟机没有任何的要求,不同的提供商实现的虚拟机可以按照自己的需要实现这个区域。
一般来说,除了保存CLass文件中描述的符号引用外,还会把编译出来的直接引用也存储在运行时常量池。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的就是String类的intern()方法。
既然运行时常量池是方法区的一部分,自然也会受到方法区的内存的限制,当常量值无法再申请到内存时会抛出OutOfMemoryError异常。
3,直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但是这部分也被频繁的使用,而且也可能导致OutOfError异常出现。
在JDK1.4中加入了Nio类,引入了一种基于通道(channel)和缓冲区(buffer)的IO方式,她可以使用Native函数库直接分配堆外内存,然后通过一个存储在JAVA堆中DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能。因为避免了在JAVA堆和native堆来回复制数据。
4,HotSpot虚拟机对象探秘
介绍完java虚拟机的运行数据区之后,我们大致知道了虚拟机内存的状况,直到内存中放了些什么,也就更想了解虚拟机内存中的其他的数据细节,比如,她们是如何创建的,如何布局以及如何访问。对于这样设计细节的问题,必须把讨论的范围限定在具体的虚拟机和某一块内存才有意义。基于优先的·原则,我们就拿HatSpot和常用的Java堆来讨论。
5,对象的创建
java是一门面向对象的编程语言,在java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(比如克隆,反序列化)通常仅仅是一个new关键字就可以了,而在虚拟机中,对象(文中讨论的对象限于普通的java对象,不包括数组和class对象等)的创建又是怎样一个过程呢?
虚拟机遇到一条new指令时,首先首先将去检查这个指令的参数是否定位到常量池一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化过,如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。假设java堆中的内存时绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存仅仅是把那个指针指向空闲那边挪动一段与对象大小相等的距离,这种分配内存方式叫做指针碰撞。如果java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,上面记录了,哪些内存是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表的记录,这种分配方式叫做空闲列表。
选用哪种分配内存的方式又java堆是否是否规整决定,而java堆是否规整由所采用的的垃圾收集器是否带有压缩整理功能决定。
除了如何划分可用空间之外,还有另外一个需要考虑的问题,对象创建在虚拟机中是非常频繁的行为。即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,解决这个问题由两个方案。
一种是对分配内存空间的动作进行同步处理----实际上是虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
(到重点啦,Thread Local,后端开发经常会用到这玩意,听说很有用)
另一种是把内存分配的动作按照线程划分在不同的空间执行,即每个线程在java堆中预先分配一小块内存,叫做本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,那就哪个线程的TLAB上分配。
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何找到类的元数据的信息,对象的哈希码,对象的GC分代年龄等信息。这些信息存放在对象的对象头(object header)之中,根据虚拟机当前的运行状态的不同,如是否启动偏向锁,对象头会有不同的设置方式。
okok,前两章基本算是大致过了一遍,有点懵,但没关系,第一次读书争取和虚拟机混个脸熟就行,就像谈恋爱一样,谁不是从知道对方的名字开始呢,(虽然我没对象)哈哈哈,扯远了,没事,估计也不会有人看到这里。