赞
踩
Java程序设计语言、Java虚拟机、Class文件格式、API库、第三方类库等几部分构成。其中,Java程序设计语言、虚拟机、API合为JDK,JDK是Java程序开发的最小环境,Java SE API子集与虚拟机,统称为JRE,JRE是支持Java运行的最小环境,
上图来自书中。
见下图。
当然这只是这本书撰写的时候,JDK12出现,到今天已经发布了JDK19。这个不影响阅读这本书,注重点是Java虚拟机。
始祖:Sun Classic/Exact VM,只能使用纯解释器执行Java代码,如果使用即使编译器就必须进行外挂,这就导致有了外挂编译器,解释器就会被替代,并且在这里,编译器必须对每一行代码进行编译,这就是当时”Java语言很慢“的由来。
武林盟主:HotSpot VM,目前使用范围最广的Java虚拟机。最突出的优点:热点代码探测能力,通过执行计数器找出最具有的编译价值的代码,然后通知即时编译器以方法为单位进行编译。
小家碧玉:Mobile/Embedded VM,Java ME虚拟机,由于智能手机这块早已被Android和IOS占据,这虚拟机一直不太行。
天下第二:BEA JRockit/IBM J9 VM,这俩当时与HotSpot并存的虚拟机,J9在模块化和职责分离比HotSpot更优秀。
软硬合璧:BEA Liquid VM/Azul VM,多用特定的软硬件配合的专用虚拟机。
挑战者:Apache Harmony/Google Android Dalvik VM,
还有很多。
内存管理对于C/C++开发人员来说,无法避免的事情,在JVM的管理,Java程序员省去了这一步骤,都交给了JVM管理,这就意味着一旦出了问题,不了解JVM的话,解决这个问题会变得很难。
可以看作师当前线程所执行字节码的行号指示器。它是程序控制流的指示器,分支、循环、跳转等等基本功能都依赖它,它是一个线程私有的,也就是每个线程都有自己的计数器,互不干涉。比如:线程执行的是一个Java方法,计数器记录的就是正在执行的虚拟机字节码指令的地址,如果是一个Native方法,这个计数器值为空,这也是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈也是线程私有的,跟线程的生命周期是一样的,它描述的是Java方法执行的线程内存模型,一般这个栈存放的是Java方法中的局部变量表、操作数栈、动态连接、方法出口等信息,局部变量表一般放着各种基本类型,对象引用类型等等。
跟虚拟机栈功能差不多,它主要存的是本地方法的信息,有的虚拟机就把这两个栈合二为一了,就是方法栈。
堆是线程都共享的一块内存区域,虚拟机启动时创建,它放的是对象实例,“几乎”所有的对象实例都在这里分配内存。Java堆还是垃圾收集器管理的内存区域。
Java堆在物理上可以是不连续的,但是在逻辑上是连续的。但是对于大对象(数组对象等),物理上也是连续的。堆的大小可以是固定的,也可以是可拓展的,当然,一般的虚拟机都按照可拓展的来处理。
方法区跟堆一样,都是线程共享的,它放的是已经被虚拟机加载的类型信息、常量、静态变量、缓存等等,方法区跟堆一样,不要求连续内存和固定大小,他甚至不要求实现垃圾收集。方法去的内存回收的主要目的是针对常量池的回收和类型的卸载
它是方法区的一部分,存放编译期生成的各种字面量与符号引用,这些都是类加载后存到常量池中的。运行时常量池的“运行时”说的他的动态性,运行期间的新的常量也可以放入其中,不必只是编译期预置到class文件中常量池的内容。
在JDK1.4新加入的NIO类,引入新的基于通道与缓冲区的IO方式,它可以直接使用Native函数库直接分配堆外内存。直接内存不是虚拟机运行时的数据区的一部分,也没在《Java虚拟机规范》中的定义的内存区域,不受java堆的大小的限制,但肯定受到本机总内存的限制。
对象实例存放在堆中,那么对象是怎么分配的、布局以及访问的呢
一般,我们创建直接new一个就行了,那么在JVM中,这个过程是怎样的呢?
JVM遇到new字节码,先检查指令的参数是否在常量池能够定位到一个类的符号引用,并且检查这个类是否加载、解析、初始化过了。没有则先执行类加载。当类加载检查过后,为对象分配内存。(对象需要的内存大小在类加载完成后确定)分配规则可以分为两种,堆中的内存是否规整,如果规整,所有的被使用的内存都在一边,中间有个指针代表分界点的指示器,另一边则是空闲的,那么分配过程就是将这个指针向空闲空间挪一点距离,这距离就是对象大小。这叫分配方式为“指针碰撞”,如果不是规整的,虚拟机会维护一个列表,这个列表记录着那个内存块可用,分配时直接在列表中找到一块空间分给对象,并更新列表即可,这种分配方式为“空闲列表”。
java堆是否规整由所采用的垃圾采集器是否有空间压缩整理的功能决定的。一般的,指针碰撞是简单高效的,空闲列表是比较复杂的。
当然指针碰撞存在一个问题,它是简单的移动一个指针,但是当高并发时,会存在来不及修改的情况,一般解决这个问题,一种是对分配这个动作进行同步处理,虚拟机采用CAS加上失败重试的方法保证更新操作的原子性,另一种是将这个动作按照线程划分不同的空间中进行,在堆中分配一小块内存,即为本地线程分配缓冲区,线程要分配内存,就在该线程分配缓冲区中进行分配,当缓冲区用完了才进行同步锁定。
分配完成之后,虚拟机必须将分配的内存空间初始化为零,这样保证对象可以不赋初值,就可使用,直接访问对应类型的零值。然后再对对象进行必要的设置。完成这个之后,以虚拟机的角度老看,对象就产生了。但是从Java程序角度来看,创建才到了构造函数的地步,即Class文件中的init()方法还没执行,还没有进行赋值初始化等操作。完成初始化工作后才完成了对象得创建。
在HotSpot虚拟机中,对象在堆中可分为三部分:对象头、实例数据、对齐填充。
对象头包括两类信息,一是用于存储对象自身的运行时数据(官方称为Mark Word),比如哈希码、GC分代年龄等等,要存的东西很多,Mark Word是一个有着动态定义的数据结构,为了保证以极小的空间存取尽量多的数据,根据对象的状态复用自己的存储空间。
另一类信息是类型指针,即对象指向它的类型元数据的指针,JVM由这个指针确定对象是那个类的实例。当然,并不是所有的虚拟机都会在对象数据上保留类型指针,也就是查找对象的元数据信息并不一定要经过对象本身。此外,对象是个java数组得话,会有一块内存存储这个数组的长度数据。
实例数据是对象真正存储的有效信息,即在程序中的各种类型的字段代码,无论是父类继承的还是子类定义的都会被记录下来。存储顺序是收到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配顺序是longs/doubles、ints shorts/chars bytes/booleans oops。
对齐填充不是必须的,仅仅是占位符的作用。因为HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍。也就说对象的大小都是8字节的整数倍,对象头是被设计成了8比特的整数倍,实例数据部分没对齐的话就需要对齐填充。
主流的访问方式由使用句柄和直接指针两种:
句柄:堆中会划分出一块内存来作为句柄池,reference中的存储是对象的句柄地址(reference是Java程序的一个类型,是一个指向对象的引用),句柄中包含了实例数据、类型数据的具体地址信息。
直接访问:reference存储就是对象地址,直接可以访问对象,当然这就需要虚拟机如何放置访问类型数据的相关信息。
采用句柄访问的话,reference存储的是稳定的句柄地址,对象被移动,也只需改变句柄中的实例数据的指针,reference本身不用修改;直接指针就是速度快。HotSpot虚拟机主要采用得是直接指针,带来的收益太明显了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。