2020-10-28 JVM基础

java程序是如何加载运行的

JDK把.java源程序编译成.class文件,jre将.class也就是字节码文件加载进JVM虚拟机。

认识jdk和jre的区别

JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它包含JRE,还包括编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。

JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但

是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译servlet。

java的跨平台优势

在java中,java能够处理的文件叫做字节码文件,他不面向任何的处理器,只面向java虚拟机。不同的操作系统,例如MAC、linux对同一个指令有着不同的解释,java实现了“一次编译,多处运行”。java开发者开发出了在不同操作系统上运行的JVM虚拟机,将字节码文件加载到虚拟机当中去,然后java虚拟机将其编译成所在操作系统可以正常识别并且执行的命令。

java在计算机中运行的三个阶段

java八种基本数据类型

整形类型数据变量范围

  • 整型类变量用来存储整数数值,即没有小数部分的值
  • 整数类型分四中不同的类型:字节型(byte)、短整型(short)、整型(int)、长整型(long)
    在这里插入图片描述
  • 在为一个long类型的变量赋值时需要注意一点,所赋值的后面要加上一个字母L(或小写l),说明赋值为long类型。如果赋的值未超出int型的取值范围,可以省略字母L(或小写l)。例如:
long  num =2200000000L;   // 所赋的值超出了int型的取值范围,后面必须加上字母L
long  num =198L;          // 所赋的值未超出int型的取值范围,后面可以加上字母L
long  num =198;           // 所赋的值未超出int型的取值范围,后面可以省略字母L

3.浮点类型变量

  • 浮点类型变量用来存储小数数值,浮点数不能用来表示精准的值,如:货币;
  • 浮点类型分为两种:单精度浮点(float)、双精度浮点(double)
  • double型所表示的浮点数比float型更精确;浮点数的默认类型为double类型。
    在这里插入图片描述
    - 在Java中,一个小数会被默认为double类型的值,因此在为一个float类型的变量赋值时需要注意一点,所赋值的后面要加上字母F(或小写f),而为double类型的变量赋值时,可以在所赋值的后面加上字母D(或小写d),也可以不加。例如:
float  f = 123.4f;      //为一个float类型的变量赋值,后面必须加上字母f;
double  d1=100.1;       //为一个double类型的变量赋值,后面可以省略字母d;
double  d2=199.3d;      //为一个double类型的变量赋值,后面可以加上字母d;
  • 也可以为一个浮点类型变量赋予一个整型数值
float  f = 100;        //声明一个float类型的变量并赋整数值;
double  d =100;        //声明一个double类型的变量并赋整数值;

4.字符类型变量

  • 字符类型变量用于存储一个单一字符,Java中用char表示。
  • Java中,每个char类型的字符变量都会占用2个字节,16位。
  • char类型的变量赋值时,需要英文的单引号’’把字符括起来,如’a’。
  • char类型的变量赋值范围是0~65535内的整数。
  • 最小值是\u0000(即为0);最大值是\uffff(即为65535)。
  • char数据类型可以存储任何字符。
//字符类型 ,占2个字节,16位   包装类 Character
char c = ‘a’;          //为一个char类型的变量赋值字符a;
char ch = 97;          //为一个char类型的变量赋值整数97,相当于赋值字符a;
char h='\r';           //特殊的转义字符         
char i='\u9990';       //Unicode字符集 \u0000‐\uFFFF         
char j=65535;          //字符0到 65535

在这里插入图片描述
5.布尔类型变量

  • 布尔类型变量用来存储布尔值,在Java中用boolean表示,boolean类型的变量只有两个值,即true和false,默认值为false。
  • boolean数据类型表示一位的信息
boolean  flag = false;    //声明一个boolean类型的变量,初始值为false;
flag = true;              //改变flag变量的值未true;

三、为什么byte类型数据范围是-128到127

public static void main(String[] args) { byte a = 127; byte b = (byte)(a+1); System.out.println(b); }

1.计算机中对数据的二进制存储形式-------补码(下面讨讨论的皆为byte类型)

原码:45:00101101 -45:10101101

在最高位代表符号位区分正数还是负数,0代表正数,1代表负数

反码:45:00101101 -45:11010010

正数的原码和反码相同,负数的反码等于原码的符号位不变,其余各位按位取反

补码:45:00101101 -45:11010011

正数的原码反码和补码都形同,负数的补码等于在其反码基础上末尾+1

2.为什么计算机设计反码?

因为计算机只有加法没有,减法,在做减法运算的时候,可以认为是加上一个负数,这样可以减少计算机电路的复杂度。使用原码进行减法运算会出现问题,例如计算1-1,因为计算机没有加法只有减法,所以计算机自动换算成1+(-1)

1-1=1+(-1)=[00000001]原+[10000001]原=[10000010]原=-2 (符号位也参与运算)

与实际结构不符

-------------------------------------------------------------------------------------

1-1=1+(-1)=[00000001]原+[10000001]原=[00000001]反+[11111110]反=[11111111]反=[10000000]原=-0

通过反码计算的结果是11111111在计算一次反就成原码了,得出的结果是正确的,但是有一个问题是 00000000可以代表+0 10000000可以代表-0,其实是一样的,用2个编码实在是浪费。于是出现了补码解决0的符号以及两个编码的问题

--------------------------------------------------------------------------------------------

这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了

1-1=1+(-1)=[00000001]原+[10000001]原=[00000001]补+[11111111]补=[00000000]补=[00000000]原

这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了.

------------------------------------------------------------------------------------------------

总结:反码是为了解决减法运算,补码是为了解决反码产生的+-0的问题

3.为什么byte类型是127 -- (-128)

首先我们用原码表示这么几个我们常见的数

+127:0111 1111、

+1:0000 0001、

+0:0000 0000、

-0:1000 0000、

-1:1000 0001、

-127:1111 1111

那么计算机为什么要表示一个-0呢?总不能把这两个都对应0吧,这显然是资源浪费。

到这里我们都认为最高位是不参与计算数值的,仅仅是一个符号位,按这种思路byte的八位是无论如何也表示不出-128。而“可怜的-0”又不知道自己代表谁,不得而知“可怜的-0”就是-128。

于是我们的计算机当中规定-0的补码就是-128在计算机当中的存储方式。这样也可好可以满足计算机当中的计算。

例如:-128 + 127 = 10000000(-1)

-----------------------------------------------------------------------------------------------------------------

四、浮点类型以及精度丢失

1.浮点类型在计算机当中的存储

首先我们来看一下浮点类型在我们的计算机当中是如何存储的

float存储需求是4字节(32位), 其中1位最高位是符号位,中间8位表示阶位,后32位表示值

double存储需求是8字节(64为),其中1位最高位是符号位,中间8位表示阶位,后52位表示值

2.浮点类型的精度丢失

①:产生精度丢失的原因

首先我们看一个例子

public class SimpleTest { public static void main(String[] args) { System.out.println(1.2 - 1); } }

输出:

0.19999999999999996

这和我们预期的情况完全不符

我们来将10进制的0.2转化为2进制进行存储

 算法是乘以2直到没有了小数为止

0.2 * 2 = 0.4 取整数部分 0

0.4 * 2 = 0.8 取整数部分 0

0.8 * 2 = 1.6 取整数部分 1

0.6 * 2 = 1.2 取整数部分 1

0.2 * 2 = 0.4 取整数部分 0

。。。。。。

0.2的2进制从上到下可以表示为 00110......

 注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去。很显然,小数的二进制表示有时是不可能精确的 。其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统也无法准确表示1/10。这也就解释了为什么浮点型减法出现了"减不尽"的精度丢失问题。

还有更多请看:

https://www.cnblogs.com/straybirds/p/6295036.html?utm_source=itdadao&utm_medium=referral

精度丢失就是我们的位数不够表示我们整个数值了

②:如何解决精度丢失

    public static void main(String[] args) {
        String name="123";
        People people = new People("张三",18,10000000,"男",name.charAt(0));
        System.out.println(people.toString());
        BigDecimal big1 = new BigDecimal(Float.toString(1.2f));
        BigDecimal big2 = new BigDecimal(Float.toString(1));

        int sum=100;
        String message="1000";
        char c1=70;
        float r1 = big1.subtract(big2).floatValue();
        String json1 ="{\"count\":\""+sum+"\",\"message\":\""+message+"\",{\"row\";[\""+message+"\",\\\"\"+message+\"\\\",]}}";
        System.out.println(c1);
        System.out.println(json1);
    }

BigDecimal可以用于数据的准确运算。

JVM虚拟机

IDEA中编译完成的class文件可以通过Terminal中的 D:\IDEA project\10-11Servlet\HellowServlet\target\classes\com\kyz\pojo>javap -c StackClass.class > StackClass.txt 指令反编译成我们可以看的懂的机器指令。

一个简单的乘法和加法。

public class StackClass {
    public static void main(String[] args) {
        int a= 100001;
        int b= 200001;
        int c= 79;
        int d = a+b*c;
        System.out.println(d);
    }
}

经过编译之后,成了这样的字节码文件。这里也可以看出jvm基于栈操作,指令比较多,所以移植性比较好,而安卓基于寄存器操作,性能好需要硬件支持,移植性差。

Compiled from "StackClass.java"
public class com.kyz.pojo.StackClass {
  public com.kyz.pojo.StackClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: sipush        10001         //入栈
       3: istore_1                         //将栈顶的值存入局部变量1
       4: sipush        20001        
       7: istore_2                
       8: bipush        79             //带符号扩展入栈
      10: istore_3               
      11: iload_1                       //从局部变量中取值入栈
      12: iload_2
      13: iload_3
      14: imul                            //将栈顶两long值相乘入栈
      15: iadd
      16: istore        4
      18: getstatic     #2               获取静态字段的值   // Field java/lang/System.out:Ljava/io/PrintStream;
      21: iload         4
      23: invokevirtual #3             运行时方法绑定调用方法     // Method java/io/PrintStream.println:(I)V
      26: return                             void函数返回。  
}

这里的istore将值存入就是一个把操作数栈的值加到局部变量表的过程

字节码执行引擎会调整每个线程的PC。

动态链接暂且不讲。

方法出口:调用方法的时候,返回结束时main应该要继续执行的位置。

这里的User互相对应。

 

JVM内部组成

当一个方法执行的时候,会从VM Stack中分配出一个空间,我们称为栈帧分配给这个方法,在new 对象时会在Heap中创建该对象,堆中主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。VM Stack中存放对象的内存地址。

VM Stack

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表操作数栈动态链接方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

运行时常量池

属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

 直接内存

非虚拟机运行时数据区的部分


对象的创建

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

类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。

前面讲的每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。

内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。

执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。

对象的内存布局

在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)实例数据(Instance Data)对齐填充(Padding)

对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

对象的访问定位

使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。

通过句柄访问

Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。详情见图。

 


 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值