JVM系列之:内存与垃圾回收篇(一)

JVM系列之:内存与垃圾回收篇(一)

##本篇内容概述:
1、JVM结构
2、类加载子系统
3、运行时数据区之:PC寄存器、Java栈、本地方法栈

一、JVM与JAVA体系结构

JAVA虚拟机与JAVA语言并没有必然的联系,它只是与特定的二进制文件格式:Class文件格式关联,Class文件中包含了JAVA虚拟机指令集(字节码、Bytecodes)和符号表,还有一些其他辅助信息。

1、JVM整体结构

【说明:线程共享->方法区和堆; 各个线程独享:Java栈、本地方法栈和程序计数器】

【程序计数器Program Counter Register即:PC寄存器】

2、JAVA代码执行流程

3、JVM的架构模型

java编译器输入的指令流基本上基于以下两种指令集架构:

  • 基于栈的指令集架构(基于零地址指令,出栈入栈,指令集小,但指令多。不依赖于硬件可移植性好)
  • 基于寄存器的指令集架构(以一地址、二地址、三地址指令为主,依赖于硬件可移植性差,但执行效率更高)

总结:

由于跨平台性的设计,Java的指令都是根据栈来设计的。

不同的平台CPU架构不同,所以不能设计为基于寄存器的。有点事跨平台,指令集小,编译器容易实现,
缺点是性能下降,实现同样的功能需要更多的指令。

4、JVM发展

虚拟机特点
SUN Classic VM只有解释器,没有JIT编译器
Exact VM编译器+解释器 混合工作模式
HotSpot(Oracle)引入了方法区Method Area。 热点代码探测+编译器解释器协同工作
JRockit(Oracle)专注于服务器端,JRockit不包含解释器实现,全部靠即时编译器编译后执行
J9(IBM)

二、类加载子系统

  • 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识;
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则有执行引擎Execution Engine决定;
  • 加载的类信息存放于一块称谓方法区Method Area的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

类加载器子系统分为三个阶段:加载阶段、链接阶段、初始化阶段

class file加载到 JVM 中,被称为DNA元数据模板,放在方法区。

.class文件 => JVM => 最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader)。

类的加载过程:

1、加载Loading

##加载
1、通过一个类的全限定名获取定义此类的二进制字节流(从本地、网络、ZIP、动态代理、加密文件中等)

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

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

类加载器:

JVM支持两种类型的类加载器:
	引导类加载器(Bootstrap ClassLoader)
	自定义类加载器(User-Defined ClassLoader)==>派生于抽象类ClassLoader的类加载器都是自定义类加载器
	
extention classloader和application classloader都是自定义类加载器

Bootstrap ClassLoader是C/C++编写的,其他类加载器是java编写的

他们是包含关系:其他自定义类加载器< Application classloader < extention classloader < bootstap classloader
                                                                         
Java的核心类库都是使用bootstrap classloader加载的
1.1、引导类加载器Bootstrap ClassLoader (启动类加载器)
  • 这个类加载使用C/C++语言实现,嵌套在JVM内部
  • 它用来加载java的核心类库,用于提供JVM自身需要的类
  • 加载extention classloader和application classloader,并指定为它们的父类加载器
  • 出于安全考虑,Bootstrap加载器只加载包名为java、javax、sun等开头的类
  • 它不继承自java.lang.ClassLoader,没有父加载器
1.2、扩展类加载器Extension ClassLoader
  • 使用java编写,由sun.misc.Launcher$ExtClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为Bootstrap ClassLoader
  • 从java.ext.dir系统属性所指定的目录或JDK的安装目录的jre/lib/ext子目录下加载类库,如果用户自定义创建的JAR包放在此目录下,也会由扩展类自动加载
1.3、应用程序类加载器Application ClassLoader(系统类加载器)
  • java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为Extension ClassLoader
  • 负责加载环境变量classpath或系统属性java.class.path指定路径下的类
  • 它是程序中默认的类加载器,一般java应用的类都是由它来完成加载
  • 通过ClassLoader.getSystemClassLoader()方法来获取该加载器

java的日常应用开发中,类的加载几乎都是由上述3种类加载器配合执行的

演示:

public class ClassLoaderTest {

    public static void main(String[] args) {
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
        
        //获取扩展类加载器
        ClassLoader extentionClassLoader = systemClassLoader.getParent();
        System.out.println(extentionClassLoader);//sun.misc.Launcher$ExtClassLoader@77459877
        
        //获取引导类加载器
        ClassLoader bootstrapClassLoader = extentionClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null
        
        //获取自定义的ClassLoaderTest类的类加载器
        //说明:对于用户自定义的类默认使用  系统类加载器
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
        
        //获取String的类加载器
        //Java的核心类库都是使用bootstrap classloader加载的
        ClassLoader stringClassLoader = String.class.getClassLoader();
        System.out.println(stringClassLoader);//null
    }
}

自定义自己的类加载器,可以继承ClassLoader或者URLClassLoader类 重写findClass()方法即可

1.4、双亲委派机制
	JAVA虚拟机对class文件采用的是"按需加载"的方式,也就是说当需要使用该类的时候才会将他的class文件加载到内存中生成class对象。

	而且加载某个类的class文件时,java虚拟机采用的是"双亲委派模式",即把请求交由父类处理,也是一种任务委派模式

##工作原理:
1、如果一个类加载器收到了类加载的请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;

2、如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;

3、如果父类加载器可以完成累的加载任务,就成功返回,若父类加载器无法完成此家在任务,子加载器才会
尝试自己去加载,这就是双亲委派模式


##作用:
【只有这样如果我们自定义的java.lang.String类才不会被执行,也就不会覆盖系统自带的String了】
1、避免了类的重复加载
2、保护了程序安全,防止核心API被恶意篡改
1.5、沙箱安全机制
如上,我们自定义java.lang.String类,但是在加载自定义String类的时候会率先使用引导类加载器加载。
从而引导类加载器在加载的过程中会先加载JDK自带的文件(rt.jar包下的java.lang.String.class)。

这样可以保证对java核心源代码的保护,这就是"沙箱安全机制"。
1.6、其他
1、在JVM中表示两个class对象是否为同一个类,有两个条件:
	a、类的完整类名必须一致,包括包名
	b、加载这个类的ClassLoader类加载器必须相同

2、JVM必须知道一个类型是由启动类加载器加载的还是用户自定义类加载器加载的。
如果一个类型是由用户自定义类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。
当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

3、JAVA程序对类的使用分为:主动使用 和  被动使用
主动使用,分为7种情况
a、创建类的实例
b、访问某个类或接口的静态变量,或者对该静态变量赋值
c、调用类的静态方法
d、反射(如:Class.forName("com.lee.MyTest"))
e、初始化一个类的自雷
f、Java虚拟机启动的时候被标明为启动类的类
g、JDK7开始提供的动态语言支持

除了以上情况,其他使用Java类的方式都被看做是队类的被动使用。

主动使用会导致类的Initailization初始化(即下面的初始化),被动使用不会

2、链接Liking

##验证verify
	文件格式验证、元数据验证、字节码验证、符号引用验证(保证类加载的正确性)
	[.class文件一般以 CA FE BA BE开头]

##准备Prepare
	为类"变量"分配内存并设置该类变量的默认初始值,即零值。
	(private static int a = 1;在prepare环节a = 0。在initial环节才被赋值为1)
	这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化。
    (private final static int a = 1;在prepare环节a = 1)
	这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到JAVA堆中。

##解析Resolve
	将常量池内的符号引用转换为直接引用的过程 

3、初始化Initailization

##初始化
初始化阶段就是执行类构造器方法<clinit>()的过程【<clinit>()不同于类的构造器<init>()】

此方法不需要定义,是javac编译器自动收集类中的所有 "类变量的赋值动作(static?)" 和 "静态代码块中的语句" 合并而来

构造方法中指令按语句在源文件中出现的顺序执行

若该类具有父类,JVM会保证子类<clinit>()执行前父类<clinit>()已经执行完毕
    
虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁(一个类只能被执行一次<clinit>())
    
##解释
类变量与实例变量的区别:

1)类变量属于类,可以共享,属于公共属性;实例变量属于某个对象个体;

2)加上static 为类变量或者静态变量,否则为实例变量;

按顺序执行

public class ClassInitTest{
    static{
        number = 20;
        //System.out.println(number);//这里可以赋值,但不可以调用-》非法前向引用
    }
    
    private static int number=10;
    //在linking的prepare:number=0 --->  initail:按顺序执行 20 ->10
    
    public static void main(String[] args){
        System.out.println(number);//number的值为10
    }
    
}

加载子类<clinit>()执行前必须保证父类<clinit>()的执行

public class ClinitTest1{
    static class Father{
        public static int A = 1;
        static{
            A=2;
        }
    }
    
    static class Son extends Father{
        public static int B = A;
    }
    
    public static void main(String[] args){
        //先加载Father类,prepare:A=0 initial A=1 然后A=2
        //再加载Son类,prepare:B=0 initail B=A=2
        System.out.println(Son.B);//2
    }
}

三、运行时数据区

1、运行时数据区概述及线程

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有些会随着虚拟机的启动二创建,随着虚拟机的退出而销毁。

另外一些则是与线程一一对应,这些与线程对应的数据区域会随着线程的开始和结束而创建和销毁。

##上图:
Method Area方法区 +  Heap Area堆空间 是共享的
Stack Area + PC Registers +  Native Method stack是各个线程独享的

1)、每个线程:独立使用程序计数器、栈、本地栈。
2)、线程间共享:堆、堆外内存(永久代或元空间、代码缓存)


Method Area方法区 在 JDK8 之后  成了 元空间,使用的是本地内存

关于线程间共享的说明:

Class Runtime  (java.lang.Runtime)

每个JVM只有一个Runtime实例。即为运行时环境(Run data Area 运行时数据区)
[所以Runtime是单例的]

线程:

线程(Thread)是一个程序里的运行单元。JVM允许一个应用程序有多个线程并行执行。

在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。
(当一个Java线程准备好执行以后,此时操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会被回收)

操作系统负责所有线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功,他就会调用Java线程的run()方法


##Hotspot JVM里主要有如下几种线程:
 ·虚 拟 机 线 程
 ·周 期 任 务 线 程
 ·GC 线 程
 ·编 译 线 程
 ·信 号 调 度 线 程

2、PC寄存器

没有GC垃圾回收,不会报OOM(OutOfMemoryError)

##PC寄存器 :Program Counter Register 【类似一个游标】
(JVM中的PC寄存器是对屋里PC寄存器的一种抽象模拟,也可以成为程序的钩子,程序的行号指示器)

##作用:
PC寄存器用来存储指向下一条指令的地址,也就是即将要执行的指令代码。
由执行引擎读取下一条指令


##特点:
·它是一块很小的内存空间,也是运行速度最快的存储区域

·JVM规范中:每个线程都有它自己的PC寄存器,是线程私有的,生命周期与线程保持一致

·任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。PC寄存器会存储当前线程
正在执行的Java方法的JVM指令地址,或者,如果是在执行native方法,则是未指定值[undefined]


下面的 栈帧 也就是 方法,当前栈帧就是程序中的当前Java方法

·PC寄存器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

·字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

·它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
javap -v PCRegister.class

下方右侧 0 2 3···即 指令地址 或 偏移地址

常见问题:

一、##使用PC寄存器存储字节码指令地址有什么用?
   ##为什么使用PC寄存器记录当前线程的执行地址?

因为CPU需要不停的快速切换各个线程,切换回来后,就得以知道接着从哪开始继续执行了。

JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。


二、##PC寄存器为什么被设定为线程私有的?

所谓的多线程在特定的时间段内只会执行其中的某一个线程,CPU会不停地做快速切换而已。
这样必然导致线程的经常中断或恢复。

为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每个线程都
分配一个PC寄存器,这样一来各个线程之间变可以进行独立计算,从而不会出现相互干扰的情况。

拓展:CPU时间片:

CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它为时间片。

宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行
并行: 多个任务 被多个CPU同时执行,相对的就是  "串行"

并发:多个任务 被一个CPU快速切换的交替执行

3、虚拟机栈(Java栈)

3.1、概述

没有GC垃圾回收问题,但会存在OOM(OutOfMemoryError)的问题

一、##出现背景:

前面在JVM架构模型中讲到,指令流基本上基于两种指令集架构:基于栈的指令集架构、基于寄存器的指令集架构。

由于跨平台性的设计Java的指令都是根据栈来设计的。(不同平台的CPU架构不同,所以不能使用基于寄存器的)。

优点是:跨平台,指令集小,编译器容易实现。
缺点是:性能下降,实现同样的功能需要更多的指令(指令集小但指令多)


二、##内存中栈与堆的区别:

栈是运行时的单位,堆是存储的单位

即:栈解决程序的运行问题,即程序如何执行或者说如何处理数据。
堆解决的是数据存储的问题,即数据怎么放,放哪儿。


三、##Java虚拟机栈是什么?

Java虚拟机栈,早期也叫Java栈。
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),每一个栈帧对应着一个的Java方法调用。
一个个方法的调用对应着一个个栈帧的入栈和出栈操作。

Java栈是线程私有的。

其生命周期和线程一致。

主管Java程序的运行,它保存 方法的 "局部变量(8种基本数据类型、对象的引用地址)"、 "部分结果",并参与方法的调用和返回。 

四、##栈的有点:

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。

JVM直接对Java栈的操作只有两个:
·方法的执行,伴随着进栈(入栈、压栈)
·执行结束后的 出栈 工作

对于栈来说不存在GC垃圾回收问题

五、##栈中可能出现的异常:

Java虚拟机规范允许Java栈的大小是 "动态的" 或者 "固定不变"的。 

·如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。
如果线程请求分配的栈内容超过Java虚拟机允许的最大容量,Java虚拟机将会抛出一个StackOverflowError 异常【递归的时候可能会出现】

·如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将会抛出一个OutOfMemoryError异常

六、##设置栈的大小
【java -Xss256k 设置stack大小】
【java -Xmx512m -Xmx512m 其中-Xmx设置堆最大值 -Xms设置堆初始值。】
3.2、栈的存储结构和运行原理
一、##栈的存储单位

每个线程都有自己的栈,栈中的数据都是以栈帧Stack Frame的格式存在。

在这个线程上正在执行的每一个方法都各自对应一个栈帧Stack Frame。

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

二、##栈运行原理

JVM直接对Java栈的操作只有两个,就是对栈帧的“压栈”和“出栈”,遵循“先进后出”、“后进先出”的原则。

在一条活动线程中,一个时间点上,只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧
(栈顶栈帧)是有效的,这个栈帧被称谓当前栈帧Current Frame,与当前栈帧相对应的方法
就是当前方法Current Method,定义这个方法的类就是当前类Current Class.

执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,称谓新的当前栈帧。

不同线程中锁包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧。

如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈
帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

Java方法有两种返回函数的方式:一种是正常的函数返回,使用return指令;另一种是抛异常。
不管使用哪种方式,都会导致栈帧被弹出。
3.3、栈帧的内部结构
一、##栈帧的内部结构

每个栈帧中存储着:
·局部变量  Local Variables
·操作数栈  Operand Stack  (或表达式)
·动态链接  Dynamic Linking (或指向运行时常量池的方法引用)
·方法返回地址 Return Address (或方法正常退出 或者异常退出的定义)
·一些附加信息

1>、局部变量表Local Variables

·局部变量表也被称谓 局部变量数组  或  本地变量表

·定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,
这些数据类型包括8种基本数据类型、对象引用(refrence),以及returnAddress类型

·由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据的安全问题

·局部变量表所需的容量大小是在编译器确定下来的,并保存在方法的Code属性的maximum local variable是数据项中。
在方法运行期间是不会改变局部变量表的大小的。

【上图 LineNumberTable表示 字节码的行号 和 java代码行号对应的关系】

·方法嵌套调用的次数由栈的大小决定(如递归)。

·局部变量表中的变量只在当前方法调用中有效。方法执行时,虚拟机通过使用局部变量表完成参数值
到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。【局部变量表是主要影响栈帧大小的】

##上图中LocalVariableTable中Slot的理解

·参数值的存放总是在局部变量数组的index0开始,到数组长度的-1的索引结束。

·局部变量表,最基本的存储单元是Slot 变量槽

·局部变量表中存放编译期可知的8中基本数据类型,引用类型,returnAddress类型的变量

·局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型)
64位的类型(long和double)占用两个slot
> byte\short\char在存储前被转换为int
> boolean也被转换为int 0表示false,非0表示true
> long和double则占据两个slot

byte\short\char\float\int 占1个slot
long\double占2个slot

·JVM回味局部变量表中每个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。

·当一个实例方法被调用的时候,他的方法参数和方法体内部定义的局部变量将会按照顺序被赋值到局部变量表中的每一个slot上。

·如果需要访问局部变量表中的一个64bit的局部变量值时,只需要使用前一个索引即可。(long和double)

·如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
(因此被static修饰的方法中是不能够使用this的)

·栈帧中的局部变量表中的槽位slot是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后
声明的新的局部变量就很有可能会服用过期局部变量的槽位,从而达到节省资源的目的。
例如:
public void test{
  int a = 0;
  {
    int b = 0;
    b = a + 1;
  }
  int c = a +1;
}
其中b的作用域在其{}内,变量c是使用之前已经销毁的变量b占据的slot的位置。

变量 f 的位置被重复利用了

##拓展变量的分类:
	按照数据类型分:①基本数据类型 ②引用数据类型
    按照在类中的声明位置分:
            ①成员变量:
              类变量(或叫静态变量,被static修饰)
              实例变量
			②局部变量	
##类变量
   linking的prepare阶段:给类变量默认赋值-->initial阶段:给类变量显式赋值即静态代码块赋值

##实例变量
	随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
##局部变量
    在使用前,必须要进行显式赋值!否则,编译不通过
##补充说明:

·在栈帧中,与性能调优关系最为模切的部分就是 局部变量表。
在方法执行时,虚拟机使用局部变量表完成方法的传递。

·局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中引用数据类型直接或间接引用的对象都不会被回收。

(局部变量表中 存储:基本数据类型 和 引用数据类型,引用数据类型引用的是堆空间中的对象,所以影响了堆空间的GC垃圾回收。如果局部变量表中的引用数据类型不存在了,那么堆空间中的数据可能就会被回收)

2>、操作数栈Operand Stack

##操作数栈:

·后进先出,也被称之为表达式栈

·操作数栈,主要是用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间

·操作数栈,在方法执行的过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈PUSH/出栈POP
(有些字节码指令是将值PUSH进操作栈,有些字节码指令是将操作数POP出栈,使用后再把结果PUSH进栈)
(比如执行赋值、交换、求和等操作)

·操作数栈,主要是用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。

·操作数栈就是JVM执行引擎的一个工作区,当一个方法开始执行的时候,一个新的栈帧也会随
之被创建出来,这个方法的操作数栈是空的。

·每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义
好了。保存在方法的code属性中,为max_stack的值。

·栈中的任何一个元素都是可以任意的Java数据类型:
  >32bit的类型占用一个栈单位深度
  >64bit的类型占用两个栈单位深度

·操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和川站操作来完成一次数据访问

·如果被调用的方法带返回值的话,其返回值将会被压入当前栈帧的操作数栈中。并更新PC寄存器中下一条需要执行的字节码指令。
(当当前方法用到上一条方法的返回值时会从操作数栈中load)

·操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行
验证,同时在类家在过程中的类检验阶段的数据流分析阶段要再次验证。

·另外,我们说Java虚拟机的解释引擎就是基于栈的执行引擎,其中的栈值得就是操作数栈。
(也就是说 JVM的执行引擎和操作数栈 配合操作的)


栈顶缓存技术Top-Of-Stack Cashing

·由于操作数是存储在内存中的,因此频繁第执行内存读/写操作必然会影响执行速度。

为了解决这个问题,HotSpot JVM的设计者提出了栈顶缓存技术。

即将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行殷勤的执行效率。

3>、动态链接Dynamic Linking

动态链接:指向运行时常量池的方法引用。
(运行时常量池  在  "方法区" 中)

·每一个栈帧内部都包含一个执行运行时常量池中该栈帧所属方法的引用。
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接:invokedynamic指令

·在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。
(比如描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的)

·动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

·符号引用:以一组符号来描述所引用的目标 符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可
如:
public static void main(java.lang.String[]);
    ....
    Code:
      stack=2, locals=3, args_size=1
        ....
        11: invokevirtual #4      // Method methods1:()I

·直接引用:直接引用可以是直接指向目标的指针、相对偏移量、一个能间接定位到目标的句柄
如:
Constant pool:
   ....
   #4 = Methodref  #2.#39  // com/lee/jvm/rundataarea/LocalVariablesTest.methods1:()I


##JVM指令拓展:
普通调用指令:
invokestatic:调用静态方法
invokespecial:调用<init>方法、私有及父类方法
invokevirtual:调用所有虚方法
invokeinterface:调用接口方法
动态调用指令:
invokedynamic:动态解析出需要调用的方法,然后执行

4>、方法返回地址 Return Address

·方法返回地址:存放调用该方法的PC寄存器的值

·一个方法的结束,有两种方式:
>正常执行完成
>出现未处理异常,非正常退出

·无论哪种方式退出,在方法退出后都返回到该方法被调用的位置。
方法正常退出是,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
1、执行引擎遇到任意一个方法返回的字节码指令return,会有返回值传递给上层的方法调用这,简称正常完成出口:
>一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。
>在字节码指令中,返回指令包含为ireturn(当返回值时boolean\byte\chat\short\int类型时使用)、lreturn(long)、freturn(float)、dreturn(double)以及areturn
另外还有一个return指令提供声明为void的方法、实力初始化方法、类和接口的初始化方法使用。

2、在方法执行的过程中遇到异常Exception,并且这个异常没有在方法内处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口

方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。

##本质上,方法的退出就是当前栈帧出栈的过程。
此时需要恢复上层方法的局部变量表、操作数栈、将返回值亚茹调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

正常完成出口和异常完成出口的区别在于:通过异常完成的出口退出的不会给他的上层调用者产生任何的返回值。

5>、一些附加信息

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。如:对程序调试提供支持的信息

6>、关于Java栈的思考

一、##举例栈溢出的情况?

如果设置了栈的大小,栈空间固定,虚拟机栈增加栈帧溢出会出现StackOverflowError
如果没有设置了栈的大小,虚拟机栈增加栈帧内存溢出或者增加新的线程时会出现OutOfMemeryError

二、##调整栈的大小,就能保证不出现溢出吗?

不能保证,只能保证栈溢出的阈值增大而已。

三、##分配的栈内存越大越好吗?

不是,内存空间固定,某个栈空间变大了,挤占了线程数或者其他结构的内存空间

四、##垃圾回收是否会涉及到虚拟机栈?

不会,栈的生命周期是随 线程 的创建和销毁的,所以不存在GC垃圾回收,如果溢出直接报错

五、##方法中定义的局部变量是否线程安全?

不一定,举例说明
public static void method() {
	int x = 0;
	for(int i = 1;i<=10;i++) {
		x *= i;
	}
	System.out.println(x);
}
上面线程安全的,当多个线程同时执行此方法时,每个线程都会产生一个自己的变量x,每个线程之间互不干扰,不会对其他线程的变量x有影响。
                            
public static void method(StringBuilder sb) {
	sb.append(1);
	sb.append(2);
	System.out.println(sb.toString());
}  
上面线程不安全,在这里sb对象是作为参数传入的,这意味着它并不是线程私有的,多个线程都可以同时访问它。所以不是线程安全的。                           
                            
public static StringBuilder method() {
	StringBuilder sb = new StringBuilder();
	sb.append(1);
	sb.append(2);
	return sb;
}
上面线程不安全,虽然sb对象是在方法内生成的对象。但是sb作为一个返回变量返回,其他线程可以去拿取它,对它进行并发的操作。                            
   
public static String method() {
	StringBuilder sb = new StringBuilder();
	sb.append(1);
	sb.append(2);
	return sb.toString();
}     
上面线程安全,sb消亡了,sb.toString()相当于我们又new 了一个string,这个new出来的string可能不安全,但是方法内的sb是安全的。

4、本地方法栈

在写本地方法栈前 先 了解一下本地方法库本地方法接口

##本地方法

本地方法就是,一个Native Method就是一个Java调用一个本地非Java代码的接口,比如是C或者C++。(即一个Java方法其方法内部是由非Java语言实现的)
如:public final native Class<?> getClass();
如:Thread 中 private native void start0();

标识符native可以与所有其他的java标识符连用,但是abstract除外

本地方法栈:

·Java虚拟机栈用于管理Java方法的调用。而本地房发展用于管理本地方法的调用。

·本地方法栈,也是线程私有的

·允许被是线程固定或者是可动态扩展的内存大小(在内存溢出方面和虚拟机栈是相同的)

·本地方法时C语言实现的

·具体做法是 虚拟机栈 中登记native方法,在 执行引擎执行时加载本地方法库。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值