一、了解Java虚拟机与跨平台原理

虚拟机

     java虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等 还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序经过编译器编译后生成字节码文件,就可以在安装JVM虚拟机后的平台上不加修改地运行。

平台原理

     平台原理: java是可以跨平台的编程语言,所谓平台就是CPU处理器与操作系统的整体。每个公司生产的CPU使用相同或不同的指令集。

    指令集:就是CPU中用来计算和控制计算机系统的一套指令的集合,每种Cpu都有其特定的指令集。

    操作系统: 操作系统是充当是充当用户和计算机交互的界面软件,不同的操作系统支持不同的CPU,严格说不同的操作系统支持不同的CPU指令集。因为现在主流的操作系统都支持主流的CPU,所以有时也将操作系统称为平台。

跨平台原理

   跨平台原理: 使用特定编译器编译的程序只能在对应的平台运行,这里也可以说编译器是与平台相关的,编译后的文件也是与平台相关的。我们说的语言跨平台是编译后的文件跨平台,而不是源程序跨平台,如果是源程序,任何一门语言都是跨平台的语言了。

平台相关C与Java跨平台

    第一,C语言是编译执行的,编译器与平台相关编译生成的可执行文件与平台相关

    第二,Java是解释执行的,编译为中间码的编译器与平台无关,编译生成的中间码也与平台无关(一次编译,到处运行),中间码再由解释器解释执行,解释器是与平台相关的,也就是不同的平台需要不同的解释器.

​     这里再说下语言根据执行方式的不同分类:

第一是编译执行,如上文中说到的C,它把源程序由特定平台的编译器一次性编译为平台相关的机器码,它的优点是执行速度快,缺点是无法跨平台

第二是解释执行,如HTML,JavaScript,它使用特定的解释器,把代码一行行解释为机器码,类似于同声翻译,它的优点是可以跨平台,缺点是执行速度慢,暴露源程序

第三种是从Java开始引入的“中间码+虚拟机”的方式,它既整合了编译语言与解释语言的优点,同时如虚拟机又可以解决如垃圾回收,安全性检查等这些传统语言头疼的问题。Java先编译后解释 同一个.class文件在不同的虚拟机会得到不同的机器指令(Windows和Linux的机器指令不同),但是最终执行的结果却是相同的

优点:

  • 一次编写 到处运行
  • 自动内存管理 垃圾回收功能
  • 数组下标越界检查
  • 多态

内存结构:

  • 程序计数器(寄存器)
  • 虚拟机栈
  • 本地方法栈
  • 方法区

1.程序计数器

流程:Java源代码先生成二进制字节码 (jvm指令)

          ——》解释器将每条jvm指令解释为机器码

          ——》cpu最后执行机器码

作用:记住下一条jvm指令的执行地址 

特点:线程私有的 不会存在内存溢出

2.虚拟机栈

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 每个栈帧调用后会在栈中释放出来
  • 栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口灯信息。局部变量表又包含基本数据类型,对象引用类型(局部变量表编译器完成,运行期间不会变化)

2.1 问题辨析:

1.垃圾回收是否涉及栈内存?

​        不涉及栈内存,因为栈内存中主要存放栈帧,每个栈帧在调用完后会自动释放

2.栈内存分配越大越好吗?

        不是,因为栈内存越大,则分配的内存越大,相应CPU可调用的线程数会越少

3.方法内的局部变量是否线程安全?

     ​ 是线程安全 例(int a=3); ​ 如果是static 或string 的话,则不是线程安全 ​ 即如果是公有的,则需要考虑线程安全,如果是线程私有的则不用考 虑线程安全 ​ 如果方法内的局部变量没有逃离方法的访问范围,它是线程安全的 ​ 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

4.栈内存溢出

​       1.栈帧过多,导致栈内存溢出 ​ 如果某个栈帧一直被调用而未被释放,则会导致栈内存溢出 ​ 例如:错误的递归调用

      ​ 2.栈帧过大导致栈内存溢出(很少出现)

3.本地方法栈

 本地方法栈: java虚拟机在调用一些本地方法的时候,需要给其提供的本地栈内存空间

 本地方法: 

       定义: 不是由java编写的代码,由C或C编写的一些方法

       作用: Java代码有时不能直接与我们的操作系统底层打交道,所以就需要由c或C编写的代码进行操作,Java方法可以通过调用本地方法来实现与操作系统的操作 例如:Object类中的 wait() notify() hashcode()等方法.

4.堆

  • 通过new 关键字,创建的对象都会使用堆内存
  • 它是线程共享的,堆中的对象都需要考虑线程安全的问题
  • 有垃圾回收机制

4.1 问题分析

1.堆内存溢出

​    不断产生新的对象 例如: 因为string每一次拼接都需要产生一个新的对象,所以在这个死循环内会不断产生新的对象,直至抛出堆内存溢出异常

5.方法区

  • 被所有线程共享
  • 在JVM虚拟机运行时创建
  • 1.8版本后存在元空间内存溢出 (1.8版本后是由元空间实现的)

静态变量 + 常量 + 类信息(构造方法/接口定义) + 运行时常量池存在方法区中

类信息与类常量池

 方法区里的class文件信息包括:魔数,版本号,常量池,类,父类和接口数组,字段,方法等信息,其实类里面又包括字段和方法的信息。

class文件常量池中存储了哪些内部呢?

     我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);每个class文件都有一个class常量池。

动态常量池

   运行时常量池方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。

静态常量池和动态常量池的关系以及区别?

      静态常量池存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量符号引用其实引用的就是常量池里面的字符串,但符号引用不是直接存储字符串,而是存储字符串在常量池里的索引

     动态常量池是当class文件被加载完成后,java虚拟机会将静态常量池里的内容转移到动态常量池里,在静态常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。

5.1 本节总结

   方法区里存储着class文件信息和动态常量池,class文件的信息包括类信息和静态常量池。可以将类的信息是对class文件内容的一个框架,里面具体的内容通过常量池来存储

   动态常量池里的内容除了是静态常量池里的内容外,还将静态常量池里的符号引用转变为直接引用,而且动态常量池里的内容是能动态添加的。例如调用String的intern方法就能将string的值添加到String常量池中,这里String常量池是包含在动态常量池里的,但在jdk1.8后,将String常量池放到了堆中

5.2 jvm中的常量池

在Java的内存分配中,总共3种常量池:

字符串常量池(String Constant Pool)、class常量池 、运行时常量池(包含stringtable)

字符串常量池在Java内存区域的哪个位置? 在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;

在 Java 7 之前,String Pool 被放在运⾏时常量池中,它属于永久代。⽽在 Java 7,String Pool 被移到 堆中。这是因为永久代的空间有限,在⼤量使⽤字符串的场景下会导致 OutOfMemoryError 错误。

StringTable特性(字符串常量池)

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池

1.8 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回

1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

可用于StringTable调优

  • 字符串变量拼接的原理是StringBulider(1.8)
  • 字符串常量拼接的原理是编译期的优化
  • 具有垃圾回收机制(1.6放于方法区,1.8后放于堆中)
        String s1="1";
        String s2="2";
        String s3="12";
        //字符串变量的拼接
        // new StringBuild().append("1").append("2").toString();
        // 又new String() 存放于堆内存中,产生新的对象
        String s4=s1+s2;
       //字符串常量的拼接
       //字符串常量拼接的原理是编译期的优化 ,若常量池中已存在,则不产生新的对象
        String s5="1"+"2";
        System.out.println(s3==s4);   // false
        System.out.println(s3==s5);  //true

实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个虚拟机中的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。

    在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;

 在JDK7.0中,StringTable的长度可以通过参数指定:

         -XX:StringTableSize=66666

字符串常量池里放的是什么?

  在JDK6.0及之前版本中,String Pool里放的都是字符串常量

  在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。关于String在内存中的存储和String#intern()方法的说明,可以参考我的另外一篇博客:

         需要说明的是:字符串常量池中的字符串只存在一份!

如:

String s1 = "hello,world!"; String s2 = "hello,world!"; 即执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2.

常量池的好处

    常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享

   例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。

 (1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。

 (2)节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等。

6.直接内存

  • Direct Memory 属于系统内存
  • 常见NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理
  • 直接内存释放内存需要调用unsafe.freeMemory(base); 不建议程序员自己调用,JDK自动调用的一个方法

6.1两种IO操作区别分析

1、不使用直接内存

  • 首先在磁盘文件中读取到系统缓冲区 (系统内存)
  • 再从系统缓冲区读取到java缓冲区 byte

 2.使用直接内存

  • 通过ByteBuffer.allocateDirect(_1mB)开辟一个直接内存(direct memory)大小1mb
  • 这个开辟的直接内存,可以由java代码访问,即系统内存可以使用,虚拟机也可以使用这片区域
  • 磁盘文件中的内容读写到直接内存中,Java代码就可以访问了,减少了一次读写操作
import sun.misc.Unsafe;

import java.io.IOException;
import java.lang.reflect.Field;

public class Main {
    static int _1gb=1024*1024*1024;
    public static void main(String[] args) throws IOException {
	// write your code here
        Unsafe unsafe=Unsafe.getUnsafe();
        //分配内存
        long base=unsafe.allocateMemory(_1gb); //开辟一个1gb大小的直接内存
        unsafe.setMemory(base,_1gb,(byte)0);
        System.in.read();
        //释放内存
        unsafe.freeMemory(base);
        System.in.read();


    }
    public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
        Field f= Unsafe.class.getDeclaredField("theUnsafee");
        f.setAccessible(true);
        Unsafe unsafe=(Unsafe)f.get(null);
        return unsafe;
    }
}

二、区分JDK与JRE

2.1 JRE是java的运行环境

 ​ jre面向java程序的使用者,而不是开发者,jre是运行Java程序所必须环境的集合 jre=jvm+java核心类库它不包括开发工具(编译器、调试器等)

​ JDK是java开发工具: jdk提供了Java的开发环境(提供了编译器javac等工具,用于将Java文件编译为class文件)和运行环境(提供了jvm和runtime辅助包,用于解析class文件使其的到运行)如果你下载并安装了jdk,那么你既可以开发程序也可以运行程序。jdk=jre+java工具包+Java标准类库

2.2 JRE是java的区别

(1)JRE主要包含:

      java类库的class文件(都在lib目录下打包成了jar)和虚拟机(jvm.dll);

      JDK主要包含:java类库的 class文件(都在lib目录下打包成了jar)并自带一个JRE。

     那么为什么JDK要自带一个JRE呢?

    而且jdk/jre/bin下的client 和server两个文件夹下都包含jvm.dll(说明JDK自带的JRE有两个虚拟机)。

(2)如果一台电脑安装两套以上的JRE,谁来决定呢?

      这个重大任务就落在java.exe身上。java.exe的工作就是找到合适的JRE来运 行Java程序。java.exe依照以下的顺序来查找JRE:

   1)自己的目录下有没有JRE;

   2)父目录有没有JRE;

   3)查询注册表: [HKEY_LOCAL_MACHINE/SOFTWARE/JavaSoft/Java Runtime Environment]。所以java.exe的运行结果与你的电脑里面哪个JRE被执行有很大的关系。

(3)JDK-->JRE-->Bin目录下有两个文件夹:server与client,这是真正的jvm.dll所在。 jvm.dll无法单独工作,当jvm.dll启动后,会使用explicit的方法(就是使用Win32 API之中的LoadLibrary()与GetProcAddress()来载入辅助用的动态链接库),而这些辅助用的动态链接库(.dll)都必须位 于jvm.dll所在目录的父目录之中。因此想使用哪个JVM,只需要设置PATH,指向JRE所在目录下的jvm.dll。

2.3 理解Java编译原理

   Javac是一种编译器,能将一种语言规范转化成另外一种语言规范,通常编译器都是将便于人理解的语言规范转化成机器容易理解的语言规范,如C/C++或者汇编语言都是将源代码直接编译成目标机器码,这个目标机器代码是CPU直接执行的指令集合。这些指令集合也就是底层的一种语言规范。

     Javac的编译器也是将Java这种对人非常友好的编程语言编译成对对所有机器都非常友好的一种语言。这种语言不是针对某种机器或某个平台。怎么消除不同种类,不同平台之间的差异这个任务就有JVM来完成,而Javac的任务就是将Java源代码语言转化为JVM能够识别的一种语言,然后由JVM将JVM语言再转化成当前这个机器能够识别的机器语言。 Javac的任务就是将Java源代码编译成Java字节码,也就是JVM能够识别的二进制代码,从表面看是将.java文件转化为.class文件。而实际上是将Java源代码转化成一连串二进制数字,这些二进制数字是有格式的,只有JVM能够真确的识别他们到底代表什么意思。

2.4 Java编译与反编译

编译:将源文件(.Java)转换成字节码文件(.class)的过程称为编译

反编译:将字节码文件转换为源文件(.java)的过程称为反编译

              可用工具:jad、frontend

三、Java语言的四大特征:

3.1 封装

   首先,属性能够描述事物的特征,方法能够描述事物的动作。封装就是把同一类事物的共性(包括属性和方法)归到同一类中,方便使用。

封装:封装也称信息隐藏,是指利用抽象数据类型把数据和基于数据的操作封装起来,使其成为一个不可分割的整体,数据隐藏在抽象数据内部,尽可能的隐藏数据细节,只保留一些接口使其与外界发生联系。也就是说用户无需知道内部的数据和方法的具体实现细节,只需根据留在外部的接口进行操作就行。 为了实现良好的封装,我们通常将类的成员变量声明为private,在通过public方法来对这个变量来访问。对一个变量的操作,一般有读取和赋值2个操作,,我们分别定义2个方法来实现这2个操作,一个是getXX(XX表示要访问的成员变量的名字)用来读取这个成员变量,另一个是setXX()用来对这个变量赋值。

3.2  继承

Java继承 Java继承是面向对象的最显著的一个特征。继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。JAVA不支持多继承,单继承使JAVA的继承关系很简单,一个类只能有一个父类,易于管理程序,父类是子类的一般化,子类是父类的特化(具体化) 继承所表达的就是一种对象类之间的相交关系,它使得某类对象可以继承另外一类对象的数据成员和成员方法。若类B继承类A,则属于B的对象便具有类A的全部或部分性质(数据属性)和功能(操作),我们称被继承的类A为基类、父类或超类,而称继承类B为A的派生类或子类。 继承避免了对一般类和特殊类之间共同特征进行的重复描述。同时,通过继承可以清晰地表达每一项共同特征所适应的概念范围——在一般类中定义的属性和操作适应于这个类本身以及它以下的每一层特殊类的全部对象。运用继承原则使得系统模型比较简练也比较清晰。

3.3  多态

​ 方法的重写、重载与动态连接构成多态性; Java之所以引入多态的概念,原因之一是它在类的继承问题上和C不同,后者允许多继承,这确实给其带来的非常强大的功能,但是复杂的继承关系也给C开发者带来了更大的麻烦。 为了规避风险,Java只允许单继承。这样做虽然保证了继承关系的简单明了,但是势必在功能上有很大的限制,所以,Java引入了多态性的概念以弥补这点的不足,此外,抽象类和接口也是解决单继承规定限制的重要手段。同时,多态也是面向对象编程的精髓所在。

      要理解多态性,首先要知道什么是“向上转型”。 我定义了一个子类Cat,它继承了Animal类,那么后者就是前者的父类。我可以通过 Cat c = new Cat(); 例化一个Cat的对象,这个不难理解。 但当我这样定义时: Animal a = new Cat(); 父类引用只能调用父类中存在的方法和属性,不能调用子类的扩展部分;因为父类引用指向的是堆中子类对象继承的父类;

      但是如果强制把父类转换成子类的话,就可以调用子类中新添加而超类没有的方法了(即进行向下转型) 同时,父类中的一个方法只有在父类中定义而在子类中没有重写的情况下,才可以被父类类型的引用调用; 对于父类中定义的方法,如果子类中重写了该方法,那么父类类型的引用将会调用子类中的这个方法,这就是动态连接。

// 定义父类 text01
public class text01 {
	public int a=1;
  public void showa() {
	  System.out.println("父类:A");
  }
  public void showb() {
	  System.out.println("父类:B");
  }
  public void showc() {
  	System.out.println("父类:C");
  }
}
// 定义子类text2并继承父类text01 
public class text2 extends text01 {
    public void showa() {  
      System.out.println("子类:A");	
    }
    public void showb() {
  	  System.out.println("子类:B");
    }
    public void showd() {
    	System.out.println("子类:D");
    }
}
// 实现
public class text {
	public static void main(String[] args) {
		 text01 t1=new text2();
		 t1.showa();
		 t1.showb();
		 t1.showc();
		 System.out.println("----------------------------------");
		text2 t2= (text2) t1;
		 t2.showa();
		 t2.showb();
		 t2.showc();
		 t2.showd();
	}      
}
//结果
子类:A
子类:B
父类:C
----------------------------------
子类:A
子类:B
父类:C
子类:D

上一篇:计算机网络讲解 ( 看完让面试官直呼WC!!!内行)

下一篇:二、深究JVM垃圾回收(保姆式讲解,内附大量图解!!!)​​​​​​​

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐