深入理解Java虚拟机学习笔记.docx

上传人:牧羊曲112 文档编号:3635071 上传时间:2023-03-14 格式:DOCX 页数:27 大小:53.90KB
返回 下载 相关 举报
深入理解Java虚拟机学习笔记.docx_第1页
第1页 / 共27页
深入理解Java虚拟机学习笔记.docx_第2页
第2页 / 共27页
深入理解Java虚拟机学习笔记.docx_第3页
第3页 / 共27页
深入理解Java虚拟机学习笔记.docx_第4页
第4页 / 共27页
深入理解Java虚拟机学习笔记.docx_第5页
第5页 / 共27页
亲,该文档总共27页,到这儿已超出免费预览范围,如果喜欢就下载吧!
资源描述

《深入理解Java虚拟机学习笔记.docx》由会员分享,可在线阅读,更多相关《深入理解Java虚拟机学习笔记.docx(27页珍藏版)》请在三一办公上搜索。

1、深入理解Java虚拟机学习笔记JVM的自动内存管理机制 一 如何划分JVM内存 JVM所管理的内存在运行时会被分为这样几个数据区:虚拟机栈区,堆区,方法区,本地方法栈,程序计数器。 程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,每条线程都需要有一个独立的程序计数器,各条线程之间程序计数器互不影响,独立存储,是线程隔离的。程序计数器所在的内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 虚拟机栈,线程私有,它的生命周期与线程相同。虚拟机栈区描述的是Java方法执行内存模型:每个方法在执行的同时都会创建一个栈帧用于存

2、储局部变量、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 局部变量表存放了8种基本数据类型、对象的引用和returnAddress。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。 本地方法栈,作用与虚拟机栈区是相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。 堆,Java堆,也称GC堆,是最大的一块,是被线程共享的区域,在虚拟机启动时创建。所有类的实例和数组都是在堆上分配内存的,堆内存由存活和死亡的对象,空闲碎片区

3、组成,对象所占的堆内存是由自动内存管理系统回收。 从内存回收角度来看,Java堆还可以细分为新生代和老年代;甚至还可以分为Eden空间、 From Survivor空间、To Survivor空间等。 从内存分配角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。 Java堆可以处于物理上不连续的内存中,只要逻辑上连续即可。 方法区在JVM中也是一个非常重要的区域,在HotSpot虚拟机上,方法区被称为“永久代”。虽然Java虚拟机规范把方法区描述为堆区的一个逻辑部分,但还是要区分来对待。方法区用于存储已被JVM加载的类信息、类变量、常量、即时编译器编译后的代码等数据。虽然方法

4、区中有些数据是线程隔离的,但是编译器编译后的代码等数据,是线程共享的。 除了和Java堆一样不需要连续的内存和可以固定大小或者可扩展外,还可以选择不实现垃圾收集。但不并非方法区就不要内存回收了,方法区的内存回收只要针对常量池的回收和对类型的卸载。 运行时常量池,是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。 二 对象的创建 在语言层面上,创建对象通常仅仅是一个new关键字而已。但在虚拟机中,对象的创建过程大致分为以下四步: 第一步,检查类加载。虚拟机遇到一条new指令时,首先需要去检查这个指令的参

5、数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 第二步,分配内存。在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定。分配方式大致有两种:指针碰撞和空闲列表。除了考虑如何划分可用空间之外,还需要考虑在并发的情况下的线程安全。解决方案有两种:一种是对分配空间的动作进行同步处理;另外一种本地线程分配缓冲。 第三步,内存空间初始化。如果使用TLAB,这一过程可以提前至TLAB分配时进行。 第四步,必要的设置。初始化后,虚拟机要对对象进行必要的设置,例如这个对象

6、是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。 上面的工作完成后,从虚拟机角度来看,一个新的对象已经产生了,但在程序员的角度来看,对象的创建才刚刚开始,init方法还没有执行,所有字段都还为零。所以,一般来说,执行new指令后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个可用的对象才算完全产生出来。 三 对象的内存布局 在HotSpot虚拟机中,对象在内存中存储的布局分为3块:对象头、实例数据和对齐填充。 对象头包括两部分信息,一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有

7、的锁等;另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型字段内容。无论是从父类继承下来,还是在子类中定义的,都需要记录。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中的顺序的影响。 对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotspotVM的自动内存管理系统要求对象起始地址必须是8字节的整数倍。 四 对象的访问定位 建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于refe

8、rence类型在Java虚拟机规范里面只规定了一个指向对象的引用地址,并没有定义这个引用应该通过那种方式去定位,访问到Java堆中的对象位置,因此不同的虚拟机实现的访问方式可能不同,主流的方式有两种:使用句柄和直接指针。 句柄访问方式:Java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。 指针访问方式:reference变量中直接存储的就是对象的地址,而Java堆对象一部分存储了对象实例数据,另外一部分存储了到对象类型数据的指针。 这两种访问对象的方式各有优势,使用句柄访问方式最大好处就是referen

9、ce中存储的是稳定的句柄地址,在对象移动时只需要改变句柄中的实例数据指针,而reference不需要改变。使用指针访问方式最大好处就是速度快,它节省了一次指针定位的时间开销,就Hotspot虚拟机而言,它使用的是第二种方式(直接指针访问)。 五 JVM的内存配置参数 -XX:+ 启用选项 -XX:- 不启用选项 -XX:= 将option参数的值设置为value 堆设置 -Xms :初始堆大小 -Xmx :最大堆大小 -Xmn:新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor

10、,即 90%。 -XX:NewSize=n :设置年轻代大小 -XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 -XX:PermSize=n 永久代(方法区)的初始大小 -XX:MaxPermSize=n :设置永久代最大大小 -Xss 设定栈容量;对于HotSpot来说,虽然-Xoss参数存在,但

11、实际上是无效的,因为在HotSpot中并不区分虚拟机和本地方法栈。 -XX:PretenureSizeThreshold 可以设置进入老生代的大小限制 -XX:MaxTenuringThreshold=n垃圾最大年龄如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率 该参数只有在串行GC时才有效。 收集器设置 -XX:+UseSerialGC :设置串行收集器 -XX:+UseParallelGC

12、:设置并行收集器 -XX:+UseParallelOldGC :设置并行年老代收集器 -XX:+UseConcMarkSweepGC :设置并发收集器 垃圾回收统计信息 -XX:+PrintHeapAtGC 打印GC的heap详情 -XX:+PrintGCDetails 打印GC详情 -XX:+PrintGCTimeStamps 打印GC时间信息 -XX:+PrintTenuringDistribution 打印年龄信息等 -XX:+HandlePromotionFailure 老年代分配担保 并行收集器设置 -XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数

13、。并行收集线程数。 -XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间 -XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) 并发收集器设置 -XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况。 -XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。 其他 -XX:PermSize=10M和-XX:MaxPermSize=10M限制方法区大小。 -XX:MaxDirectMemorySize=10M指定DirectMemor

14、y容量,如果不指定,则默认与JAVA堆最大值一样。 -XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便时候进行分析。 六 JVM的堆内存 简单的来说Java的堆内存分为两块:permant space和 heap space。 持久代/方法区:主要存储结构信息的地方,比如方法体,同时也是存储静态变量,以及静态代码块的区域,构造函数,常量池,接口初始化等等 。与垃圾收集器要收集的Java对象关系不大。 而heapspace分为新生代和年老代。 新生代:对象被创建时的对象通常被放在新生代的Eden区,经过一次GC收集后

15、,存活下来的会被复制到survivor区(一个满了,就全部移动到另外一个大的中,但要保证其中一个survivor为空),经过一定的Minor GC还活着的对象会被移动到年老代。 年老代:就是上述新生代移动过来的和一些比较大的对象。FullGC是针对年老代的回收 新生代的垃圾回收叫 Minor GC, 年老代的垃圾回收叫 Full GC。 在年轻代中经历了多次垃圾回收后仍然存活的对象,就会被复制到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过一次 Minor GC后仍然存活,并且能被Survi

16、vor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor空间中每熬过一次 Minor GC,年龄就增加1岁,当它的年龄增加到一定程度,就将会被晋升到年老代中。对象晋升年老代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。 年老代溢出原因:循环上万次的字符串处理、创建上千万个对象、在一段代码内申请上百M甚至上G的内存。 持久代溢出原因 :动态加载了大量Java类而导致溢出。 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 Xms、-Xmx 来指定。 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的

17、值为 1:2 ( 该值可以通过参数 XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。 JVM 每次只会使用 E

18、den 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。 七 垃圾回收 垃圾回收主要针对的是堆区的回收,因为栈区的内存是随着线程而释放的。垃圾回收线程在jvm中优先级相当相当低。数组和对象在没有引用变量指向它的时候,才变为垃圾,不能再被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走。这也是 Java 比较占内存的原因。 在Java虚拟机一书中明确讲了,释放掉被占据的内存空间是由GC完成,但是程序员无法明确强制其运行,该空间在不被引

19、用的时候不一定会立即被释放,这取决于GC本身,无法由程序员通过代码控制。垃圾收集器程序开发者只能建议JVM进行回收,但何时回收,回收哪些,程序员不能控制。垃圾回收机制只是回收不再使用的内存,如果程序有严重BUG,照样内存溢出。所以垃圾回收机制不能保证Java程序不会出现内存溢出。 死对象和活对象 在垃圾回收器进行垃圾回收前,第一件事情就是确定哪些对象还“存活”,哪些已经“死去”。判断对象是否还存活的算法主要是两种:引用计数算法和可达性分析算法。 引用计数算法。基本思想:给对象添加一个引用计数器,每当有一地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可

20、能再被使用的。这种算法实现简单,判断效率也很高,但主流的Java虚拟机没有选用引用计数算法来管理内存,主要原因是它很难解决对象之间相互循环引用的问题。 可达性分析算法。基本思想:通过一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用。可当做GC Roots的对象:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈JNI引用的对象。 无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象引用链是否可达,判定对象是否存活都与

21、“引用”有关。引用关系:强引用软引用弱引用虚引用。 垃圾收集算法 “标记-清除”算法。先标记后清除。不足:一,效率问题;二,空间问题,产生大量不连续的内存碎片。 复制算法。为了解决效率问题,出现了复制算法。将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉。实现简单,运行高效,但代价就是把内存缩小为原来的一半来使用。一般这种收集算法主要用来回收新生代,因为新生代的对象绝大多数是“朝生夕死”的,存活时间短,所以并不需要按1:1的比例来划分空间,而是将内存分为一块较大的Eden空间和两块较小的Surv

22、ivor空间,每次使用Eden空间和其中一块Survivor空间。当回收时,将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden空间和刚才用过的Survivor空间。Hotspot虚拟机默认的Eden和Survivor的大小比例是8:1。 “标记-整理”算法。在老年代中因为对象存活率高、没有额外空间对它进行分配担保,所以一般在老年代使用这种收集算法。 分代收集算法。没什么新的思想,只是上面两种算法的在不同年代的使用。 垃圾收集器 1.Serial New/Serial Old Serial/Serial Old收集器是最基本最古老的收集器,它

23、是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial New收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。 2.Parallel New Parallel New收集器是Serial收集器的多线程版本(参照Serial New),使用多个线程进行垃圾收集。除了Serial收集器外,目前只有Parallel New可以与CMS收集器配合工作。 3.Parallel Scavenge Parallel Scavenge收集器是

24、一个新生代的多线程收集器,它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。 4.Parallel Old Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和Mark-Compact算法。 5.CMS CMS收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。CMS运行的过程:初始标记、并发标记、重新标记、并发清除。 6.G1 G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CP

25、U、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。G1的运行过程:初始标记、并发标记、最终标记、筛选回收。 Hotspot虚拟机的垃圾收集器 虚拟机执行子系统 一 类文件结构 实现语言与平台无关的基础是虚拟机和字节码存储格式。 Class文件是一组以8位为基础单位的二进制流,各数据项严格按顺序排列其中,中间没有添加任何分隔符。根据Java虚拟机规范的规定,CLASS文件格式采用一种类似C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。 无符号数属于基本的数据类型,以u1,u2,u4,u8来分别表示一个字节,两个字节,四个字节和8个字节的无符号数

26、,无符号数用来描述数字,索引引用,数量值或按照UTF8编码构成字符串数。 表是由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性的以_info结尾,表用于描述有层次关系的复合结构的数据。整个CLASS文件本质上也是一张表。 Class类文件格式按如下顺序排列: 类型 u4 u2 u2 u2 cp_info u2 u2 u2 u2 u2 u2 field_info u2 method_info u2 attribute_info 名称 magic(魔数) minor_version(次版本号) major_version(主版本号) constant_pool_count(常量个

27、数) constant_pool(常量池表) access_flags(类的访问控制权限) this_class(类名) super_class(父类名) interfaces_count(接口个数) interfaces(接口名) fields_count(字段个数) fields(字段表) methods_count(方法的个数) methods(方法表) attributes_count(属性的个数) attributes(属性表) 数量 1 1 1 1 constant_pool_count-1 1 1 1 1 interfaces_count 1 fields_count 1 met

28、hods_count 1 attributes_count 魔数:0xCAFEBABE。用来确定这个文件是否是Class文件,进行身份识别。 紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号,第7 和第8个字节是主版本号。Java的版本号是从45开始,每个JDK版本发布主版本号加1,高版本号的JDK可以向下兼容以前版本的Class文件,但不能运行版本高于自己的CLASS文件。 紧接着主次版本号之后的是常量池入口,常量池中常量的数量不同,用常量池计数器代表常量池容量的计数值。计数器从1而不是0开始,例如当常量池容量为0x0016,十进制为22,代表有21个常量。索引

29、为121。没有使用0索引是因为在后面某些指向常量池的索引可以通过0索引表示不引用任何一个常量池项目的意思。 常量池中主要存放两大类常量:字面量和符号引用。 字面量的例子有文本字符串,被声明为final的常量值等。 符号引用包含三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。 常量池后面紧接着是类的访问权限控制符,类以及父类的全限定名,以及接口的个数,之后是接口的全限定名,全限定名都是指向常量池的符号引用。 再下面就是字段表集合、方法表集合和属性表集合。 二 虚拟机类加载机制 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行验证、转换解析和初始化,最终形成可以被

30、虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 在Java语言里,类型的加载、连接和初始化的过程都是在程序运行期间完成的。 一个类的生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。部分解析可以在初始化开始之后再开始,这样可以支持java的运行时绑定。 Java虚拟机规范中并没有强制规定什么情况下需要开始类加载过程的第一个阶段:加载,这个交给虚拟机自由把握,但却严格规定了有且只有5种情况必须立即对类进行初始化: 1)遇到new创建实例,getstatic获取类的静态字段,putstatic设置类的静态字段,invokestatic调用类的静态方法 2)用java.la

31、ng.reflect包方法对类进行反射调用的时候,如果这个类没有初始化过,那么先触发其初始化 3)初始化一个类的时候,如果父类没有进行初始化,那么必须先触发其父类的初始化 4)当虚拟机启动的时候,需要指定一个执行的主类,虚拟机会先初始化这个主类 5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化。 这5中场景中的行为称为对一个类进行主动引用。除此之外,所有引用

32、类的方法都不会触发初始化,称为被动引用。例如:通过子类引用父类的静态字段,不会导致子类的初始化;用new关键字创建数组,通过数组定义来引用类,不会触发相应的类初始化:AClass a=new AClass10; 调用一个类的静态常量也不会触发该类的初始化,因为调用类在编译阶段就已经把常量转化为对自己的常量池的引用。 接口的初始化过程与类的初始化过程的显著区别:当一个类初始化时,要求其父类全部都已初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只要在真正使用到父接口的时候才会初始化。 三 类加载的过程 类加载的过程:加载、验证、准备、解析和初始化。 加载阶段是整个类加载阶

33、段的第一个阶段,在加载阶段主要完成3件事情: 1)通过类的全限定名来回去定义此类的二进制流 2)将这个二进制流所代表的静态存储结构转化为方法区的运行时数据结构 3)在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。 验证阶段,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致上完成以下4种验证:Class文件格式的验证,元数据的验证,字节码的验证,符号引用验证。 1)Class文件格式验证为了验证是否符合Class文件的格式,并且能被当前版本的虚拟机处理。这个阶段是验证是基于二进制字

34、节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流; 2)元数据验证是为了对类的元数据信息进行语义校验,保证不存在不符合java语义规范的元数据信息; 3)字节码验证主要是通过数据流和控制流,对类的方法体中的字节码进行校验分析; 4)符号引用验证主要是为了给解析阶段符号引用转化为直接引用做准备,对类自身以外的信息进行匹配性校验。 准备阶段,正式为类变量分配内存并设置初始值。这些变量使用的内存都将在方法区中进行分配。说明两点:这个阶段仅对类变量分配内存,不对实例变量分配,实例变量将会在对象实例

35、化时随着对象一起分配在Java堆中;这里的初始值“通常情况”下是数据类型的零值。 “通常情况”:public static int a = 123;/类变量在准备阶段初始化的值为0,而在初始化阶段,在构造方法中会把a的值初始化为123。 “不通常情况”:public static final int a = 123;/用final修饰的类变量在准备阶段,会把a的值初始化为123。 解析阶段,把虚拟机在常量池中的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用。 初始化阶段,是执行类构造器方法的过程,会自动收集类中的所有

36、类变量以及静态语句块,在初始化方法的时候,虚拟机会自动调用父类的方法,接口的方法可以到使用的时候在去初始化,虚拟机会保证方法在多线程环境先被正确的加锁和同步。还有一个方法,这个方法是实例构造器,在创建实例的时候会被调用并且初始化。 四 Java类加载器 对于任意一个类,都需要根据加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间。通俗讲,判定两个类是否相等时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器加载的。 从Java虚拟机角度来讲,只存在两种不同的加载器:一种是启动类加载器,这个类加载器使用C+语言实现,是虚拟机的一

37、部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。 从Java开发人员的角度来看,类加载器还可以划分得更细致一些,绝大部分Java程序都会使用一下3种系统提供的类加载器:启动类加载器、扩展类加载器、应用程序类加载器。 1)Bootstrap ClassLoader 负责把存放在$JAVA_HOME中jre/lib目录中的核心库和基础库加载到虚拟机内存中。由C+实现,不是ClassLoader子类,无法被Java程序直接引用。 2)Extension ClassLoader 负责加载java平台

38、中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext或-Djava.ext.dirs指定目录下所有类库。开发者可以直接使用扩展类加载器。 3)AppClassLoade 负责加载用户类路径classpath中指定的类库。开发者可以直接使用扩展类加载器。 4)User ClassLoader 属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。 类加载器双亲委派模型 类加载器使用双亲委派模型,这样当要加载一个类时,首先查找这个类是否已经被加载过,如果没有,那么类加载器会把这个类委派给这个加载器

39、的父类去进行加载,如果父类不能加载,那么再自己尝试加载。加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrapClassLoader逐层检查,只要某个classloader已加载过就视为已加载此类,保证此类只被所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。 ClassLoader加载类用的是全盘负责委托机制。所谓全盘负责,即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所有 Class也由这个classloader负责载入,除非是显式的使用另外一个cl

40、assloader载入。所以,当我们自定义的classloader加载成功了pany.MyClass以后,MyClass里所有依赖的class都由这个classLoader来加载完成。 五 字节码执行 栈帧,是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区的虚拟机栈的栈元素。栈帧中从上到下依次存储了方法的局部变量表,操作数栈,动态链接和方法返回地址等信息。每个方法从调用开始到调用结束,都对应着一个栈帧从入栈到出栈的过程。一个栈帧需要分配多大的内存,在编译程序代码期间就确定了。一个线程中只有栈顶的栈帧才是有效的,称为当前栈帧,这个栈帧所关联的方法就是当前方法。 局部变量表,

41、是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽为最小单位。为了节省栈帧空间,Slot是可以重用,但可能会影响到系统的垃圾收集行为。在方法内的局部变量定义了但没有赋初始值是不能使用的。一般编译器会检查到并提示这一点。 操作数栈,也称为操作栈,是一个后入先出栈。操作数栈中元素的数据类型必须要与字节码指令的序列完全一致。 动态链接,是在运行期间把符号引用转化为直接引用的过程,相对于静态解析。 方法的返回地址,方法返回有两种类型,一种是正常完成出口,另一种是异常完成出口,方法退出的过程等同于栈帧出栈,因此栈帧出栈的时候可能执行的操作有:恢复上层调用方法的局部变

42、量表和操作数栈,把返回值压入调用方法的操作数栈中,调整PC计数器的值以执行方法调用指令的后一条指令等。 方法调用,不等同方法执行,唯一的任务就是确定被调用方法的版本,不涉及方法内部的具体运行过程。Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存入口地址。 静态方法、实例构造器、私有方法、父类方法和final方法,这些方法叫做非虚方法。这类非虚方法的调用叫做解析。解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期间再去完成。 Hu

43、man man = new Man; /Human是变量的静态类型 Man是变量的实际类型 静态分派:所有依赖静态类型来定位方法执行版本的分派动作都称为静态分派,静态分派的典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。 动态分派:在运行期根据实际类型确定方法的执行版本的分派过程称为动态分派,动态分派的典型应用就是方法重写。 宗量:方法的接受者和方法的参数统称为方法的宗量。 单分派:根据一个宗量对目标方法进行选择 多分派:根据多个宗量对目标方法进行选择 Java是一种静态多分派,动态单分派语言。 类的方法区会保存一张虚方法表,存放方法的实际入口地

44、址,如果没有重写父类的方法,那么入口与父类的一样,如果重写了父类的方法,那么方法的入口地址指向自己的方法入口地址。方法表一般在类加载的连接阶段进行初始化,准备了类变量的初始值之后,虚拟机会把该类的方法表也初始化完毕,这是java实现动态分派方法。 方法执行,Java虚拟机的执行引擎在执行Java代码的时候都有解释执行和编译执行两种选择。基于栈的解释器执行 在Class文件格式与执行引擎这部分中,用户的程序能直接影响的内容并不太多,Class文件以何种格式存储,类型何时加载,如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序无法进行干预。能通过程序进行操作的,主要是字节

45、码生成和类加载器这两部分。 程序编译 一 编译期 Java语言的“编译期”其实是一段不确定的操作过程,因为它可能是指一个前端编译器把*.java文件转变成*.class文件的过程;也可能是指虚拟机的后端运行期编译器把字节码转变成机器码的过程。Java的即时编译器在运行期的优化过程对于程序运行来说更重要,而前端编译器在编译期的优化过程对于程序编码来说关系更加密切。 Javac编译器,是Sun公司的前端编译期,它本身就是一个由Java语言编写的程序。从Javac的代码来看,编译过程大致可以分为3个过程:解析与填充符号表过程、插入式注解处理器的注解处理过程和分析与字节码生成过程。 Javac的编译过

46、程 过程1.1:解析 过程1.2:填充到符号表 过程2:执行注解处理 过程3.1:标注检查 过程3.2:数据及控制流分析 过程3.3:解语法糖 过程3.4:字节码生成 二 运行期 前面Javac这类将Java源代码转变成字节码的编译器一般称为“前端编译器”,是因为他只完成了从程序到中间字节码的生成,而在此之后,还有一组在虚拟机内部的“后端编译器”完成了从字节码生成机器码的过程,这类编译器一般称作为即时编译器或JIT编译器,工作在程序的运行期。这类编译器的编译速度及编译结果的优劣,是衡量一个虚拟机性能很重要的指标。 Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码的运行特别

47、频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些热点代码编译成与本地平台相关的机器码,并进行各层次优化,完成这个任务的编译器就是即时编译器。 Hotspot虚拟机采用的是解释器与编译器并存的架构。解释器和编译器两者各有优势:当程序需要快速启动和执行的时候,解释器可以首先发挥作用,省去编译时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率。 Hotspot虚拟机中内置了两个即时编译器,分别称为Client Compiler 和Server Compiler。用C1可以获取更高的

48、编译速度,用C2可以获取更好的编译质量。程序使用哪个编译器,取决虚拟机运行模式,虚拟机会根据自身版本和宿主机器的硬件性能自动选择运行模式,用户也可以自己使用参数强制指定运行模式。 在运行过程中能被即时编译器编译的“热点代码”有两类:被多次调用的方法;被多次执行的循环体。 判断一段代码是不是热点代码,需不需要触发即时编译,这样的行为称为热点探测。主要的热点探测判定方式:基于采样的热点探测和基于计数器的热点探测。 基于计数器的热点探测方式为每个方法准备了两个计数器:方法调用计数器和回边计数器。 编译优化技术:公共子表达式消除、数组范围检查消除、方法内联、逃逸分析 Javac字节码的编译器与虚拟机内的JIT编译器的执行过程合并起来其实就等同于一个传统编译器所执行的编译过程。 高效并发 一

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 生活休闲 > 在线阅读


备案号:宁ICP备20000045号-2

经营许可证:宁B2-20210002

宁公网安备 64010402000987号