深入理解Java虚拟机(一)

来源:互联网 发布:skycc软件 编辑:程序博客网 时间:2024/06/05 05:35

一、Java体系结构

  •  Java与生俱来的优点

    1.     OOA——面向对象的分析;OOD面向对象的设计;OOP——面向对象的编程

    2.     Java的5项重要优势:体系结构中立;安全性优越;多线程;分布式;丰富的第三方开源组件。

    3.     体系结构中立:Java之所以能够实现“一次编译,处处运行”,是因为源代码的默认编译结果并非是可执行代码(本地机器指令),而是具有平台通用性的字节码。尽管不同平台的java虚拟机内部实现机制不尽相同,但是它们共同解释出来的字节码却是一样的。

    4.     安全性:因为java只能够运行在java虚拟机中,这与实际的物理宿主环境之间是相互“隔离”的,因此可以禁止很多不安全的因素,有助于防止错误的发生,增强程序的可靠性。同时java的部分语法限制也在某种意义上保障了程序的安全:废弃指针、自动内存管理、边界检查、类型转换检查、线程安全机制和物理环境访问限制等。

  • 语法结构和对象模型

    1.     Java继承了C语言的语法结构,并改编了C++语言的对象模型。所以Java是C++衍生出来的一种语言。

    2.     面向对象之所以目前大行其道,其中最关键的因素在于在系统构建复杂化的当下,允许开发人员以面向对象式思维设计出更具复用性、维护性、扩展性和伸缩性的应用程序。

    3.     开发人员可以在程序中直接使用new关键字创建一个对象,并返回当前对象的一个引用。Java中的引用操作绝对不等价于C++中的指针,因为引用类型的变量持有的仅仅只是一个引用而已而非实际值,也就是说开发人员并不能在程序中直接与对象实例打交道,而必须通过引用进行“牵引”。

  • 历史版本                                                                                                                           


                                               


  • Java重要概念

    1.     字节码:任何编程语言的编译结果满足并包含java虚拟机的内部指令集、符号表以及一些其他辅助信息,它就是一个有效的字节码文件,就能够被虚拟机所识别并装在运行。在大部分情况下,字节码更多是存储在本地磁盘文件中,比如后缀名“.class”的文件。每一个字节码文件都对应着全局唯一的一个类或者接口定义的信息,但是类和接口不一定只能存储在文件里,还可以通过类装载器直接运行时生成。

    2.     API——应用程序编程接口:包含的就是Java的基础类库集合,它提供一套访问主机系统资源的标准方法。

    3.     Java虚拟机:它是由一组规范所定义出的抽象计算机。主要任务就是负责将字节码封装到内部,解释/编译为对应平台上的机器指令执行。                                                                                                                                                       


  • Java技术的新特性

    1.     Java模块化:开发人员在构建大型系统时,能够将系统中的每一个功能模块进行独立的开发和物理部署。模块化的目的就是为了将系统中原本耦合的逻辑进行分解,以此满足各个模块之间的独立,并定义一种标准化的接口契约来进行相互之间的通信。

    2.     OSGI技术:Java动态化模块化系统的一系列规范。OSGi一方面指维护OSGi规范的OSGI官方联盟,另一方面指的是该组织维护的基于Java语言的服务(业务)规范。简单来说,OSGi可以认为是Java平台的模块层。                               


    3.     语言无关性:在Java虚拟机平台上运行非Java语言编写的程序。Java虚拟机根本不关心运行在其内部的程序到底是使用何种编程语言编写的,只关心“字节码”文件。

    4.     使用Fork/Join框架实现多核并行:http://www.2cto.com/kf/201409/330226.html 

    5.     丰富的语法特性

    6.     过渡到64位虚拟机:最大优势就是可以访问大内存。32为虚拟机的最大可用内存被限定在了4GB。


    二、Java内存区域与内存溢出异常

  • 运行时内存区域:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。                                                                                                                                                  


  • 运行时内存区域——程序计数器

    1.     程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

    2.     Java虚拟机的多线程是通过线程的轮流切换,为了线程切换后能回复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。

    3.     如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Nactive方法,这个计数器值为空。


  • 运行时内存区域——Java虚拟机栈

    1.     Java虚拟机栈也是线程私有的,它的生命周期与线程相同。

    2.     虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(栈帧是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

    3.     每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

    4.     局部变量表存放了编译器可知的各种基本数据类型(int,boolean,byte,char,short,float,long,double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法所需的帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

  • 运行时内存区域——本地方法栈:本地方法栈与虚拟机栈所发挥的作用是非常相似的,区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

  • 运行时内存区域——Java堆

    1.     Java堆尸Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

    2.     Java堆区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。(所有的对象实例以及数组都要在堆上分配)。

    3.     Java堆尸垃圾收集器管理的主要区域。可以细分为:新生代和老年代;再细致一点有Eden空间、From Survivor空间、To Survivor空间等

    4.     Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以是可扩展的。

  • 运行时内存区域——方法区

    1.     方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    2.     Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

    3.     运行时常量池是方法区的一部分,用于存放编译期间生成的各种字面量和符号引用。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

  • 对象的创建

    1.     虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

    2.     在类加载检查通过后,接下来虚拟机将为新生对象分配内存。可以使用“指针碰撞”和“空闲列表”方法进行分配。

    3.     对象创建是分配内存还需要考虑并发情况下的线程安全问题。有两种解决方法,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方法保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲。

    4.     虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。

    5.     执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

  • 对象的内存布局

    1.     对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对象填充。

    2.     对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分是类型指针,即对象指向它的类元数据指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    3.     示例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

    4.     对齐填充并不是必然存在的,也没有特别的含义。虚拟机要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

  • 对象的访问定位

    1.     Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,所以对象访问方式取决于虚拟机的实现。目前主流的访问方式有:使用句柄和直接指针两种。

    2.     使用句柄:Java堆将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。                                                                                                                                


    3.     直接指针:那么Java堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,而reference中存储的直接就是对象地址。


    4.     使用句柄最大好处就是reference中存储的是稳定的句柄指针,在对象被移动时会改变句柄中的实例数据指针而reference本身不需要修改;使用直接指针访问方式最大的好处就是速度更快,它节省了一次指针定位的时间开销。

  • Java堆溢出

    1.     Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

    2.     -Xms参数设置堆的最小值,-Xmx参数设置堆的最大值


  • 虚拟机栈和本地方法栈溢出

    1.     通过-Xss参数设定栈容量

    2.     如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。其实这两种异常说明的是同一种情况。

  • 方法区和运行时常量池溢出

    1.     JDK1.7开始逐步“去永久代”事情。在JDK1.6及之前的版本中,由于常量池分配在永久代内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制常量池的容量。

    2.     想要在方法去出现内存溢出,只要运行时产生大量的类去填满方法区即可。

    3.     方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件比较苛刻。在经常动态生成大量CLASS的应用中,需要特别注意类的回收状况。

  • 本机直接内存溢出:DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值一样。


    三、垃圾收集器和内存分配策略

  • 判断对象是否存活

    1.     引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。但是,至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

    1.  objA.instance=objB;

    2.  objB.instance=objA;

    3.  //AB互相引用着对方,导致AB的引用计数都不为0

    2.     可达性分析算法:通过一系列成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象是不可用的。                                                              


                                                                             GC Roots的对象包括:虚拟机栈(栈帧中的局部变量表)中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI引用的对象。

    3.     引用:当内存空间还足够时,能够保留在内存之中;如果内存空间在进行垃圾收集之后还是非常紧张,则可以抛弃的这些对象。将引用分为强引用,软引用,弱引用,虚引用。http://zhangjunhd.blog.51cto.com/113473/53092/                                                                                     


    4.     真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过(也就是说一个对象只能调用一次finalize方法,对象也因此只有一次可以拯救自己的机会),虚拟机将这两种情况都视为“没有必要执行”。

    5.     回收方法区:方法区是人们常说的永久代,永久代的垃圾收集效率远低于堆区的新生代。永久代的垃圾收集主要回收两个部分——废弃常量和无用类。废弃常量较为简单,当常量池的常量没有被任何对象引用,则为废弃常量。而无用类需要满足的条件较为苛刻——


  • 垃圾收集算法

    1.     标记-清除法(Mark-Sweep):首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点——标记和清除两个过程的效率都不高;标记清除之后会产生大量不连续的内存碎片。


    2.     复制算法(Copying):将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存块用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。                                                                    现在商业虚拟机都采用这种收集算法来回收新生代,因为新生代的对象大部分朝生夕死,所以不需要分成1:1的空间,而是将内存分成一块较大的Eden空间和两块较小的Survivor空间(HotSpot虚拟机默认两者的大小比例为8:1)。                                    当回收时将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor空间,最后清理掉Eden和刚才用过的Survivor空间。如果另一块Survivor空间没有足够空间存放上一代新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。新生代使用该算法的原因——不用考虑内存碎片,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。                                                                                                                                                       


    3.     标记-整理算法(Mark-Compact):常用于老年代的回收。标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。                                                                   老年代不使用复制算法是因为在对象存活率高的时候要进行较多的复制操作,效率变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况。


    4.     分代收集算法(Generational Collection):没有什么新思想,只是根据对象存活周期的不同将内存划分为几块。一般是把JAVA堆分为新生代和老年代,根据各个年代的特点采用最适合的收集算法。                                                                                     新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,所以选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代因为对象存活率高、没有额外空间进行分配担保,就必须使用“标记-清理”或者“标记-复制”算法来进行回收。

  • 垃圾收集器:如果说手机算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。


  • Serial收集器

    1.     Serial收集器是最基本、发展历史最悠久的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯一选择。

    2.     Serial收集器是一个单线程收集器,不仅仅说明它只会使用一个CPU或者一条手机线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。


    3.     Serial收集器依然是虚拟机运行在client模式下的默认新生代收集器。它的优点在于——简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

  • ParNew收集器

    1.     ParNew收集器其实就是Serial收集器的多线程版本。


    2.     ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中一个重要原因是因为只有它能够与CMS收集器配合工作。在JDK1.5时期,推出了CMS收集器,是第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

  • Parallel Scavenge收集器

    1.     ParallelScavenge收集器是一个新生代收集器,使用复制算法的收集器,又是并行的多线程收集器。

    2.     ParallelScavenge收集器的特点是它关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

    3.     停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提高用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

    4.     ParallelScavenge收集器设置-XX:+UseAdaptiveSizePolicy参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或者最大吞吐量,这种调节方式成为GC自适应调节策略。这是Parallel Scavenge收集器与ParNew收集器最大的区别。

  • Serial Old收集器

    1.     Serial Old收集器是Serial收集器的老年代版本,使用“标记-整理”算法。主要是给在client模式下的虚拟机使用。

    2.     在server模式下,有两个主要用途:在JDK1.5以及之前的版中中与Parallel Scavenge收集器搭配使用;另一种是作为CMS收集器的后备预案。

  • Parallel Old收集器

    1.     Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK1.6中才开始使用。

    2.     在Parallel Old收集器出现之前,ParallelScavenge收集器一直处于比较尴尬的位置,因为它只能和Serial Old收集器配合工作,无法获得吞吐量最大化的效果。                                                                                                              


  • CMS收集器

    1.     CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的。CMS收集器首次实现了垃圾线程和用户线程并发进行。

    2.     CMS收集器回收过程分为四个步骤:初始标记;并发标记;重新标记;并发清除。

    3.     初始标记、重新标记这两个步骤仍然需要停止用户线程。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots Tracing(可达性分析)的过程。而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。


    4.     CMS收集器缺点:对CPU资源非常敏感——CMS虽然不会导致用户线程停顿,但是会因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量会降低,随着CPU数量的减少,CMS就会对应用程序影响很大;CMS收集器无法处理浮动垃圾——当进行并发清扫时会出现新的垃圾,CMS无法处理它们,只能等到下一次GC出现;大量的空间碎片产生——因为CMS收集器使用的是“标记-清除”算法。

  • G1收集器

    1.     G1收集器是当今收集器技术发展的最前沿成果之一,它是一款面向服务端应用的垃圾收集器,目的是替换掉CMS收集器。

    2.     G1收集器的特点:                                                                                                                          


    3.     G1之前的其他收集器进行收集的范围都是整个新生代和老年代,而G1不再是这样。它将Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的,他们都是一部分Region的集合。

    4.     G1收集器之所以能建立可预测的停顿模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

    5.     G1收集器运作的步骤:初始标记;并发标记;最终标记;筛选回收。


  • 理解GC日志

    1.    


    2.    



  • 对象优先在Eden分配

    1.     大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

    2.     当Eden区内存不够时,会进行一次GC操作,将存活的对象转移至Survivor区,如果Survivor区也没有足够空间存放则分配担保至老年代。

    3.    


  • 大对象直接进入老年代

    1.     虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

    2.     PretenureSizeThreshold参数支队Serial和ParNew两款收集器有效。

  • 长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor每熬过一次GC,年龄就增加1岁,当它年龄增加到一定程度(默认15岁),将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数       -XX:MaxTenuringThreshold设置。

  • 动态对象年龄判定:虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到要求的年龄。


    四、字节码文件

  • 字节码结构组成比较特殊,其内部并不包含任何的分隔符区分段落,所以无论是字节顺序、数量都是有严格规定的。所有16位、32位、64位长度的数据都将构造成2个、4个、8个8位字节单位来表示。多字节数据项总是按照big-endian顺序(高位字节在地址最低位,低位字节在地址最高位)来进行存储的,也就是说,一组8位字节单位的字节流组成了一个完整的字节码文件。

  • 字节码文件的内部组成结构

    1.     字节码文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个字节码文件本质上就是一张表。


    2.     magic:一个有效的字节码文件的前4个字节为0xCAFEBABE,也被称为魔术。魔术就是JVM用于校验所读取的目标文件是否是一个有效且合法的字节码文件。

    3.     minor_version(此版本号)和major_version(主版本号):比如字节码文件的主版本号为M,次版本号为m,那么这个字节码文件的版本号就被确定为M.m。随着JAVA技术的不停更新,版本号会不断的改变。对于JVM而言,字节码文件中的版本号确定了特定的字节码文件格式,通常只有给定主版本号和一系列次版本号之后,JVM才能够读取一个字节码文件。

    4.     constant_pool_count(常量池计数器)和constant_pool(常量池):                                                          


                                                              当JAVA虚拟机在运行时,从常量池中获取出对应的符号引用,并经过解析阶段将这些符号引用全部转换为直接引用后,JAVA虚拟机才能正常使用。常量池列表中的每一个常量项其实都是一个表,JAVA7一共包含14种类型不尽相同的常量项。


                                                       将该常量池的每一项转换为对应的字节码,如:第一项#1对应的常量池为class,标志位为7,则字节码的标志位为0x07,该常量池所需的结构还有name_index,指向了UTF-8常量池的第二个常量。                                                                                                                      


    5.     access_flags(访问标志):主要用于表示某个类或者接口的访问权限。                                                                        


    6.     this_class(类索引)和super_class(超类索引):类索引和超类索引各自会通过索引指向常量池列表中一个类型为CONSTANT_class_info的常量池。用来获取当前类和超类的全限定名。如果超类是Object则父类索引的值为0。

    7.     interface_count(接口计数器)和interfaces(接口表):接口计数器用于表示当前类或者接口的直接超类接口数量。而接口表实际上是一个数组集合,包含当前类或者接口在常量池列表中直接超类接口的索引集合,通过这个索引即可确定当前类或者接口的超类接口的全限定名。

    8.     fields_count(字段计数器)和fields(字段表):字段计数器用于表示一个字节码文件中的field_info表总数,也就是一个类中类变量和实例变量的数量总和。而字段表实际上则是一个数组集合,字段表中的每一个成员都必须是一个field_info的数据项。filed_info用于表示一个字段的完整信息。字段表中所包含的字段信息仅限于当前类或者接口中的所属字段,并不包含继承超类后的字段信息

    9.     methods_count(方法计数器)和methods(方法表):方法计数器用于表示一个字节码文件中的method_info表总数。而方法表实际上则是一个数组集合,方法表中的每一个成员都必须是一个method_info的数据项。method_info用于表示当前类或者接口中某个方法的完整描述。方法表中所包含的方法信息仅限于当前类或者接口中的所属方法,并不包含继承超类后的方法信息。

    10. attributes_count(属性计数器)和attributes(属性表):属性计数器用于表示一个字节码文件中的method_info表总数。而属性表实际上则是一个数组集合,属性表中的每一个成员都必须是一个attribute_info的数据项。每一个attribute_info表的第一项都是指向常量池列表中CONSTANT_Utf8_info项的索引,该表给出了属性的名称。

  • 符号引用

    1.     符号引用由3种特殊字符串构成,分别是:全限定名、简单名称和描述符。在字节码文件中,使用全限定名可以用于描述类或者接口。而类或者接口中定义的字段、方法都会包含一个简单名称和描述符。

    2.     字节码文件中包含的所有类或者接口的名称,都是通过全限定名的方式进行表示的。CONSTANT_Class_info表中的name_index是一个指向常量池列表中一个类型为CONSTANT_Utf8_info常量项索引,通过这个索引值即可成功获取权限定名字符串。如,Object的权限定名为java.lang.Object,但在字节码文件中要变成java/lang/Object。

    3.     在字节码文件中,类或者接口所定义的所有字段名称和方法名称都是使用简单名称来进行存储的。简单名称中不能包含"."";"   "[" "/"等以ASCII和UNICODE字符的表示形式。

    4.     类或者接口中定义的字段和方法除了都会包含一个简单名称外,还会包含一个描述符,描述字段的称为字段描述符,描述方法的成为方法描述符。字段描述符用于描述字段类型,而方法描述符用于描述方法的返回值类型以及方法的参数数量、类型和参数顺序。                                                                                                                                                   






  • 作者:龙猫小爷
    链接:http://www.jianshu.com/p/62f9db4d1df3
    來源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    一、Java体系结构

  •  Java与生俱来的优点

    1.     OOA——面向对象的分析;OOD面向对象的设计;OOP——面向对象的编程

    2.     Java的5项重要优势:体系结构中立;安全性优越;多线程;分布式;丰富的第三方开源组件。

    3.     体系结构中立:Java之所以能够实现“一次编译,处处运行”,是因为源代码的默认编译结果并非是可执行代码(本地机器指令),而是具有平台通用性的字节码。尽管不同平台的java虚拟机内部实现机制不尽相同,但是它们共同解释出来的字节码却是一样的。

    4.     安全性:因为java只能够运行在java虚拟机中,这与实际的物理宿主环境之间是相互“隔离”的,因此可以禁止很多不安全的因素,有助于防止错误的发生,增强程序的可靠性。同时java的部分语法限制也在某种意义上保障了程序的安全:废弃指针、自动内存管理、边界检查、类型转换检查、线程安全机制和物理环境访问限制等。

  • 语法结构和对象模型

    1.     Java继承了C语言的语法结构,并改编了C++语言的对象模型。所以Java是C++衍生出来的一种语言。

    2.     面向对象之所以目前大行其道,其中最关键的因素在于在系统构建复杂化的当下,允许开发人员以面向对象式思维设计出更具复用性、维护性、扩展性和伸缩性的应用程序。

    3.     开发人员可以在程序中直接使用new关键字创建一个对象,并返回当前对象的一个引用。Java中的引用操作绝对不等价于C++中的指针,因为引用类型的变量持有的仅仅只是一个引用而已而非实际值,也就是说开发人员并不能在程序中直接与对象实例打交道,而必须通过引用进行“牵引”。

  • 历史版本                                                                                                                           


                                               


  • Java重要概念

    1.     字节码:任何编程语言的编译结果满足并包含java虚拟机的内部指令集、符号表以及一些其他辅助信息,它就是一个有效的字节码文件,就能够被虚拟机所识别并装在运行。在大部分情况下,字节码更多是存储在本地磁盘文件中,比如后缀名“.class”的文件。每一个字节码文件都对应着全局唯一的一个类或者接口定义的信息,但是类和接口不一定只能存储在文件里,还可以通过类装载器直接运行时生成。

    2.     API——应用程序编程接口:包含的就是Java的基础类库集合,它提供一套访问主机系统资源的标准方法。

    3.     Java虚拟机:它是由一组规范所定义出的抽象计算机。主要任务就是负责将字节码封装到内部,解释/编译为对应平台上的机器指令执行。                                                                                                                                                       


  • Java技术的新特性

    1.     Java模块化:开发人员在构建大型系统时,能够将系统中的每一个功能模块进行独立的开发和物理部署。模块化的目的就是为了将系统中原本耦合的逻辑进行分解,以此满足各个模块之间的独立,并定义一种标准化的接口契约来进行相互之间的通信。

    2.     OSGI技术:Java动态化模块化系统的一系列规范。OSGi一方面指维护OSGi规范的OSGI官方联盟,另一方面指的是该组织维护的基于Java语言的服务(业务)规范。简单来说,OSGi可以认为是Java平台的模块层。                               


    3.     语言无关性:在Java虚拟机平台上运行非Java语言编写的程序。Java虚拟机根本不关心运行在其内部的程序到底是使用何种编程语言编写的,只关心“字节码”文件。

    4.     使用Fork/Join框架实现多核并行:http://www.2cto.com/kf/201409/330226.html 

    5.     丰富的语法特性

    6.     过渡到64位虚拟机:最大优势就是可以访问大内存。32为虚拟机的最大可用内存被限定在了4GB。


    二、Java内存区域与内存溢出异常

  • 运行时内存区域:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。                                                                                                                                                  


  • 运行时内存区域——程序计数器

    1.     程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

    2.     Java虚拟机的多线程是通过线程的轮流切换,为了线程切换后能回复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。

    3.     如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Nactive方法,这个计数器值为空。


  • 运行时内存区域——Java虚拟机栈

    1.     Java虚拟机栈也是线程私有的,它的生命周期与线程相同。

    2.     虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(栈帧是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

    3.     每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

    4.     局部变量表存放了编译器可知的各种基本数据类型(int,boolean,byte,char,short,float,long,double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法所需的帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

  • 运行时内存区域——本地方法栈:本地方法栈与虚拟机栈所发挥的作用是非常相似的,区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

  • 运行时内存区域——Java堆

    1.     Java堆尸Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

    2.     Java堆区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。(所有的对象实例以及数组都要在堆上分配)。

    3.     Java堆尸垃圾收集器管理的主要区域。可以细分为:新生代和老年代;再细致一点有Eden空间、From Survivor空间、To Survivor空间等

    4.     Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以是可扩展的。

  • 运行时内存区域——方法区

    1.     方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    2.     Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

    3.     运行时常量池是方法区的一部分,用于存放编译期间生成的各种字面量和符号引用。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

  • 对象的创建

    1.     虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

    2.     在类加载检查通过后,接下来虚拟机将为新生对象分配内存。可以使用“指针碰撞”和“空闲列表”方法进行分配。

    3.     对象创建是分配内存还需要考虑并发情况下的线程安全问题。有两种解决方法,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方法保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲。

    4.     虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。

    5.     执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

  • 对象的内存布局

    1.     对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对象填充。

    2.     对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分是类型指针,即对象指向它的类元数据指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    3.     示例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

    4.     对齐填充并不是必然存在的,也没有特别的含义。虚拟机要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

  • 对象的访问定位

    1.     Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,所以对象访问方式取决于虚拟机的实现。目前主流的访问方式有:使用句柄和直接指针两种。

    2.     使用句柄:Java堆将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。                                                                                                                                


    3.     直接指针:那么Java堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,而reference中存储的直接就是对象地址。


    4.     使用句柄最大好处就是reference中存储的是稳定的句柄指针,在对象被移动时会改变句柄中的实例数据指针而reference本身不需要修改;使用直接指针访问方式最大的好处就是速度更快,它节省了一次指针定位的时间开销。

  • Java堆溢出

    1.     Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

    2.     -Xms参数设置堆的最小值,-Xmx参数设置堆的最大值


  • 虚拟机栈和本地方法栈溢出

    1.     通过-Xss参数设定栈容量

    2.     如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。其实这两种异常说明的是同一种情况。

  • 方法区和运行时常量池溢出

    1.     JDK1.7开始逐步“去永久代”事情。在JDK1.6及之前的版本中,由于常量池分配在永久代内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制常量池的容量。

    2.     想要在方法去出现内存溢出,只要运行时产生大量的类去填满方法区即可。

    3.     方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件比较苛刻。在经常动态生成大量CLASS的应用中,需要特别注意类的回收状况。

  • 本机直接内存溢出:DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值一样。


    三、垃圾收集器和内存分配策略

  • 判断对象是否存活

    1.     引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。但是,至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

    1.  objA.instance=objB;

    2.  objB.instance=objA;

    3.  //AB互相引用着对方,导致AB的引用计数都不为0

    2.     可达性分析算法:通过一系列成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象是不可用的。                                                              


                                                                             GC Roots的对象包括:虚拟机栈(栈帧中的局部变量表)中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI引用的对象。

    3.     引用:当内存空间还足够时,能够保留在内存之中;如果内存空间在进行垃圾收集之后还是非常紧张,则可以抛弃的这些对象。将引用分为强引用,软引用,弱引用,虚引用。http://zhangjunhd.blog.51cto.com/113473/53092/                                                                                     


    4.     真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过(也就是说一个对象只能调用一次finalize方法,对象也因此只有一次可以拯救自己的机会),虚拟机将这两种情况都视为“没有必要执行”。

    5.     回收方法区:方法区是人们常说的永久代,永久代的垃圾收集效率远低于堆区的新生代。永久代的垃圾收集主要回收两个部分——废弃常量和无用类。废弃常量较为简单,当常量池的常量没有被任何对象引用,则为废弃常量。而无用类需要满足的条件较为苛刻——


  • 垃圾收集算法

    1.     标记-清除法(Mark-Sweep):首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点——标记和清除两个过程的效率都不高;标记清除之后会产生大量不连续的内存碎片。


    2.     复制算法(Copying):将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存块用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。                                                                    现在商业虚拟机都采用这种收集算法来回收新生代,因为新生代的对象大部分朝生夕死,所以不需要分成1:1的空间,而是将内存分成一块较大的Eden空间和两块较小的Survivor空间(HotSpot虚拟机默认两者的大小比例为8:1)。                                    当回收时将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor空间,最后清理掉Eden和刚才用过的Survivor空间。如果另一块Survivor空间没有足够空间存放上一代新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。新生代使用该算法的原因——不用考虑内存碎片,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。                                                                                                                                                       


    3.     标记-整理算法(Mark-Compact):常用于老年代的回收。标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。                                                                   老年代不使用复制算法是因为在对象存活率高的时候要进行较多的复制操作,效率变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况。


    4.     分代收集算法(Generational Collection):没有什么新思想,只是根据对象存活周期的不同将内存划分为几块。一般是把JAVA堆分为新生代和老年代,根据各个年代的特点采用最适合的收集算法。                                                                                     新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,所以选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代因为对象存活率高、没有额外空间进行分配担保,就必须使用“标记-清理”或者“标记-复制”算法来进行回收。

  • 垃圾收集器:如果说手机算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。


  • Serial收集器

    1.     Serial收集器是最基本、发展历史最悠久的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯一选择。

    2.     Serial收集器是一个单线程收集器,不仅仅说明它只会使用一个CPU或者一条手机线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。


    3.     Serial收集器依然是虚拟机运行在client模式下的默认新生代收集器。它的优点在于——简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

  • ParNew收集器

    1.     ParNew收集器其实就是Serial收集器的多线程版本。


    2.     ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中一个重要原因是因为只有它能够与CMS收集器配合工作。在JDK1.5时期,推出了CMS收集器,是第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

  • Parallel Scavenge收集器

    1.     ParallelScavenge收集器是一个新生代收集器,使用复制算法的收集器,又是并行的多线程收集器。

    2.     ParallelScavenge收集器的特点是它关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

    3.     停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提高用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

    4.     ParallelScavenge收集器设置-XX:+UseAdaptiveSizePolicy参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或者最大吞吐量,这种调节方式成为GC自适应调节策略。这是Parallel Scavenge收集器与ParNew收集器最大的区别。

  • Serial Old收集器

    1.     Serial Old收集器是Serial收集器的老年代版本,使用“标记-整理”算法。主要是给在client模式下的虚拟机使用。

    2.     在server模式下,有两个主要用途:在JDK1.5以及之前的版中中与Parallel Scavenge收集器搭配使用;另一种是作为CMS收集器的后备预案。

  • Parallel Old收集器

    1.     Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK1.6中才开始使用。

    2.     在Parallel Old收集器出现之前,ParallelScavenge收集器一直处于比较尴尬的位置,因为它只能和Serial Old收集器配合工作,无法获得吞吐量最大化的效果。                                                                                                              


  • CMS收集器

    1.     CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的。CMS收集器首次实现了垃圾线程和用户线程并发进行。

    2.     CMS收集器回收过程分为四个步骤:初始标记;并发标记;重新标记;并发清除。

    3.     初始标记、重新标记这两个步骤仍然需要停止用户线程。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots Tracing(可达性分析)的过程。而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。


    4.     CMS收集器缺点:对CPU资源非常敏感——CMS虽然不会导致用户线程停顿,但是会因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量会降低,随着CPU数量的减少,CMS就会对应用程序影响很大;CMS收集器无法处理浮动垃圾——当进行并发清扫时会出现新的垃圾,CMS无法处理它们,只能等到下一次GC出现;大量的空间碎片产生——因为CMS收集器使用的是“标记-清除”算法。

  • G1收集器

    1.     G1收集器是当今收集器技术发展的最前沿成果之一,它是一款面向服务端应用的垃圾收集器,目的是替换掉CMS收集器。

    2.     G1收集器的特点:                                                                                                                          


    3.     G1之前的其他收集器进行收集的范围都是整个新生代和老年代,而G1不再是这样。它将Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的,他们都是一部分Region的集合。

    4.     G1收集器之所以能建立可预测的停顿模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

    5.     G1收集器运作的步骤:初始标记;并发标记;最终标记;筛选回收。


  • 理解GC日志

    1.    


    2.    



  • 对象优先在Eden分配

    1.     大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

    2.     当Eden区内存不够时,会进行一次GC操作,将存活的对象转移至Survivor区,如果Survivor区也没有足够空间存放则分配担保至老年代。

    3.    


  • 大对象直接进入老年代

    1.     虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

    2.     PretenureSizeThreshold参数支队Serial和ParNew两款收集器有效。

  • 长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor每熬过一次GC,年龄就增加1岁,当它年龄增加到一定程度(默认15岁),将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数       -XX:MaxTenuringThreshold设置。

  • 动态对象年龄判定:虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到要求的年龄。


    四、字节码文件

  • 字节码结构组成比较特殊,其内部并不包含任何的分隔符区分段落,所以无论是字节顺序、数量都是有严格规定的。所有16位、32位、64位长度的数据都将构造成2个、4个、8个8位字节单位来表示。多字节数据项总是按照big-endian顺序(高位字节在地址最低位,低位字节在地址最高位)来进行存储的,也就是说,一组8位字节单位的字节流组成了一个完整的字节码文件。

  • 字节码文件的内部组成结构

    1.     字节码文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个字节码文件本质上就是一张表。


    2.     magic:一个有效的字节码文件的前4个字节为0xCAFEBABE,也被称为魔术。魔术就是JVM用于校验所读取的目标文件是否是一个有效且合法的字节码文件。

    3.     minor_version(此版本号)和major_version(主版本号):比如字节码文件的主版本号为M,次版本号为m,那么这个字节码文件的版本号就被确定为M.m。随着JAVA技术的不停更新,版本号会不断的改变。对于JVM而言,字节码文件中的版本号确定了特定的字节码文件格式,通常只有给定主版本号和一系列次版本号之后,JVM才能够读取一个字节码文件。

    4.     constant_pool_count(常量池计数器)和constant_pool(常量池):                                                          


                                                              当JAVA虚拟机在运行时,从常量池中获取出对应的符号引用,并经过解析阶段将这些符号引用全部转换为直接引用后,JAVA虚拟机才能正常使用。常量池列表中的每一个常量项其实都是一个表,JAVA7一共包含14种类型不尽相同的常量项。


                                                       将该常量池的每一项转换为对应的字节码,如:第一项#1对应的常量池为class,标志位为7,则字节码的标志位为0x07,该常量池所需的结构还有name_index,指向了UTF-8常量池的第二个常量。                                                                                                                      


    5.     access_flags(访问标志):主要用于表示某个类或者接口的访问权限。                                                                        


    6.     this_class(类索引)和super_class(超类索引):类索引和超类索引各自会通过索引指向常量池列表中一个类型为CONSTANT_class_info的常量池。用来获取当前类和超类的全限定名。如果超类是Object则父类索引的值为0。

    7.     interface_count(接口计数器)和interfaces(接口表):接口计数器用于表示当前类或者接口的直接超类接口数量。而接口表实际上是一个数组集合,包含当前类或者接口在常量池列表中直接超类接口的索引集合,通过这个索引即可确定当前类或者接口的超类接口的全限定名。

    8.     fields_count(字段计数器)和fields(字段表):字段计数器用于表示一个字节码文件中的field_info表总数,也就是一个类中类变量和实例变量的数量总和。而字段表实际上则是一个数组集合,字段表中的每一个成员都必须是一个field_info的数据项。filed_info用于表示一个字段的完整信息。字段表中所包含的字段信息仅限于当前类或者接口中的所属字段,并不包含继承超类后的字段信息

    9.     methods_count(方法计数器)和methods(方法表):方法计数器用于表示一个字节码文件中的method_info表总数。而方法表实际上则是一个数组集合,方法表中的每一个成员都必须是一个method_info的数据项。method_info用于表示当前类或者接口中某个方法的完整描述。方法表中所包含的方法信息仅限于当前类或者接口中的所属方法,并不包含继承超类后的方法信息。

    10. attributes_count(属性计数器)和attributes(属性表):属性计数器用于表示一个字节码文件中的method_info表总数。而属性表实际上则是一个数组集合,属性表中的每一个成员都必须是一个attribute_info的数据项。每一个attribute_info表的第一项都是指向常量池列表中CONSTANT_Utf8_info项的索引,该表给出了属性的名称。

  • 符号引用

    1.     符号引用由3种特殊字符串构成,分别是:全限定名、简单名称和描述符。在字节码文件中,使用全限定名可以用于描述类或者接口。而类或者接口中定义的字段、方法都会包含一个简单名称和描述符。

    2.     字节码文件中包含的所有类或者接口的名称,都是通过全限定名的方式进行表示的。CONSTANT_Class_info表中的name_index是一个指向常量池列表中一个类型为CONSTANT_Utf8_info常量项索引,通过这个索引值即可成功获取权限定名字符串。如,Object的权限定名为java.lang.Object,但在字节码文件中要变成java/lang/Object。

    3.     在字节码文件中,类或者接口所定义的所有字段名称和方法名称都是使用简单名称来进行存储的。简单名称中不能包含"."";"   "[" "/"等以ASCII和UNICODE字符的表示形式。

    4.     类或者接口中定义的字段和方法除了都会包含一个简单名称外,还会包含一个描述符,描述字段的称为字段描述符,描述方法的成为方法描述符。字段描述符用于描述字段类型,而方法描述符用于描述方法的返回值类型以及方法的参数数量、类型和参数顺序。                                                                                                                                                   






  • 作者:龙猫小爷
    链接:http://www.jianshu.com/p/62f9db4d1df3
    來源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    原创粉丝点击