此篇比较零散 后面会再系统的学一下jvm再更新
jvm的一些东西
运行时数据区包括
1.程序计数器
2.Java 堆
3.Java 虚拟机栈
4.本地方法栈
5.方法区
其中方法区和堆是所有线程共享的 其他的是线程隔离的数据区
程序计数器(Program Counter Register)
一块较小的的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。
如果线程正在执行的是一个 Java 方法 这个计数器记录的是正在执行的虚拟机字节码指令的地址
如果正在执行的是 Native 方法,这个计数器值则为空。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError情况的区域。
在虚拟机的概念模型里,字节码解释器工作时就是通过在改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换,分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储
Java 虚拟机栈的特征
线程私有
后进先出(LIFO)栈
存储栈帧,支撑 Java 方法的调用、执行和退出
可能出现 OutOfMemoryError 异常和 StackOverflowError 异常
生命周期和线程一样 是线程特有的 描述java方法执行时的内存概念模型 每个方法在执行的时候都会创建一个栈帧 用来创建整个方法的XXX信息 每一个方法在调用和结束的过程就对应了一个栈帧在虚拟机中从入栈到出栈的过程
Java 本地方法栈的特征
线程私有
后进先出(LIFO)栈
作用是支撑 Native 方法的调用、执行和退出
可能出现 OutOfMemoryError 异常和 StackOverflowError 异常有一些虚拟机(如 HotSpot)将 Java 虚拟机栈和本地方法栈合并实现
注意和虚拟机栈的区别 和它非常相似 虚拟机栈是为了java执行字节码服务的 而本地方法栈是为了java执行native方法所服务的 因此本地方法栈也是私有的
栈帧的概念和特征
Java 虚拟机栈中存储的内容,它被用于存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派
一个完整的栈帧包含:局部变量表、操作数栈、动态连接信息、方法正常完成和异常完成信息
在编译的时候 栈帧需要多大的局部变量表 多深的操作数栈 都已经确定了 吧这些信息写入class文件的code表之中 栈帧需要多大的内存 不会受运行期间变量数据的影响的 只有在栈顶的那个栈帧才是有效的 这个栈帧称为当前栈帧 与这个栈帧相关联的方法 方法称为当前方法 虚拟机引擎中所有执行的字节码指令 都是针对当前栈帧和当前方法进行操作的
1.局部变量表概念和特征
由若干个 Slot 组成,长度由编译期决定
单个Slot可以存储一个类型为 boolean、byte、char、short、float、reference 和 returnAddress的数据 两个Slot可以存储一个类型为long或double的数据。局部变量表用于方法间参数传递,以及方法执行过程中存储基础数据类型的值和对象的引用局部变量表是一组变量值的存储空间用于方法参数 方法内部的局部变量等等 由变量槽slot为最小单位 局部变量表建立在线程堆栈之上
2.操作数栈的概念和特征
是一个后进先出栈,由若干个 Entry 组成,长度由编译期决定
单个 Entry 即可以存储一个 Java 虚拟机中定义的任意数据类型的值,包括 long和 double 类型,
但是存储 long 和 double 类型的 Entry 深度为2,其他类型的深度为1
在方法执行过程中,栈帧用于存储计算参数和计算结果;在方法调用时,操作数栈也用来准备调用方法的参数以及接收方法返回结果
3.操作数栈
也称为操作栈 最大深度在编译期决定 Entry(操作数栈的栈元素)是实体的意思 当一个方法刚开始执行的时候 它的操作数栈是空的 在方法的执行过程 会遇到各种不同的字节码指令
往操作数栈之中写入和提取内容 就对应了操作数栈的出栈和入栈操作
Bipush 100 占用两个偏移量 因为指令本身占用一个偏移量 参数占用一个偏移量 作用是吧100入栈到操作数栈的栈顶 注意操作数栈的栈顶在底部
istore_1 只占用一个偏移量 指令作用是吧操作数栈的栈顶元素出栈出栈 并把数据存储到局部变量表索引号为1的slot 局部变量表的索引为0的值是this
Iload_1将局部变量表索引号位1的数据存储到操作数栈的栈顶之中
Iadd距离操作数栈栈顶的两个元素出栈 吧这两个数相加的结果重新存储到操作数栈的栈顶 深度-1
先把数比如(100)入栈到操作数栈栈顶 然后把200也入到操作数栈顶 然后istore_1吧操作数栈的栈顶元素出栈 并把数据存储到局部变量表索引号为1的slot 局部变量表(索引为0的是this)
Iload_1将局部变量表索引号位1的数据存储到操作数栈的栈顶之中
Iadd距离操作数栈栈顶的两个元素出栈 把这两个数相加的结果重新存储到操作数栈的栈顶
数据的流动 操作数栈--->局部变量表--->操作数栈--->操作数栈(这些都是在栈帧里面的)栈帧又是方法栈和本地虚拟机栈会创建的 注意和方法区的区别 方法区和堆一样 全局的 线程共享存储已经被java虚拟机加载到方法区的类型数据
实例数据是指定义在类中的各种实例对象以及他们的值 类信息是指定义在java代码之中中的常量静态变量 以及在类中所声明的各种方法字段等等 方法区叫none heap 就是非堆 都不用实现垃圾收集 但是商用jvm能够自动管理该区域的内存
java栈
本地变量表
int java堆 方法区
short |对象实例数据 里面包含
reference----------------------> |到对象数据类型的指针-------------------->对象类型数据
还有句柄的实现
方法区的特征
全局共享
作用是存储 Java 类的结构信息
JVMS 不要求该区域实现自动内存管理,但是商用 Java 虚拟机都能够自动管理该区域的内存
可能出现 OutOfMemoryError 异常
运行时常量池的特征
全局共享
是方法区的一部分
作用是存储 Java 类文件常量池中的符号信息
可能出现 OutOfMemoryError 异常
在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈.而堆则是所有线程共享的.栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的.包括局部变量、程序运行状态、方法返回值等等 而堆只负责存储对象信息
1,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据.这样分开,使得处理逻辑更为清晰.分而治之的思想.这种隔离、模块化的思想在软件设计的方方面面都有体现.
2,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象.这种共享的收益是很多的.一方面这种共享提供了一种有效的数据交互方式(如:共享内存),
另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间.
3,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分.由于栈只能向上增长,因此就会限制住栈存储内容的能力.而堆不同,堆中的对象是可以根据需要动态增长的,
因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可.
4,面向对象就是堆和栈的完美结合.其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别.但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考.当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中. '我们在编写对象的时候,其实即编写了数据结构',也编写的处理数据的逻辑.不得不承认,面向对象的设计,确实很美.
在Java中,Main函数就是栈的起始点,也是程序的起始点.
堆中存的是对象.栈中存的是基本数据类型和堆中对象的引用 一个对象的大小是不可估计的,或者
说是可以动态变化的,但是在栈中,一个对象只对应了一个4btye的引用(堆栈分离的好处:))
类成员变量new是在堆上的 类的本地(local)变量是在栈上的
public class Main { public static void main(String[] args) { int i = 10; System.out.println(i * 2); } } public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: bipush 10 2: istore_1 3: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 6: iload_1 7: iconst_2 8: imul 9: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 12: return
bipush 10 是将10压入操作数栈。
istore_1,是将操作数栈的栈顶元素弹出,并写入本地变量表的下标1处。
也即是说,本地变量的名字都不见了。
会被编译器直接编译成针对本地变量表的操作。
所以,到这里你就应该明白了,本地变量没有什么名字,地址映射 也没有什么副本
6: iload_1
7: iconst_2
8: imul
这三行是 i * 2
iload_1 是载入本地变量表的下标1,到操作数栈栈顶
iconst_2 是将常数2压入操作数栈栈顶
imul 是弹出栈顶的两个数,并将相乘的结果压回操作数栈栈顶。
所以还是那个道理,对于本地变量的存储,读取都被编译器直接写死在了字节码指令中。
只要知道是下标几,JVM就有办法定位到那个位置。
局部变量和上面的本地变量是一回事
public class Test {
public static void main(String[] args) {
int i = 10;//i是本地变量
}
int a = 11;//a是成员变量
}
new Test()的时候,只会开辟成员变量所需的空间
当不运行Test的任何方法时,本地变量都是不存在的 只有成员变量被初始化了,成员变量的生命周期和对象一致
Test a = new Test();的时候 fun()方法里的东西都还没有在内存上创建 直到a.fun(); 而且fun()方法执行完毕的时候,方法里面的变量立刻就失效了
new只和堆有关 a.fun();的时候就用到栈了
static的方法没有优先性,static的成员变量会优先初始化。
staitc块和static的成员变量,叫做静态初始化。
Java是个多线程的语言。
每个线程都有一个JVM栈(这个名字没有什么意义)
每个JVM栈维护着调用堆栈所需的信息。
JVM栈中又分为很多frame(帧)
具体来说就是一个方法对应一个帧
public static void main(String[] args) {
foo();
}
void foo() {
bar();
}
假设main方法是该线程的第一个方法
当进入main方法时,就会创建一个frame
这个frame里又有一个操作数栈和本地变量表。
操作数栈先不管
本地变量表就是存放本地变量的地方。
JVM Stack >> Frame >> (操作数栈 + 本地变量表)
一个方法的本地变量占用的空间是可以算出来的。
所以当进入方法时,创建Frame时,就可以直接创建出一片空间,用于存放本地变量。
new 一个类时,方法的本地变量还都没有创建呢
只有实际运行一个方法时,本地变量才被创建。
本地变量的名字没有意义
a.fun()时,是在当前线程关联的那个JVM栈上,新建一个Frame
每个线程都有一个JVM栈,每个线程都会运行某个方法,当前方法关联的那个栈就叫做当前帧
Java中,是有多个Stack,有一个Heap。
两个线程可以同时进入一个方法
一个本地变量可以同时存在于两个线程中,互不冲突
但是成员变量是存在于Heap中,Heap只有一个,被所有线程共享
如果是一个线程创建了两个对象
当运行a.fun()时,创建了一个Frame,运行结束Frame被抛弃掉。
当运行b.fun()时,又创建了一个Frame,运行结束Frame也被抛弃掉。
如果是public static void main(String[] args) {
foo();
}
void foo() {
bar();
}
这种嵌套关系
那么一开始是main的Frame,main没运行玩,又创建了一个foo的Frame,foo没运行完,又创建了一个bar的Frame
当bar运行完了,bar的Frame被抛弃,又回到foo的Frame继续执行。
依次类推。
一个线程同时只能有一个当前帧。
随着调用退栈越来越深入,JVM栈中包含的Frame就越来越多。
随着调用堆栈的逐步返回
JVM栈中的Frame也一个一个的被抛弃掉。
方法的代码是共享的,但是上下文环境是不共享的。
new只与堆有关 与栈无关
只是 new Test() 的话与栈无关。
Test a = new Test()的话
a 是与栈有关,存储的只是一个四字节的引用。
Test a = new Test();
a.fun();
a 是局部变量
a 在 main 方法关联的 Frame 的本地变量表里。
当new 一个类时,方法的本地变量 存储空间早就计算好了
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
stack=3 最多用到三个操作数位置
locals=2最多用到2个本地变量位置
args_size=1方法有一个参数
这些信息是在类的字节码中的
当类被载入虚拟机时,这些内容也被载入,在内存中只存在一份。
是在方法区中,方法区即不是Heap,也不是Stack。
所以当要新建一个对象时,要先检查类是否已经初始化好了。
0: bipush 10
2: istore_1
3: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
6: iload_1
7: iconst_2
8: imul
9: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
12: return
字节码就是方法代码被编译后的形式 .java编译后就是字节码
不过里面有很多内容,关于方法的只是一部分
基本类型,不会出现动态增长的情况——长度固定
1. 不要试图与C进行类比,Java中没有指针的概念
2. 程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题.不会直接传
对象本身
Java在方法调用传递参数时,因为没有指针,所以它都是进行传值调用
如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样
的 被传递的这个引用的值,被程序解释(或者查找)到堆中的对象,这个时候才对应到真正的对象
----------------------------------------------------------------------------------------------------------------------------------
.c++内存 以前学过c++的 可能会对java的内存有疑惑 可以对比着看看
在C++中,int a = 10的内存表现形式取决于你的具体代码和优化级别,主要的几种形式:
不存在于内存中。比如a从未改变,被编译器当成常量,所有代码中的a直接替换成10;
存在于寄存器中;比如对a的读写很简单,10可能就直接放在了寄存器eax中。
放在函数栈(stack)中。比如包含a的函数的栈基指针是0xC0000000,a的地址就可能是0xBFFFFFFC
也可能在堆(heap)中,比如a是一个类成员变量,而该类的对象new在堆上。
① 如果是在方法中声明这个变量,那么这个变量就是本地变量。本地变量位于虚拟机栈中,只有当运行到这个方法之后才会存在。对于任意一个方法,本地变量最多需要多少空间是固定的。
因此当运行到这个方法之后,会直接开辟出一片空间用于存放本地变量。基本类型的变量的值就是直接存放于变量之中,也就是上面开辟出的空间中。本地变量的名字没有意义,也不存在于字节码中,变量的获取和存放都被写死在字节码指令中。
② 如果是在类中声明这个变量,那么这个变量就是成员变量。
成员变量位于堆(Heap)中,一个类有多少成员是固定的,每个成员占用空间是固定的。
所以当我们new一个对象时,直接在堆中申请一片固定大小的空间用于存放成员变量即可。
同样对于基本类型变量的值,也是直接存放于变量中,也就是在上面堆中开辟的空间中。
成员变量有名字,名字与内存的映射关系要靠类型的元数据来推算。(而类型的元数据是存放在方法区中,整个程序只需要一份即可,在类型静态初始化时完成。这也是为什么新建对象之前,必须先将类型加载进JVM,并先进行类型的静态初始化)
③ 说到类型的初始化,那么还需要说明的就是对于静态成员变量,也是存在于方法区的。
当然只是基本类型变量和引用而已,占用的空间大小也是固定的。引用指向的对象,仍然是位于堆中。
④ 扩展阅读:对于变量,除了基本类型之外,还有数组和引用类型。
这时变量中直接存放的数据就是引用了,而引用的大小是对于当前JVM来说是固定的。
一般32位JVM的引用大小是4个字节,64位的JVM引用大小是4或8个字节。
所以不管是方法本地变量所占用的空间,还是对象成员变量所占用的空间,实际上都是在编译时可以确定的。同样的,数组对象所占用的空间也是可以确定的。对于多维数组来说,只有第一维必须在创建时确定大小,也是这个道理。因为多维数组对象实际上存放的只是低维度数组对象的引用。