01 basic

下载安装的 JDK 目录

java 主目录下的: src.zip 是 jdk 的源代码.

JDK: java development kit 包含java 编译器和JRE

JRE: java run environment java 运行时环境,JRE 包含 JVM

JVM: java virtual machine java 的虚拟机, 真正跑 class 文件的.  

JAVA 内存结构

java 虚拟机内存分为3个区域, 栈stack, 堆heap, 方法区method area

底层都类似的,各种语言互相抄袭的.

栈 stack

表示方法执行的内存模型, 每个方法被调用都会创建一个栈帧(栈帧是栈内部的一个独立的存储空间)(存储局部变量, 操作数, 方法出口 等)

JVM 为每个线程创建一个栈, 用于存放该线程执行方法的信息(实际参数,局部变量等)

栈属于线程私有, 不能实现线程间的共享。

栈的存储特性是“先进后出”

栈由系统自动分配,速度快,栈是一个连续的内存空间.

堆heap

堆用于存储创建好的对象数组(数组也是对象) {对象的所有东西都存储在这里, 包括实例变量, 比如 new一个对象, 那么全部都在这里}

JVM 只有一个堆, 被所有线程共享.

堆是一个不连续的内存空间, 分配灵活, 速度慢.

方法区 (又叫静态区)

JVM 只有一个方法区, 被所有线程共享。

方法区实际上也是堆,只是用于存储类(不是实例, 而是类[模板]), 常量的相关信息.

用于存储程序中永远不变或唯一的内容(类信息, 静态变量,字符常量 等)

画一个程序内存走势

1. java xxx 开始执行程序. 这时候开始启动虚拟机, 分配内存空间(栈和堆)

2. 将 文件名相同的类(也就是main方法所在的类), 这个类本身加载到内存的方法区中. 也就是将 xxx 加载到方法区.

    然后找到 static 的 main方法(xxx的静态方法刚才都已经在内存中了), 然后可以执行这个 main 方法.

3. 这时候在栈空间开辟 main 方法的 栈帧, 然后 main 方法里有 stu = new Student(),

    这时: (新建对象的过程)

    a. 首先执行类加载过程(如果此时Student类本身没在方法区), 加载类之后, 这个类的相关代码就在内存中了(也就是方法区中)

    b. JVM 在堆中为新生成的对象Student 开辟内存空间. 对于分配内存的大小, 在Student类本身加载到方法区后, 就可以知道了.

    c. 将构造函数放到栈 stack 中, 开始进行对象初始化工作. 

    d. 将新开辟内存的地址返回给 main 函数中的 stu 局部变量.

4. 根据main函数的执行, stu.id = 1001, stu.sname = "aa", 这样就会在堆的对象中, 对实例变量进行赋值.

5. 当调用 stu.play() 时, 找到堆中该对象的 play()方法的内容, 将play方法入栈, 开始执行 play() 方法中的内容.

Comment: 栈中的函数执行完之后, 就会出栈(根据栈的规则)

JVM 调优与垃圾回收

垃圾回收

引用计数: 判断对象是否要回收, 但是如果是循环互相引用,就会有问题。

引用可达法: 所有的引用关系做一张图,(貌似可以排除循环互相引用)

分代垃圾回收机制: 年轻代(新创建的对象),年老代(新创建之后过了一段时间),持久代(不用回收)

持久代: 用于存储静态文件, 如类方法,静态变量等. (一直在内存中, 也不变化,不会被移出内存) 这个区满了,会调用 FullGC来清理(要注意)

年轻代: 分为 Eder 和 Suvivor 两个区域.

Eder, Suvivor {Eder 满了,触发垃圾回收GC(Minor GC) -> 将留下来的对象copy放到 Suvivor1Suvivor2 中, 然后清空Eder,  然后再新增加对象到Eder区}

  依次类推, 这样如果迭代15次, 对象还在Suvivor中, 就把该对象放到 Tenured/Old 区.

年老代old: 只有一个区, 名字叫 Tenured

Tenured: 当这个区的占用达到一定比例, 就会触发 Major GC 来清理这个Old区. 如果Old 区满了,就会触发 Full GC.(影响性能)

GC(垃圾回收) 的分类

Minor GC: Eder 满了触发,用于清理年轻代区域.

Major GC: 用于清理年老代区域.

Full GC: 对所有的区域做 GC 清理, 这个代价很高,我们很多时候优化,都是对这个 Full GC 做优化. (尽量不要使用Full GC)

  System.gc(); 这实际只是发一个请求,掉GC, 这只是一个建议,但是,程序是否真正掉GC, 还是有垃圾回收器决定。

对象创建与 this

创建一个对象分为如下四步:

  1. 分配对象空间,并将对象成员变量初始化为0, false 或空
  2. 执行属性值的显式初始化, 比如属性值等于一个静态变量或字符常量等.
  3. 执行构造函数, 在这里都可以使用 this, 因为通过上面1,2两步,实际上对象已经创建好了.
  4. 返回对象的地址给相关的变量, 比如 Student stu = new Student(), 就返回地址给了 stu.

this 的本质就是创建好的对象的地址this(a,b)  实际上是调用构造函数. 必须位于第一句. this 不能用于static方法中.

继承的初始化过程从继承树向上, 首先执行爷爷的初始化 -> 爸爸的初始化 -> 自己的初始化. 因为 super() 一层一层往上调用父类的构造器.

静态导入

import static java.lang.Math.PI;     // 相当于导入这个类的静态属性, 也可以使用 import static java.lang.Math.*   就是导入这个类的所有的静态属性和方法.

这样导入之后, 在程序中, 就不用在写 类似 System.out.println(Math.PI), 而是可以直接写System.out.println(PI) , 但是个人觉得还是之前那种看着习惯.

封装的一般规则

类属性: private 修饰,暴露方法来修改这个属性, 可以加 validation.

javaBean: 只提供了属性的get 和 set 方法的简单方类. 没有复杂逻辑功能。

需要被外部访问的,一般情况下方法都是用 public.

多肽3个必要条件

继承, 方法重写, 父类引用指向子类对象(向上转型).

final 关键字

final 修饰变量是常量.

final 修饰方法: 方法不能被子类重写,可以被重载.

final 修饰类: 此类不能被继承. (这个类就是最终的类, 此类不能再被扩展了) 比如 String 这个类就是一个 final 类.

数组

数组是一个对象,数组的每一个元素可以看做是这个对象的成员变量.

三种初始化

静态初始化: int[] a = {1, 2, 3};

动态初始化: int[] a = new int[3]; a[0] = 1; 先分配空间, 之后分别单独初始化

默认初始化: 默认的, 比如 int[] a = new int[3];    这时就是数组元素默认初始化为 0

for each 循环 : 只能读取, 而不用于修改, 因为没有下标, 但是适合遍历数组

for ( int m: a ) {

}

抽象类 与 抽象方法

  1. 有抽象方法, 只能定义成抽象类
  2. 抽象类不能实例化,即不能用 new 来实例化抽象类
  3. 抽象类可以包含属性,方法,构造方法,但是构造方法不能用来 new 实例, 只能用来被子类调用.
  4. 抽象类只能用来被继承
  5. 抽象方法必须被子类实现(重写Override)

抽象类的意义 为子类提供统一的,规范的模板,子类必须实现抽象方法。例如 Number 类(包装类) 就是一个抽象类.

接口

只能定义 常量 或 抽象方法. (即便不加 final 或 abstract, 默认的就是常量和抽象方法), 而且全部都是 public 的.

因为接口需要稳定

内部类

非静态内部类: Outer.Inner inner = new Outer().new Inner();    // 定义内部类, 必须要有外部类, 因为这时内部类相当于外部类的成员变量.

  所以也就是说,非静态内部类依托外部对象(实例)。

静态内部类: 理解为outer 类的一个内部类变量, 这时不再依托外部类实例.  (使用不多)

匿名内部类: 这个内部类只调用一次. 别的地方不再用. 

this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});

数组

Arrays 这个类集成了一些数组的方法,可以直接调用, 比如排序等.

包装类自动装箱拆箱

Integer a = 234;   对象, 装箱

int b = a;   int 型, 拆箱

Collection

ArrayList, LinkedList: 线性结构

因为 java 不像 C 语言, 允许动态分配数组, 所以, 实际上可以使用普通数组来构建ArrayList, 比如每当数组长度超过80%时,

就创建一个新数组(2倍长度于原来数组), 并copy 这个数组到新数组里.

下边是 key, value 的形式:

HashSet: set 是无序不可重复的.

TreeSet: TreeSet 底层是 TreeMap 实现的, 通过 key 来存储 Set 元素,  TreeSet是一维的, 比如 Set<User> = new TreeSet<User>();

HashMap: Map<Integer, String>m1 = new HashMap<>();

  散列码: 首先将键key 转换为散列码整数, 每个Java对象都有散列码. 散列码是32位整数

  散列码是通过 hash 函数计算得到的数值,一般情况下, 不同的对象, 散列码是不同的. 在数学中, 要将较大的组映射到较小的组上时, 必须使用重复,比如你有6个球, 想放到5个抽屉里, 那必然会有重复出现, 而一般在数学上,我们可以采取取模的方式, 这样抽屉1可能就重复了,对于重复的情况,应该继续查找. 手工解决这种重复问题, 举例:

  首先考虑数据结构, 我们使用了一个对象 HashEntry 来存储, 这个对象有3个实例变量, key,value,link指针, 也就是说, 当有重复发生时, 这个指针变量可以帮助这些重复的hashcode值来继续遍历这个link链表. 还拿放球的例子, 本例中, N_BUCKETS = 5, 而我们实际有6个球, 执行过程是:

1,2,3,4,5 球来了, 都正常进入相应的 bucketArray 数组中, 当 6 来了时, 把6作为 bucketArray[1] 的第一个元素, 原来的的1是6的后继. 这样前插入链表的好处是, 不用遍历链表, 直接插入在链表头.

链表头插入:

package com.hash.duplicate;

public class SimpleStringMap {
    public SimpleStringMap() {
        bucketArray = new HashEntry[N_BUCKETS];
    }
    
    public void put(String key, String value) {
        int bucket = Math.abs(key.hashCode()) % N_BUCKETS;
        HashEntry entry = findEntry(bucketArray[bucket], key);
        if (entry == null) {
            entry = new HashEntry(key, value);
            entry.setLink(bucketArray[bucket]);
            bucketArray[bucket] = entry;
        } else {
            entry.setValue(value);
        }
    }
    
    public String get(String key) {
        int bucket = Math.abs(key.hashCode()) % N_BUCKETS;
        HashEntry entry = findEntry(bucketArray[bucket], key);
        if (entry == null) {
            return null;
        } else {
            return entry.getValue();
        }
    }
    
    public HashEntry findEntry(HashEntry entry, String key) {
        while (entry != null) {
            if (entry.getKey().equals(key)) return entry;
            entry = entry.getLink();
        }
        return null;
    }private static final int N_BUCKETS = 5;
    private HashEntry[] bucketArray;
}


class HashEntry {
    public HashEntry(String key, String value) {
        entryKey = key;
        entryValue = value;
    }
    
    public String getKey() {
        return entryKey;
    }
    
    public String getValue() {
        return entryValue;
    }
    
    public void setValue(String value) {
        entryValue = value;
    }
    
    public HashEntry getLink() {
        return entryLink;
    }
    
    public void setLink(HashEntry nextEntry) {
        entryLink = nextEntry;
    }
    
    private String entryKey;
    private String entryValue;
    private HashEntry entryLink;    // a reference to the next entry in the chain.
}

TreeMap: 一般不用, 需要map 中排序时会使用, 是通过红黑二叉树实现的.

因为是 key,value 存储, 键值是否重复, 根据 equals 方法可以比较. 也就是说, JAVA 对象的散列码通过 hashcode()函数得出来的散列码就等于equals方法,也是判断对象是否相等的依据. 所以尽量不要重写equals方法,因为重写equals方法, 就要重写hashcode方法.

 

原文地址:https://www.cnblogs.com/moveofgod/p/12316866.html