赞
踩
!
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨一下经典额JVM内存布局。
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机的启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
灰色的为单独线程私有的,红色的为多个线程共享的。即:
每个JVM只有一个Runtime实例。即为运行时环境,相当于内存结构的中间的那个框框:运行时环境。
JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。
这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽闲模拟。
作用:PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
- public class PCRegisterTest {
- public static void main(String[] args) {
- int i = 10;
- int j = 20;
- int k = i + j;
- String s = "abc";
- System.out.println(i);
- System.out.println(k);
- }
- }
使用PC寄存器存储字节码指令地址有什么用呢?
为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变pc寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么会被设定为线程私有?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停的做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个pc寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
由于CPU时间片轮转限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
CPU时间片及CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
在宏观上:我们可以同事打开多个应用程序,每个程序并行不悖,同时运行。
但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
虚拟机栈出现的背景
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
有不少Java开发人员一提到Java内存结构,就会非常粗粒度的将JVM中的内存区理解为仅有Java堆(heap)和Java栈(stack)?为什么?
内存中的栈与堆
栈是运行时的单位,而堆是存储的单位。
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么存放、放在哪儿。
- package com.atguigu.java;
-
- /**
- * @author shkstart
- * @create 2020 下午 8:32
- */
- public class StackTest {
-
- public static void main(String[] args) {
- StackTest test = new StackTest();
- test.methodA();
- }
-
- public void methodA() {
- int i = 10;
- int j = 20;
-
- methodB();
- }
-
- public void methodB(){
- int k = 30;
- int m = 40;
- }
- }
栈的特点(优点):
栈中可能出现的异常:
例:演示栈中的异常
-
-
- /**
- * 演示栈中的异常:StackOverflowError
- * @author shkstart
- * @create 2020 下午 9:08
- *
- * 默认情况下:count : 11323
- * 设置栈的大小: -Xss256k : count : 2461
- */
- public class StackErrorTest {
- private static int count = 1;
- public static void main(String[] args) {
- System.out.println(count);
- count++;
- main(args);
- }
-
- }
设置栈内存大小:
我们可以使用-Xss选项来设置线程的最大空间,栈的大小直接决定了函数调用的最大可达深度。
栈中存储什么?
栈运行原理:
- /**
- * @author shkstart
- * @create 2020 下午 4:11
- *
- * 方法的结束方式分为两种:① 正常结束,以return为代表 ② 方法执行中出现未捕获处理的异常,以抛出异常的方式结束
- *
- */
- public class StackFrameTest {
- public static void main(String[] args) {
- try {
- StackFrameTest test = new StackFrameTest();
- test.method1();
-
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- System.out.println("main()正常结束");
-
- }
-
- public void method1(){
- System.out.println("method1()开始执行...");
- method2();
- System.out.println("method1()执行结束...");
- // System.out.println(10 / 0);
-
- // return ;//可以省略
- }
-
- public int method2() {
- System.out.println("method2()开始执行...");
- int i = 10;
- int m = (int) method3();
- System.out.println("method2()即将结束...");
- return i + m;
- }
-
- public double method3() {
- System.out.println("method3()开始执行...");
- double j = 20.0;
- System.out.println("method3()即将结束...");
- return j;
- }
-
- }
每个栈帧中存储着:
多个线程的情况
4.4.1 局部变量表
示例:验证局部变量表的大小是在编译器确定下来的
- import java.util.Date;
-
- /**
- * @author shkstart
- * @create 2020 下午 6:13
- */
- public class LocalVariablesTest {
- private int count = 0;
-
- public static void main(String[] args) {
- LocalVariablesTest test = new LocalVariablesTest();
- int num = 10;
- test.test1();
- }
-
- //练习:
- public static void testStatic(){
- LocalVariablesTest test = new LocalVariablesTest();
- Date date = new Date();
- int count = 10;
- System.out.println(count);
- //因为this变量不存在于当前方法的局部变量表中!!
- // System.out.println(this.count);
- }
-
- //关于Slot的使用的理解
- public LocalVariablesTest(){
- this.count = 1;
- }
-
- public void test1() {
- Date date = new Date();
- String name1 = "atguigu.com";
- String result = test2(date, name1);
- System.out.println(date + name1);
- }
-
- public String test2(Date dateP, String name2) {
- dateP = null;
- name2 = "songhongkang";
- double weight = 130.5;//占据两个slot
- char gender = '男';
- return dateP + name2;
- }
-
- public void test3() {
- this.count++;
- }
-
- public void test4() {
- int a = 0;
- {
- int b = 0;
- b = a + 1;
- }
- //变量c使用之前已经销毁的变量b占据的slot的位置
- int c = a + 1;
- }
-
- /*
- 变量的分类:按照数据类型分:① 基本数据类型 ② 引用数据类型
- 按照在类中声明的位置分:① 成员变量:在使用前,都经历过默认初始化赋值
- 类变量: linking的prepare阶段:给类变量默认赋值 ---> initial阶段:给类变量显式赋值即静态代码块赋值
- 实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
- ② 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过
- */
- public void test5Temp(){
- int num;
- //System.out.println(num);//错误信息:变量num未进行初始化
- }
-
- }
使用javap -v LocalVariablesTest.class 命令或者 jclasslib工具(idea插件)可以看到
使用jclasslib插件查看
演示静态函数的字节码 代完成
上图中Start PC是变量作用域起始的位置,length是作用域的长度
关于Slot的理解
例:
例:
Slot的重复利用
栈帧中的局部变量表中的槽位是可以重用的。如果一个局部变量过了其作用域,那么在其作用域之后声明的新的局部变量就会有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
例:
例:
举例:静态变量与局部变量的对比
例:这样的代码是错误的,没有赋值不能够使用
变量的分类:
按照数据类型分:①基本数据类型 ②引用数据类型
按照在类中声明的位置分:①成员变量:在使用前,都经过默认初始化赋值(类变量-》linking的prepare阶段:给类变量默认赋值,initial阶段:给类变量显示赋值,即静态代码块赋值;实例变量-》随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值);②局部变量:在使用前必须要进行显示赋值,否则,编译不通过。
补充说明:
4.4.2 操作数栈 (Operand Stack)
栈:先进后出,后进先出,可以由数组和链表实现
例:
4.4.3 代码追踪
bipush:把15 放到操作数栈,此时pc寄存器的指令地址为0;接着pc寄存器指令地址下移到指令地址为2的指令,istore_1,i表示int类型,_1表示将栈顶数据拿出保存在局部变量表中索引为1的变量中;
iload_1、iload_2表示将局部变量表中索引为1和2的变量的值取出,以此入栈
iadd是将操作数栈中的数据依次出栈,进行add操作;istore_3表示将add的结果以int类型保存在局部变量表中索引为3的变量中;
带返回值的例子
--------在面试过程中经常遇到的i++和++i的区别 -------------
- package com.atguigu.java1;
-
- /**
- * @author shkstart
- * @create 2020 下午 10:25
- */
- public class OperandStackTest {
- public void testAddOperation() {
- //byte、short、char、boolean:都以int型来保存
- byte i = 15;
- int j = 8;
- int k = i + j;
-
- // int m = 800;
-
- }
-
- public int getSum(){
- int m = 10;
- int n = 20;
- int k = m + n;
- return k;
- }
-
- public void testGetSum(){
- //获取上一个栈桢返回的结果,并保存在操作数栈中
- int i = getSum();
- int j = 10;
- }
-
- /*
- 程序员面试过程中, 常见的i++和++i 的区别,放到字节码篇章时再介绍。
- */
- public void add(){
- //第1类问题:
- int i1 = 10;
- i1++;
-
- int i2 = 10;
- ++i2;
-
- //第2类问题:
- int i3 = 10;
- int i4 = i3++;
-
- int i5 = 10;
- int i6 = ++i5;
-
- //第3类问题:
- int i7 = 10;
- i7 = i7++;
-
- int i8 = 10;
- i8 = ++i8;
-
- //第4类问题:
- int i9 = 10;
- int i10 = i9++ + ++i9;
- }
- }
4.4.4 栈顶缓存技术(Top-of-stack Cashing)
前面提过,基于栈式架构的虚拟机所使用的领地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读写次数。
由于操作数栈是存储在内存中的,因此频繁地执行内存读写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(ToS,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理cpu的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。
4.4.5 动态链接 (Dynamic Linking)
动态链接(或指向运行时常量池的方法引用)
- package com.atguigu.java1;
-
- /**
- * @author shkstart
- * @create 2020 下午 10:25
- */
- public class DynamicLinkingTest {
-
- int num = 10;
-
- public void methodA(){
- System.out.println("methodA()....");
- }
-
- public void methodB(){
- System.out.println("methodB()....");
-
- methodA();
-
- num++;
- }
-
- }
javap -v DynamicLinkingTest.class
看methodB方法的字节码指令和动态链接
#开始的为符号引用,箭头没有说明的是直接引用
为什么需要常量池?
常量池的作用,就是为了提供一些符合和常量,便于指令的识别。
4.4.6 方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
对应的方法的绑定机制为:早起绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换直接引用的过程,这仅仅发生一次。
例:
- package com.atguigu.java2;
-
- /**
- * 说明早期绑定和晚期绑定的例子
- *
- * @author shkstart
- * @create 2020 上午 11:59
- */
- class Animal {
-
- public void eat() {
- System.out.println("动物进食");
- }
- }
-
- interface Huntable {
- void hunt();
- }
-
- class Dog extends Animal implements Huntable {
- @Override
- public void eat() {
- System.out.println("狗吃骨头");
- }
-
- @Override
- public void hunt() {
- System.out.println("捕食耗子,多管闲事");
- }
- }
-
- class Cat extends Animal implements Huntable {
-
- public Cat() {
- super();//表现为:早期绑定
- }
-
- public Cat(String name) {
- this();//表现为:早期绑定
- }
-
- @Override
- public void eat() {
- super.eat();//表现为:早期绑定
- System.out.println("猫吃鱼");
- }
-
- @Override
- public void hunt() {
- System.out.println("捕食耗子,天经地义");
- }
- }
-
- public class AnimalTest {
- public void showAnimal(Animal animal) {
- animal.eat();//表现为:晚期绑定
- }
-
- public void showHunt(Huntable h) {
- h.hunt();//表现为:晚期绑定
- }
- }
多态的调用 属于晚期引用
多个构造函数调用父类的构造方法或其他构造方法,即方法调用指定的方法属于早起绑定
随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是他们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早起绑定和晚期绑定两种绑定方式。
Java中任何一个普通的方法其实都具备虚函数的特征,他们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显示定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
虚函数就是在运行期才能确定下来的方法
虚方法与非虚方法:
非虚方法:
虚拟机中提供了以下几条方法调用指令:
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
例:
- package com.atguigu.java2;
-
- /**
- * 解析调用中非虚方法、虚方法的测试
- * <p>
- * invokestatic指令和invokespecial指令调用的方法称为非虚方法
- *
- * @author shkstart
- * @create 2020 下午 12:07
- */
- class Father {
- public Father() {
- System.out.println("father的构造器");
- }
-
- public static void showStatic(String str) {
- System.out.println("father " + str);
- }
-
- public final void showFinal() {
- System.out.println("father show final");
- }
-
- public void showCommon() {
- System.out.println("father 普通方法");
- }
- }
-
- public class Son extends Father {
- public Son() {
- //invokespecial
- super();
- }
-
- public Son(int age) {
- //invokespecial
- this();
- }
-
- //不是重写的父类的静态方法,因为静态方法不能被重写!
- public static void showStatic(String str) {
- System.out.println("son " + str);
- }
-
- private void showPrivate(String str) {
- System.out.println("son private" + str);
- }
-
- public void show() {
- //invokestatic
- showStatic("atguigu.com");
- //invokestatic
- super.showStatic("good!");
- //invokespecial
- showPrivate("hello!");
- //invokespecial
- super.showCommon();
-
- //invokevirtual
- showFinal();//因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。
- //虚方法如下:
- //invokevirtual
- showCommon();
- info();
-
- MethodInterface in = null;
- //invokeinterface
- in.methodA();
- }
-
- public void info() {
-
- }
-
- public void display(Father f) {
- f.showCommon();
- }
-
- public static void main(String[] args) {
- Son so = new Son();
- so.show();
- }
- }
-
- interface MethodInterface {
- void methodA();
- }
关于invokedynamic指令
动态类型语言和静态类型语言
动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
例:
- package com.atguigu.java2;
-
- /**
- * 体会invokedynamic指令
- * @author shkstart
- * @create 2020 下午 3:09
- */
- @FunctionalInterface
- interface Func {
- public boolean func(String str);
- }
-
- public class Lambda {
- public void lambda(Func func) {
- return;
- }
-
- public static void main(String[] args) {
- Lambda lambda = new Lambda();
-
- Func func = s -> {
- return true;
- };
-
- lambda.lambda(func);
-
- lambda.lambda(s -> {
- return true;
- });
- }
- }
-
Java语言中方法重写的本质:
IllegalAccessError介绍:
程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行期,就说明一个类发生了不兼容的改变。
需方发表
- package com.atguigu.java3;
-
- /**
- * 虚方法表的举例
- *
- * @author shkstart
- * @create 2020 下午 1:11
- */
- interface Friendly {
- void sayHello();
- void sayGoodbye();
- }
- class Dog {
- public void sayHello() {
- }
- public String toString() {
- return "Dog";
- }
- }
- class Cat implements Friendly {
- public void eat() {
- }
- public void sayHello() {
- }
- public void sayGoodbye() {
- }
- protected void finalize() {
- }
- public String toString(){
- return "Cat";
- }
- }
-
- class CockerSpaniel extends Dog implements Friendly {
- public void sayHello() {
- super.sayHello();
- }
- public void sayGoodbye() {
- }
- }
-
- public class VirtualMethodTable {
- }
方法返回地址(return address)
当一个方法开始执行后,只有两种方式可以退出这个方法:
方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。
例:
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的却别在于:通过异常完成出口退出的不会给他的上层调用者产生任何返回值。
例:
- package com.atguigu.java3;
-
- import java.io.FileReader;
- import java.io.IOException;
- import java.util.Date;
-
- /**
- *
- * 返回指令包含ireturn(当返回值是boolean、byte、char、short和int类型时使用)、
- * lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、
- * 实例初始化方法、类和接口的初始化方法使用。
- *
- * @author shkstart
- * @create 2020 下午 4:05
- */
- public class ReturnAddressTest {
- public boolean methodBoolean() {
- return false;
- }
-
- public byte methodByte() {
- return 0;
- }
-
- public short methodShort() {
- return 0;
- }
-
- public char methodChar() {
- return 'a';
- }
-
- public int methodInt() {
- return 0;
- }
-
- public long methodLong() {
- return 0L;
- }
-
- public float methodFloat() {
- return 0.0f;
- }
-
- public double methodDouble() {
- return 0.0;
- }
-
- public String methodString() {
- return null;
- }
-
- public Date methodDate() {
- return null;
- }
-
- public void methodVoid() {
-
- }
-
- static {
- int i = 10;
- }
-
-
- //
- public void method2() {
-
- methodVoid();
-
- try {
- method1();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- public void method1() throws IOException {
- FileReader fis = new FileReader("atguigu.txt");
- char[] cBuffer = new char[1024];
- int len;
- while ((len = fis.read(cBuffer)) != -1) {
- String str = new String(cBuffer, 0, len);
- System.out.println(str);
- }
- fis.close();
- }
-
-
- }
4.4.8 一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
4.4.9 栈的相关面试题
例:
- package com.atguigu.java3;
-
- /**
- * 面试题:
- * 方法中定义的局部变量是否线程安全?具体情况具体分析
- *
- * 何为线程安全?
- * 如果只有一个线程才可以操作此数据,则必是线程安全的。
- * 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。
- * @author shkstart
- * @create 2020 下午 7:48
- */
- public class StringBuilderTest {
-
- int num = 10;
-
- //s1的声明方式是线程安全的
- public static void method1(){
- //StringBuilder:线程不安全
- StringBuilder s1 = new StringBuilder();
- s1.append("a");
- s1.append("b");
- //...
- }
- //sBuilder的操作过程:是线程不安全的
- public static void method2(StringBuilder sBuilder){
- sBuilder.append("a");
- sBuilder.append("b");
- //...
- }
- //s1的操作:是线程不安全的
- public static StringBuilder method3(){
- StringBuilder s1 = new StringBuilder();
- s1.append("a");
- s1.append("b");
- return s1;
- }
- //s1的操作:是线程安全的
- public static String method4(){
- StringBuilder s1 = new StringBuilder();
- s1.append("a");
- s1.append("b");
- return s1.toString();
- }
-
- public static void main(String[] args) {
- StringBuilder s = new StringBuilder();
-
-
- new Thread(() -> {
- s.append("a");
- s.append("b");
- }).start();
-
- method2(s);
-
- }
-
- }
设么是本地方法?
简单的讲,一个Native Method就是一个Jva调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现是有非Java语言实现,比如C。这个特征并发Java所特有,很多其他的编程语言都有这一机制,比如在C++中,你可以用extern“C”告知C++编译器去调用一个C函数。
“A native ,ethdo is a Java method whose implementation is ptovided by non-java code.”
在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是有非java语言在外面实现的。
本地接口的作用时融合不同的编程语言为Java所用,它的初衷时融合C/C++程序。
为什么要使用Native Method?
Java使用起来分成方便,然后有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
现状: 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。