java 笔记

由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。在任何一个时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的位置,每一条线程都需要有一个独立的程序计数器,各条线程的程序计数器互不影响,独立存储。


java虚拟机栈是线程私有的,它的生命周期与线程相同,虚拟机栈描述的是java方法执行的内存模型,每一个方法在执行的同时都会创建一个栈帧用于存贮局部 变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。


java虚拟机规范中描述:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象在堆上分配也渐渐的不是那么的绝对了。


java堆和方法区是各个线程共享的。

堆用于存放对象的实例,方法区用于存贮已被java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据[可以理解为存放class文件的信息]。

运行时常量池是方法区的一部分。class文件中除了有类的版本信息、字段、方法、接口等的描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。


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


在java语言中,可以作为GC ROOTS的对象包括下面的几种:

1、虚拟机栈(栈帧中的本地变量表)中引用的对象

2、方法区中类静态属性引用的对象

3、方法区中常量引用的对象

4、本地方法栈中JNI引用的对象


即时在可达性分析算法中不可达的对象,也并非是非死不可的,这时候他们展示处于缓刑阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行了可达性分析之后发现没有与GC ROOTS相连的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置到一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里的所谓的“执行”是指虚拟机会触发这个方法,但是并不承诺等到它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者是发生死循环。将很可能导致F-Queue队列中其他的对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡的最后一个机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己---只要与引用链上的任何一个对象建立关联即可,比如把自己赋值给某一个类变量或者对象的成员变量,那么在第二次标记时它将被移除“即将回收”的集合;如果对象在这个时候还没有逃脱,那基本上是它就真的被回收了。


jvm判断一个类是否是“无用的类”的条件,需要同时满足一下三个条件:

1、该类的所有实例都已经被回收,也就是java堆里面不存在该类的任何实例对象

2、加载该类的classloader已经被回收

3、该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。


垃圾收集算法:

1、“标记-清除”算法,该算法分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。利用可达分析来进行标记。

2、“复制”算法,将可用的内存按照容量相等分为两块,每次只是使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块没有使用过的内存上面,然后再把已经使用过的内存空间一次清理掉。

3、“标记-整理”算法,该算法的标记过程与“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是所有存活的对象都像一端移动,然后清理掉端边界以外的内存。

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


常量池中主要存放两类常量:

1、字面值

    字面值比较接近与java语言层面的常量概念,如文本字符串、声明为final的常量值等

2、符号引用

    符号引用属于编译原理方面的概念,包括了下面三种常量:

    a、类和接口的全限定名

    b、字段的名称和描述符

    c、方法的名称和描述符

当虚拟机运行的时候,需要从常量池中获得对应的符号引用,再在类创建的时候或者运行时解析、翻译到具体的内存 地址之中


内存申请过程

  1. JVM会试图为相关Java对象在Eden中初始化一块内存区域;
  2. 当Eden空间足够时,内存申请结束。否则到下一步;
  3. JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
  4. Survivor区被用来作为Eden及old的中间交换区域,当old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
  5. 当old区空间不够时,JVM会在old区进行major collection;
  6. 完全垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"Out of memory错误";

对象衰老过程

   新创建的对象的内存都分配自eden。Minor collection的过程就是将eden和在用survivor space中的活对象copy到空闲survivor space中。对象在young generation里经历了一定次数(可以通过参数配置)的minor collection后,就会被移到old generation中,称为tenuring。


虚拟机把描述类的数据从Class文件加载到内存,并且对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。


java语言 类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载稍微增加一些性能开销,但是会为java应用程序提供高度的灵活性,java可以扩展的语言特性就是依赖运行期动态加载和动态连接实现的。



图中加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定;它在某些情况下可以是在初始化阶段之后再开始,这是为了支持java语言的运行时绑定。


在加载阶段,虚拟机需要完成一下3件事情:

1、通过一个类的全限定名来获取定义此类的二进制字节流

2、将这个字节流所代表的静态存贮结构转化为方法区的运行时数据结构。

3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。


数组类不是通过类加载创建,它是由虚拟机直接创建的。


验证阶段:

1、文件格式验证

2、元数据验证

3、字节码验证

4、符号引用验证


准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。注意:这个阶段进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。


解析阶段 虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用: 符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用是能无起义的定位到目标即可,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中 。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在内存中存在。

解析动作主要针对类和接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。


<clinit>()

<clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。

<clinit>() 方法与类的构造方法[ <init>()方法 ]不同,它不需要显式的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>() 方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>() 方法的类肯定是java.lang.Object。

由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

<clinit>()方法对于类或者接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但是接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有 耗时很长的操作,就可能造成多个线程阻塞。


类与类加载器

    对于任何一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。


从开发的角度来看 类加载器可以分为一下三类:

1、启动类加载器 [ Bootstrap ClassLoader,该类加载器是虚拟机的一部分],加载<JAVA_HOME>\lib目录或者是被 -Xbootclasspath参数所指定的路径中的类。

2、扩展类加载器,加载<JAVA_HOME>\lib\ext 目录中的类。

3、应用程序类加载器,加载用户类路径上的类。


在OSGI环境下,类加载器不再是双亲委派模型中的树状结构。而是进一步发展为更加复杂的网状结构。


运行时栈幀结构

    栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行是数据区中的虚拟机栈(Virtual Machinr Stack) 的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

    在编译代码的时候,栈帧需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

局部变量表

    局部变量表是一组变量值存储空间,用于存储方法参数和方法内部定义的局部变量。在java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。

    局部变量表的容量以变量槽(Slot)为最小单位


操作数栈

    操作数栈也常称为操作栈,后入先出的数据结构。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。

    当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。

    java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的栈就是操作数栈。


动态连接

    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。


方法返回地址

    当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。

    另一种退出方式是,在方法执行过程中遇到异常,并且这个异常没有在方法体内得到处理,无论是java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者任何返回值的。


编译器在重载时是通过参数的静态类型而不是即时类型作为判定依据的。


动态实现 invokevirtual 指令的运行时解析过程大致分为一下几个步骤:

1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作 C

2、如果在类C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回java.lang.IllegalAccessError 异常。

3、否则,按照继承关系从上往下一次对C的各个父类进行第二步的搜索和验证过程。

4、如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。


GCJ编译器可以直接把java文件编译成本地代码,C/C++解释执行的版本 CINT

在说java“解释执行”的时候,只有确定了讨论对象是某一种具体的java实现版本和执行引擎运行模式时,谈论解释执行还是编译执行才会比较的确切。

                                                                            编译过程

    java语言中,javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。


基于栈的指令集与基于寄存器的指令集

    java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作,也即操作指令的操作数都是存放在栈中的。与之相对的另外一套常用的指令集架构是基于寄存器的指令集,最典型的就是x86的二地址指令集,通俗点说,就是现在主流pc机中直接支持的指令集架构,这些指令依赖寄存器进行工作。

    基于栈的指令 集主要有点在于可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地受到硬件约束。如果是使用栈架构指令集,用户程序不会直接使用这些寄存器。就可以由虚拟机实现来自行决定把一些访问最频繁的数据放到寄存器中以获取尽量好的性能,这样实现起来也更加简单一些,栈架构的指令集还有一些其他的优点,如代码相对更加紧凑(字节码中每个字节就对应一条指令,而多地址指令中还需要存放参数)、编译器实现更加简单(不需要考虑空间分配的问题,所需空间都在栈上操作)等。

    栈架构指令集的主要缺点是执行速度相对来说稍慢一些,所有主流物理机的指令集都是寄存器架构也从侧面印证了这一点。


sun javac编译过程:

    1、解析与填充符号表过程

    2、插入式注解初期里的注解处理过程

    3、分析与字节码生成过程


java中的泛型在编译器就已经被擦除。


在class文件中,只要描述符不是完全一致的两个方法就可以共存。也就是说,两个方法如果有相同的名称和签名,但是返回值不同,那它们也是可以合法地共存于一个class文件中的。


逃逸分析

    逃逸分析是目前虚拟机中比较前沿的优化技术,它并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术。

    逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,比如赋值给类变量或可以在其它线程的实例变量,称为线程逃逸。

    如果能证明一个对象不会逃逸到方法或者是线程之外,则可以为这个变量做一些优化:

    1、栈上分配,这样对象所占的内存空间就可以随着栈帧出栈而销毁。在一般的应用中,不会逃逸的局部对象所占比列很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集系统的压力将会小很多。

    2、同步消除,线程同步是一个比较耗时的操作,如果一个变量不会逃逸出线程,无法被其他的线程访问,那么这个线程的读写就不会有竞争了,对于这个变量实施的同步措施就可以消除。

    3、标量替换:标量是指一个数据已经无法再分解为更小的数据来表示了,java虚拟机中的原始数据类型以及reference都不能在进一步分解,他们就可以称为标量。相对的,如果一个数据可以继续分解,那它就称作聚合量,java中的对象就是最典型的聚合量。如果把一个java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复元好似类型来访问就叫做标量替换。如果逃逸分析可以证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不会创建这个对象,而改为直接创建它的若干个被这个方法直接使用到的成员变量来代替。将对象拆散后,出了可以让对象的成员变量在栈上分配和读写之外,还可以为后续进一步的优化手段创建条件。


                                                                                处理器、高速缓存、主内存间的交互关系


主内存与工作内存

    java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。

    java内存模型中规定了所有的变量都存放在主内存中(虚拟机内存的一部分)。每一条线程都有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接的操作主内存中变量。不同的线程之间也无法访问到其他线程工作内存中的变量,线程间变量值的传递都需要通过主内存来完成。




volatile保证了新值能够立即同步到主内存,以及每次使用前立即从主内存刷新。














    





































猜你喜欢

转载自blog.csdn.net/u013749540/article/details/79523488