1.1-1.2 Java和JVM

200 阅读12分钟

Java 基础+集合+多线程+JVM

1.1 java 基础

1.1.1 java 语言的三大特性

封装:抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。

继承:承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。

多态:一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

1.1.2 面向对象和面向过程

面向过程注重于每一个步骤和顺序,面向对象注重于参与者,以及各个参与者需要做什么

1.1.3 八种基本数据类型

类型字节数
boolean?
byte1
char2
short2
int4
long8
float4
double8

1.1.3.1 包装类和基本数据类型

  1. ​ 最开始赋值时候, 基本上数据类型都是0, 包装类是 null
  2. ​ 八种基本数据类型不是对象,包装类是对象
  3. ​ 存储位置不同
  4. ​ 包装类存在 常量池 int 会将 -128 到 127 的数据进行缓冲

  1. 无论如何,Integer与new Integer不会相等。不会经历拆箱过程,new出来的对象存放在堆,而非new的Integer常量则在常量池(在方法区),他们的内存地址不一样,所以为false。
  2. 两个都是非new出来的Integer,如果数在-128到127之间,则是true,否则为false。因为java在编译Integer i2 = 128的时候,被翻译成:Integer i2 = Integer.valueOf(128);而valueOf()函数会对-128到127之间的数进行缓存。
  3. 两个都是new出来的,都为false。还是内存地址不一样。
  4. int和Integer(无论new否)比,都为true,因为会把Integer自动拆箱为int再去比。

1.1.4 重载和重写

  1. 重载就是同样的⼀个⽅法能够根据输⼊数据的不同,做出不同的处理

  2. 重写就是当⼦类继承⾃⽗类的相同⽅法,输⼊数据⼀样,但要做出有别于⽗类的响应时,你就要覆盖⽗类⽅法

    区别:重载 发生在 编译时候 重写 发生在运行时候

image-20210510102523809.png

1.1.5 Sting 类型

1.1.4.1 String,StringBuffer、StringBuilder

	1. String 底层是一个 final 修饰的 char[] 所以String 是不可变的
	2. StringBuilder 与 StringBuffer  都是 char[] 但是没有采用 final 修饰

1.1.4.2 线程安全问题

​ String 对象不可变,每一次修改都会创建一个新的 String 所以是线程安全

​ StringBuilder 线程不安全

​ StringBuffer 线程安全 =====》源码 采用了 Synchronized 进行修饰

1.1.4.3 String 的hashcode

String类中的hashCode计算方法还是比较简单的,就是以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模。

哈希计算公式可以计为s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

关于为什么取31为权,可以参考StackOverflow上的这个问题

1.1.4.4 String 的拼接问题

参考

1. String字符串在内存中分配
	String s1 = new String("Hello world");
        String s2 = "Hello world";
        System.out.println(s1 == s2);  //false
        System.out.println(s1.equals(s2)); //true

image.png 在jdk1.8 里面,字符串常量池在堆中,运行时常量池在原空间中(上图有点问题,不过意思是对的)

2.字符串常量拼接
String str3 = "Hello"+" word";
String str4 = "Hello word";
System.out.println(str3 == str4); //true

上面是字符串常量拼接的例子:在编译时,JVM编译器对字符串做了优化,str3就被优化成“Hello word”,str3和str4指向字符串常量池同一个字符串常量,所以==比较为true

3、字符串常量+字符串变量、字符串变量之间的拼接
                String str5 = "Hello";
		String str6 = " word";
		String str7 = "Hello word";
		String str8 = str5+" word";
		System.out.println(str7 == str8);  //false

String通过+号来拼接字符串的时候,如果有字符串变量参与,实际上底层会转成通过StringBuilder的append( )方法来实现,大致过程如下

                StringBuilder sb = new StringBuilder( );
		sb.append(str5);
		sb.append(" word");
		str8 = sb.toString();

StringBuilder 的 toString( )方法底层new了一个String对象,所以str8在堆内存中重新开辟了新空间,而str7指向常量池,所以str7 == str8为false。 变量字符串拼接和常量字符串拼接结果是不一样的。因为变量字符串拼接是先开辟空间,然后再拼接。

1.1.6 接口和抽象类

  1. 接⼝中除了 static 、 final 变量,不能有其他变量,⽽抽象类中则不⼀定
  2. ⼀个类可以实现多个接⼝,但只能实现⼀个抽象类。接⼝⾃⼰本身可以通过 extends 关键字扩展多个接⼝。
  3. 接⼝⽅法默认修饰符是 public ,抽象⽅法可以有 public 、 protected 和 default 这些修饰符(抽象⽅法就是为了被重写所以不能使⽤ private 关键字修饰!
  4. 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,⽽接⼝是对⾏为的抽象,是⼀种⾏为的规范。

1.1.7 final 关键字

final 关键字主要⽤在三个地⽅:变量、⽅法、类。

  1. 对于⼀个 final 变量,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。

  2. 当⽤ final 修饰⼀个类时,表明这个类不能被继承。final 类中的所有成员⽅法都会被隐式地指定为 final ⽅法。

  3. 使⽤ final ⽅法的原因有两个。第⼀个原因是把⽅法锁定,以防任何继承类修改它的含义;

    第⼆个原因是效率。在早期的 Java 实现版本中,会将 final ⽅法转为内嵌调⽤。但是如果⽅法过于庞⼤,可能看不到内嵌调⽤ 带来的任何性能提升(现在的 Java 版本已经不需要使⽤final ⽅法进⾏这些优化了)。类中所有的 private ⽅法都隐式地指定为 final

1.2 JVM

1.2.1 为什么 编译和解释 共存

image-20210510100432980.png

为什么是解释和编译共存: 热点代码,引进了 JIT 编译器,⽽ JIT 属于运⾏时编译。当 JIT 编译器完成第⼀次编译后,其会将字节码对应的机器码保存下来,下次可以直接使⽤。⽽我们知道,机器码的运⾏效率肯定是⾼于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语⾔

java 中的编译:java文件 通过javac 编译变成 .class 文件, 引入热点代码,采用 JIT编译器(有两个点!!)

java 中的解释:通过 JVM 一条条解释成为 机器可以执行的二进制

1.2.2 对象结构

image-20210510182006952.png

记住对象头:Mark Word Klass Word 数组的长度

1.2.3 内存模型

image-20210511143015883.png

虚拟机栈出现的错误:StackOverFlowError,OutOfMemoryError

本地方法栈出现错误:StackOverFlowError,OutOfMemoryError

堆中出现的错误 :OutOfMemoryError: Heap Space OutOfMemoryError:GC Overhead Limit Error

image-20210511143738633.png

每一个线程对应一个虚拟机栈,每一个虚拟机栈对应多个栈帧,每个栈帧里面存在局部变量表、操作上栈、动态链接、返回地址

1.2.4 类加载的过程

image-20210511144814568.png

  1. 加载:

    1. 在加载阶段,虚拟机需要完成以下3件事情:
      • 通过一个类的全限定名来获取定义此类的二进制字节流
      • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
      • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  2. 连接阶段:

    1. 验证: 确保被加载的类的正确,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。文件验证、元数据验证、字节码验证、符号引用验证

    2. 准备: 为类的静态变量分配内存,并将其赋默认值

      1. 只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)。

      2. 对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值

    3. 解析:

      1. 将常量池中的符号引用替换为直接引用(内存地址)的过程(这里是静态解析)

      2. 符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

  3. 初始化:为类的静态变量赋初值

    赋初值两种方式:

    • 定义静态变量时指定初始值。如 private static String x="123";
    • 在静态代码块里为静态变量赋值。如 static{ x="123"; }

jvm相关的资料

(早绑定和晚绑定)

静态链接 当一个 字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。

动态链接 如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。(多态)

image-20210511150845343.png

符号引用和直接引用


1. 符号引用(Symbolic References):

  符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替

比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

2.直接引用:

直接引用可以是

(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)

(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)

(3)一个能间接定位到目标的句柄

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。


1.2.5 类加载器

(一) 启动类加载器(Bootstrap Class-Loader)

加载 jre/lib 下面的 jar 文件,如 rt.jar。

它是个超级公民,即使是在开启了 Security Manager 的时候,JDK 仍赋予了它加载的程序 AllPermission。

我们一般可以使用下面方法获取父加载器,但是在通常的 JDK/JRE 实现中,扩展类加载器 getParent() 都只能返回 null

(二) 扩展类加载器(Extension or Ext Class-Loader)

负责加载我们放到 jre/lib/ext/ 目录下面的 jar 包,这就是所谓的 extension 机制。

该目录也可以通过设置 “java.ext.dirs”来覆盖。

(三) 应用类加载器(Application or App Class-Loader)

重写loadClass 打破双亲委派

重写 findClass 维持双亲委派

definclass() 把字节码转化为Class

在默认实现的 loadClass 中, 会调用 findClass 方法,所以,如果重写 findClass, 在调用的时候,,还是通过 loadClass 进行加载, 会传递到上一级,流程如下,到最后一步 才是调用 自己的 findClass

1、先检查类是否已经被加载过

2、若没有加载则调用父加载器的loadClass()方法进行加载

3、若父加载器为空则默认使用启动类加载器作为父加载器。

4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

如果是重写了 LoadClass, 可以直接重写*definclass 打破双亲委派

JDBC中通过引入ThreadContextClassLoader(线程上下文加载器,默认情况下是AppClassLoader)的方式破坏了双亲委派原则。

(四)双亲委派机制优势

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意篡改
    • 自定义类:java.lang.String (没用)
    • 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)

(五) SPI 机制

SPI 注重的是一种插槽的思想,可以通过 ServiceLoader 去加载 META—INFO /service 下面的东东,返回的是一个迭代器,需要注意,文件的类名需要和你想要加载的接口(一般都用接口)对应起来。

public class SpiTest {
    public static void main(String[] args) {
        ServiceLoader<People> load = ServiceLoader.load(People.class);
        Iterator<People> loaders = load.iterator();

        while (loaders.hasNext()) {
            loaders.next().run();
            System.out.println(1);

        }

    }
}

1.2.6 对象的创建过程

  1. 类加载

  2. 分配内存

    1. 空闲列表

    2. 指针碰撞

    3. 选择以上两种⽅式中的哪⼀种,取决于 Java 堆内存是否规整。⽽ Java 堆内存是否规整,取决于

      GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制

      算法内存也是规整的

    4. 并发问题

      1. CAS+失败重试
      2. TLAB : 就是,在eden 区直接分配了一小个内存给线程
  3. 初始化零值

  4. 设置对象头

  5. 执行 init 方法