Lichord

学习笔记

0%

内存溢出

OutOfMemoryError:PermGen space

PermGen space就是永久保存区,也就是直接内存。该块内存主要是被JVM用来存放class和mete信息的,当class被加载loader的时候就会被存储到该内存区中,与存放类的实例的heap区不同,java中的垃圾回收器GC不会在主程序运行期对PermGen space进行清理。

出错原因:系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法,导致元空间的内存占用很大。

解决方案:增加空间分配——增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小,其中XX:PermSize是初始永久保存区域大小,XX:MaxPermSize是最大永久保存区域大小。

OutOfMemoryError:Java heap space

heap是Java内存中的堆区,主要用来存放对象,当对象太多超出了空间大小,GC又来不及释放的时候,就会发生溢出错误。即内存泄露越来越严重时,可能会发生内存溢出。

要么代码有问题,要么访问量太多并且每个访问的时间太长或者数据太多,导致数据释放不掉

解决方案:

  • 检查程序,减少大量重复创建对象的死循环,减少内存泄露。
  • 如果代码没有什么问题的情况下,可以适当调整-Xms和-Xmx两个jvm参数,使用压力测试来调整这两个参数达到最优值。
  • 尽量避免大的对象的申请,像文件上传,大批量从数据库中获取,这是需要避免的,尽量分块或者分批处理,有助于系统的正常稳定的执行。
  • 尽量提高一次请求的执行速度,垃圾回收越早越好,否则,大量的并发来了的时候,再来新的请求就无法分配内存了,就容易造成系统的雪崩。

StackOverFlowError

stack是Java内存中的栈空间,主要用来存放方法中的变量,参数等临时性的数据的,发生溢出一般是因为分配空间太小,或是执行的方法递归层数太多创建了占用了太多栈帧导致溢出。

解决方案:修改配置参数-Xss参数增加线程栈大小之外,优化程序是尤其重要。

内存泄漏

内存泄漏是堆中的存在无用但可达的对象,GC无法回收

分类

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
1
2
3
4
5
6
7
8
//循环申请对象o,并将o放入容器中,虽然我们释放了o,但是由于容器还引用这这个对象,所以GC仍然是不会回收的。我们需要通过释放容器才能被GC回收
Vector v=new Vector(10);
for (int i=1;i<100; i++)
{
Object o=new Object();
v.add(o);
o=null;
}

LinkedList

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//add
boolean add(E e)//在链表尾部添加一个元素,如果成功,返回true,否则返回false;
void addFirst(E e)//在链表头部插入一个元素;
addLast(E e)//在链表尾部添加一个元素;
void add(int index, E element)//在指定位置插入一个元素。

//remove
E remove()//移除链表中第一个元素;
boolean remove(Object o)//移除链表中指定的元素;
E remove(int index)//移除链表中指定位置的元素;
E removeFirst()//:移除链表中第一个元素,与remove类似;
E removeLast()//:移除链表中最后一个元素;
boolean removeFirstOccurrence(Object o)//移除链表中第一次出现所在位置的元素;
boolean removeLastOccurrence(Object o)//移除链表中最后一次出现所在位置的元素;

//get
E get(int index)//按照下边获取元素;
E getFirst()//获取第一个元素;
E getLast()//获取第二个元素;
--------------------------------------------
//掌握上面一组增删查就够用了
//offer
boolean offer(E e)//与add一样
boolean offerFirst(E e)//与addFirst一样
boolean offerLast(E e)//与addLast一样

//push pop poll
void push(E e)//与addFirst一样
E pop()//与removeFirst一样
E poll()//查询并移除第一个元素
//链表为空时,poll返回null, pop产生异常

//peek
E peek()//获取第一个元素,但是不移除;
E peekFirst()//获取第一个元素,但是不移除;
E peekLast()//获取最后一个元素,但是不移除;
---------------------------------------------
//其他
int size();
boolean isEmpty();
boolean contains(Object o);//判断是否包含某元素
void set(int index,E element);//修改指定位置元素
List subList(int fromIndex,int toIndex);//返回[,from,to)区间的新链表,不会修改原链表
clear();//清空

源码分析

  • 继承于AbstractSequentialList的双向链表:可以被当作堆栈、队列或双端队列进行操作。
  • 实现 List 接口,能进行队列操作。
  • 实现 Deque 接口,能将LinkedList当作双端队列使用
    1
    2
    3
    public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{...}

基于双向链表实现,使用 Node 存储链表节点信息。

1
2
3
4
5
private static class Node<E> {
  E item;
  Node<E> next;
  Node<E> prev;
}

每个链表存储了 first 和 last 指针:

1
2
transient Node<E> first;
transient Node<E> last;

链表.png
LinkedList的源码也就是普通的链表操作,没有什么难点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//构造函数
//空参构造
public LinkedList() {
}
//传入其他Collection转换成LinkedList
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
//转换方式就是先变成数组类型,再将数组中的元素依次创建结点放在List中
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);

Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;

Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}

for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}

if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}

size += numNew;
modCount++;
return true;
}
//链表存储了first last指针,所以对头尾结点增删查非常好实现

与 ArrayList 的比较

  • ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
  • ArrayList 支持随机访问,LinkedList 不支持;
  • LinkedList 在任意位置添加删除元素更快

遍历方法及效率

Linkedlist不支持随机访问,所以使用访问next的方式便利更高效。ArrayList使用for随机访问更高效。foreach本质上是使用iterator实现的。

  • Linkedlist : foreach > iterator >for
  • ArrayList :for >foreach >iterator

参考:https://www.cnblogs.com/aoguren/p/4771589.html

ArrayList

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//创建
ArrayList<String> list = new ArrayList<String>();
//指定容量大小
ArrayList<Integer> list = new ArrayList<Integer>(7);

//add
boolean add(Element e)//增加指定元素到链表尾部.
void add(int index, Element e)//增加指定元素到链表指定位置

//remove
E remove(int index)
E remove(Object o)

//get
E get(int index)获取链表中指定位置处的元素

//set
E set(int index, E element)//将链表中指定位置上的元素替换成新元素。

Object[] toArray() //即将链表转换为一个数组

int indexOf(Object o)//返回元素在链表中第一次出现的位置,没有返回-1
int lastIndexOf(Object o)//返回元素在链表中最后一次出现的位置,没有返回-1

boolean contains(Object o)//如果链表包含指定元素,返回true.
boolean isEmpty()
int size()
void clear()

参考:实例讲解ArrayList用法

源码分析

  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
  • 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。
  • 实现java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。
  • 实现了 RandomAccess 接口,因此支持随机访问。
    1
    2
    public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}

构造方法(3种)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static final int DEFAULT_CAPACITY = 10;
//空参构造,实际上初始化赋值一个空数组,当真正对数组添加元素时,才真正分配大小=10
public ArrayList() {
//private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

//创建ArrayList时传入初始容量
public ArrayList(int initialCapacity) {
//创建指定大小的数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

//支持传入一个集合,将集合转换成ArrayList类型
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void add(E e, Object[] elementData, int s) {
//数组没有空间了就进行扩容
if (s == elementData.length)
//扩容后的大小是之前大小的1.5倍
elementData = grow();
elementData[s] = e;
size = s + 1;
}
//在数组末尾插入数据
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
//指定位置插入数据
public void add(int index, E element) {
//检测下标是否越界
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
//把index之后的所有元素向后移一位
//elementData 源数组 index:源数组中起始位置
//elementData 目标数组 index:目标数组中起始位置
//s - index:要复制的数组元素的数量
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}

扩容(重点)

插入数据时没有空间了就进行扩容。

使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1) ,也就是旧容量的 1.5 倍。

扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private Object[] grow(int minCapacity) {
//elementData:要复制的数组,newCapacity(minCapacity):复制的长度
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}

private Object[] grow() {
return grow(size + 1);
}

private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}

删除数据

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出ArrayList 删除元素的代价是非常高的。

ArrayList源码大量使用了System.arrayCopy()和Arrays.copyOf()方法

copyOf()内部实际上调用了System.arrayCopy()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//固定位置删除,返回要删除的元素
public E remove(int index) {
Objects.checkIndex(index, size);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
//向前移动一位
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//最后一位置空,size减小1
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}
//删除指定元素
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}

遍历时删除元素

对集合进行遍历的时候,将集合的大小改变了就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//会报错 java.util.ConcurrentModificationException
ArrayList <Integer> list=new ArrayList();
list.add(10);
list.add(11);
list.add(12);
list.add(13);
list.add(14);
list.add(15);
list.add(16);
list.add(17);
for(Integer i: list){
if(i==13){
list.remove(i);
}
}
//同样报错
Iterator<Integer>it=list.iterator();
while(it.hasNext()){
Integer num=it.next();
if(num==13){
list.remove(num);
}
}
//用迭代器删除可以解决
Iterator<Integer>it=list.iterator();
while(it.hasNext()){
Integer num=it.next();
if(num==13){
//区别点
it.remove(num);
}
}

ensureCapacity方法

大量添加元素之前最好先使用ensureCapacity()方法。可以减少扩容次数,提高效率

1
2
3
4
5
6
7
8
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}

参考:https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md

与Vector区别

  • Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
  • Vector是线程安全的

Vector(弃用)

实现与 ArrayList 类似,但是使用了 synchronized 进行同步,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;

替代方案1

使用Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

1
2
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);

替代方案2

可以使用 java.util.concurrent 并发包下的 CopyOnWriteArrayList 类

1
List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList

读写分离

  • 写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
  • 写操作需要加锁,防止并发写入时导致写入数据丢失。
  • 写操作结束之后需要把原始数组指向新的复制数组。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    Object[] elements = getArray();
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    newElements[len] = e;
    setArray(newElements);
    return true;
    } finally {
    lock.unlock();
    }
    }
    final void setArray(Object[] a) {
    array = a;
    }
1
2
3
4
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
  return (E) a[index];
}

适用场景

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
但是 CopyOnWriteArrayList 有其缺陷:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

为什么JUC包里没有并发的ArrayList实现

主要原因:很难去开发一个通用并且没有并发瓶颈的线程安全的List。

像ConcurrentHashMap这样的类的真正价值并不是它们保证了线程安全。而在于它们在保证线程安全的同时不存在并发瓶颈。举个例子,ConcurrentHashMap采用了锁分段技术和弱一致性的Map迭代器去规避并发瓶颈。

像ArrayList这样的数据结构,你不知道如何去规避并发的瓶颈。拿contains() 这样一个操作来说,当你进行搜索的时候如何避免锁住整个list?

另一方面,Queue和Deque(基于LinkedList)有并发的实现是因为他们的接口相比List的接口有更多的限制,这些限制使得实现并发成为可能。

CopyOnWriteArrayList是一个有趣的例子,它规避了只读操作(如get/contains)并发的瓶颈,但是它为了做到这点,在修改操作中做了很多工作和修改可见性规则。 此外,修改操作还会锁住整个List,因此这也是一个并发瓶颈。所以从理论上来说,CopyOnWriteArrayList并不算是一个通用的并发List。

shell script:将一堆linux命令集放在一个文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#创建test.sh文件,输入
a=10
b=3
c=`expr $a + $b`
echo $c
echo "Hello"
#注释用#号

#表示:定义变量a=10,b=3,expr用于计算表达式,输出c,输出Hello

#注意:等于号前后不可以有空格,运算符前后必须有空格,输出字符串可以不加"",使用变量的时候要加$符,`不是单引号,而是.md文件插入代码那个符号

#乘法的话:c=`expr $a * $b`会报语法错,*会被认为是正则表达式,正确写法:c=`expr $a \* $b`使用\转义

# ()也需要转义\ (\)

# 执行shell文件:sh test.sh

判断符号

1
2
3
4
5
> -gt     //greater than
< -lt //less than
<= -le //less equal
>= -ge
= -eq

条件判断:

判断a b中的最大值

1
2
3
4
5
6
7
8
a=10
b=3
if [ $a -gt $b ]
then
echo $a
else
echo $b
fi

循环语句:

1
2
3
4
5
6
7
8
9
10
11
for x in 1 2 3 4 5 6 7 8 9
do
echo $x
done

x=1
while [ $x -le 10 ]
do
echo $x
x=`expr $x + 1`
done

字符串:

1
2
3
4
5
6
7
8
9
10
11
a="hello"
b="world"
echo $a $b
#输出为hello world 空格也被输出

#输入a b
read a
read b
c=`expr $a + $b`
echo $a + $b = $c
#控制台输入1 2 ,输出则为1 + 2 = 3

比较字符串用一个等于号就行
不等用!=

1
2
3
4
5
6
7
8
password="hello123"
read user
if [ $user = $password ]
then
echo "密码正确"
else
echo 密码错误
fi

字符串拼接:

1
2
3
str1="hello"
str2="world"
str3="$str1 + $str2"

判断字符串是否长度为0:

1
2
3
4
5
6
7
8
9
# -z zero    是0
# -n not zero 不是0
str1=""
if [ -z $str1 ]
then
echo "字符串长度为0"
else
echo "字符串长度不为0"
fi

数组:

传统shell脚本不支持数组,但bash可以运行数组

执行命令:bash test.sh

1
2
3
4
5
6
7
8
9
10
arr=(7 8 9 10)
echo $arr
#输出arr下标为0的内容
echo ${arr[1]}
#输出arr下标为1的内容

for i in ${arr[@]}
do
echo $i
done

全局变量

1
2
3
4
5
6
7
#在命令行中:
#输出用户:
echo $USER
#输出用户主目录
echo $HOME
#输出环境变量
echo $PATH

添加环境变量

Linux下的环境变量用:分开,不同于windows
创建一个c程序

1
2
3
vim example.c
gcc example.c -o example
./example

要想在其他文件夹下也可以执行这个文件,就需要将example所在目录设置为全局变量
在原来的PATH下增加新变量

1
PATH~$PATH:/home/ubuntu/workspace

  • cal 12 2019 日历
  • pwd 显示当前所在文件夹
  • cd / 切换到根目录 也可以切换到别的目录
  • cd .. 退回到下一个文件夹
  • cd 切换到用户主目录
  • ls 当前文件夹下面有什么文件
  • ls -l 当前文件夹下面文件极其详细信息
  • clear清空屏幕
  • mkdir hello创建一个名为hello的文件夹
  • mkdir -p dir1/dir2/dir3/ 使用-p选项可以创建一个路径上所有不存在的目录
  • rmdir hello/ 移除文件夹
  • cp a.txt file.txt 复制a.txt 再生成file.txt
  • cat sym.txt 查看文件所有内容
  • less sym.txt文件太长的时候可以翻页查看,点击q退出这个查看模式
  • diff sym.txt file2.txt 查看两个文件的不同的地方
  • head sum.txt -n 6 看文件前面6行
  • tail sum.txt -n 6 看文件最后面6行
  • wc sym.txt
    • 输出:178 225 1836 sym.txt
    • 表示:sym.txt文件一共178行,有225个单词,1836个字节
  • wc -w sym.txt 看有多少个单词
  • wc -l sym.txt 看有多少行
  • wc -c sym.txt 看有多少字节
  • rm file2.txt 删除文件(慎用)
  • mv file1.txt a.txt 将file1.txt改名成a.txt
  • mv hi.txt bye/将hi.txt移动到bye文件夹下
  • zip program.zip * 将文件夹下所有文件压缩成program.zip
  • zip workspace.zip -r workspace/*把workspace下所有东西做成压缩包,-r表示把文件夹的子文件夹也一层一层打包
  • tar -zcvf file.tar.gz wo/ 压缩wo文件夹
  • tar -zxvf file.tar.gz 解压
  • tar -tvf file.tar 查看tar文件
  • unzip workspace.zip

wget 下载文件

  • wget 网址 //下载这个网址的内容
  • wget 网址 -O newname.tar.gz//下载这个网址的文件并重命名
    1
    2
    3
    $ wget http://prdownloads.sourceforge.net/sourceforge/nagios/nagios-3.2.1.tar.gz

    $ wget -O taglist.zip http://www.vim.org/scripts/download_script.php?src_id=7701

ping ping一个远程主机,只发5个数据包

1
$ ping -c 5 gmail.com

ifconfig 用于查看和配置Linux系统的网络接口

1
2
3
4
5
#查看所有网络接口及其状态
$ ifconfig -a
#使用up和down命令启动或停止某个接口
$ ifconfig eth0 up
$ ifconfig eth0 down

netstat

1
2
3
4
5
netstat 查看网络连接情况
netstat -i 显示网卡运行情况
netstat -r 查看主句的路由列表

netstat -an 查看当前系统端口使用情况

service

1
2
3
4
5
6
#重启网卡
service network restart
# 关闭网卡
ifdown eth0
#开启网卡
ifup eth0

hostname

1
2
hostname 显示主机名
hostname -i 显示当前主机名的IP

ps命令用于显示正在运行中的进程的信息

1
2
3
4
5
6
7
8
9
10
11
查看当前正在运行的所有进程

$ ps -ef | more
以树状结构显示当前正在运行的进程,H选项表示显示进程的层次结构
$ ps -efH | more

ps aux|grep java
#查看java进程

ps aux
#查看所有进程

kill 终止进程

1
2
3
4
5
6
#kill用于终止一个进程。一般我们会先用ps -ef查找某个进程得到它的进程号,然后再使用kill -9 进程号终止该进程。你还可以使用killall、pkill、xkill来终止进程

$ ps -ef | grep vim
ramesh 7243 7222 9 22:43 pts/2 00:00:00 vim

$ kill -9 7243

date 显示时间

1
2
# 修改系统日期
date -s "01/31/2010 23:59:53"

chmod权限管理:

1
2
3
4
5
linux下的三种不同用户,分别是u,g,o代表作者,小组,其他人;每种用户的权限都有三种r,w,x,分别是可读可写运行

ls -l查看文件或者文件夹的详情时
输出:-rm-r--r--
-开头表示这是文件,d开头的话表示是目录,用户可读可写,小组成员其他人可读
  • chmod u-r hello.txt将作者的权限减去可读
  • chmod u+r hello.txt为作者增加可读权限
  • chmod go-r 小组,其他人都不可读
    1
    2
    3
    4
    5
    6
    7
    8
    9
    权限可以用二进制表示,1有权限,0无权限

    r w x

    1 1 0

    可读可写不可运行,十进制等于6

    chmod 644 hello.txt 作者可读可写,小组,其他人可读

grep 结合正则表达式在文件中查找字符串

  • grep en hello.txt 显示hello.txt里面包含en这个字符串的所有单词。en这个位置可以用其他正则表达式代替
  • grep l* hello.txt查找所有l出现0-n次的单词
  • grep ll* hello.txt查找所有l出现1-n次的单词
  • grep ^H hello.txt查找以H开头的单词
  • grep H.ll hello.txt .表示任意字母
  • grep [Hh].ll hello.txt []里面的H,h任选一个
  • grep [A-Za-z]ill hello.txt 任意字母
  • grep [A-Za-z]ill hello.txt | wc 把输出的所有内容放在wc中进行统计
    • 输出:19 29 224
    • 表示:19行,29个单词,224字符

find

1
2
3
4
5
6
7
8
9
#查找指定文件名的文件(不区分大小写)

$ find -iname "MyProgram.c"
#对找到的文件执行某个命令

$ find -iname "MyProgram.c" -exec md5sum {} \;
#查找home目录下的所有空文件

$ find ~ -empty

export 输出跟字符串oracle匹配的环境变量

1
2
3
4
5
6
7
$ export | grep ORCALE
declare -x ORACLE_BASE="/u01/app/oracle"
declare -x ORACLE_HOME="/u01/app/oracle/product/10.2.0"
declare -x ORACLE_SID="med"
declare -x ORACLE_TERM="xterm"
#设置全局环境变量
$ export ORACLE_HOME=/u01/app/oracle/product/10.2.0

重定向符号 >

ls > 1.txt

将本来会在shell输出的内容,就是当前文件夹下的文件信息,放在1.txt文件中

whatis 显示某个命令的描述信息

1
2
$ whatis ls
ls (1) - list directory contents

whereis

1
2
3
4
5
6
#当你不知道某个命令的位置时可以使用whereis命令,下面使用whereis查找ls的位置
$ whereis ls
ls: /bin/ls /usr/share/man/man1/ls.1.gz /usr/share/man/man1p/ls.1p.gz
#当你想查找某个可执行程序的位置,但这个程序又不在whereis的默认目录下,你可以使用-B选项,并指定目录作为这个选项的参数。下面的命令在/tmp目录下查找lsmk命令
$ whereis -u -B /tmp -f lsmk
lsmk: /tmp/lsmk

uname

uname可以显示一些重要的系统信息,例如内核名称、主机名、内核版本号、处理器类型之类的信息

1
2
$ uname -a
Linux john-laptop 2.6.32-24-generic# 41-Ubuntu SMP Thu Aug 19 01:12:52 UTC 2010 i686 GNU/Linux

vim编辑器

1
2
3
yy 复制一行数据
pp 粘贴
dd 剪切

rpm安装软件包

mysql

1
2
3
4
#连接一个远程数据库,需要输入密码
$ mysql -u root -p -h 192.168.1.2
#连接本地数据库
$ mysql -u root -p

http://gywbd.github.io/posts/2014/8/50-linux-commands.html

https://blog.csdn.net/bieleyang/article/details/76665022

https://www.hollischuang.com/archives/800

https://segmentfault.com/a/1190000015497396

一.类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块
称为“类加载器”。
类加载子系统.png

类加载生命周期七阶段:

  • 类加载:类加载器将class文件加载到虚拟机的内存
  • 加载:在硬盘上查找并通过IO读入字节码文件
  • 连接:执行校验,准备,解析(可选)步骤
  • 校验:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值
  • 解析:类加载器装入类所引用的其他类
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

    类与类加载器

  • 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

    常见加载器

    从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类
    java.lang.ClassLoader。

从java开发人员角度,类加载器可以分为三类:

  • 启动类加载器:Bootstrap ClassLoader,C++语言实现,负责加载JDK中的核心类库。这个类加载器负责将存放<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可
  • 扩展类加载器:Extension ClassLoader,java实现,负责加载JDK\jre\lib\ext目录,或者由java.ext.dirs系统变量指定路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器
  • 应用程序类加载器 Application ClassLoader,负责加载应用程序classpath目录下的所有jar和class文件。开发者可以直接使用该类加载器。

除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器

双亲委托模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器
20190506_5cd066188b4ea.png

工作过程

如果一个类加载器收到了类加载的请
求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。如果没有找到,会抛出ClassNotFoundException。

优点

避免重复加载,保证Java程序的稳定运作。Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。如果不使用这种委托模式,那我们就可以随时使用自定义的Object来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况

自定义classLoader

为什么要自定义类加载器

Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader

怎么定义
  1. 继承java.lang.ClassLoader
  2. 重写父类的findClass方法

父类有那么多方法,为什么偏偏只重写findClass方法?

  • 因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。下图是API中ClassLoader的loadClass方法:
    0_1330186866b93o.gif

    二. 运行时数据区

    线程独占区的虚拟机栈,本地方法栈,程序计数器都随线程的生死而生死
    f620089148f9f1061ff2db8a725048f5.png

    1. 虚拟机栈

    java线程执行方法的内存模型,一个线程对应一个栈,栈的生命周期与线程相同。每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)不存在垃圾回收问题,只要线程一结束该栈就释放
    fdaf4c58a254103c4730d7ab13f5b602.jpeg
    虚拟机栈规定了两种异常
  • 如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常
  • 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class App{
    public int add(){
    int a=1;
    int b=2;
    int c=(a+b)*100;
    return c;
    }

    public static void main String[]args){
    App app=new App();
    int result=app.add();
    System.out.plintln(result);
    }
    }

虚拟机栈.png
局部变量表存放编译时期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(存放指针),和returnAdddress类型(指向一条字节码指令的地址)

局部变量表所需内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

64位的long,double会占用2个局部变量空间,其余数据类型只占一个

Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。

Java 方法有两种返回方式:

  • return 语句。
  • 抛出异常。
    不管哪种返回方式都会导致栈帧被弹出。

2. 本地方法栈

结构与虚拟机栈一致,也会抛出StackOverflowError和OutOfMemoryError。本地方法栈的方法都是有native关键字修饰的。区别只是虚拟机栈为执行java方法(字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。(使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。)

本地方法栈登记native方法,在Execution Engine执行时加载本地方法库

3. 程序计数器

一个指针,存储下一个指令的地址,是一个非常小的内存空间。由执行引擎读取下一条指令

在任何确定的时刻,一个内核都只会处理一条线程的指令,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这是线程私有的内存。

如果线程执行java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行native方法,这个计数器值则为空(Undefined)。

程序计数器是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的内存区域

4. 方法区(线程共享)/非堆

存储已被虚拟机加载的静态变量+常量+类信息+即时编译器编译后的代码等数据,类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。

java虚拟机规范把方法区描述为堆的一个逻辑部分,但是方法区有个别名叫non-heap与堆区分开

方法区可以选择不实现垃圾收集器。这区域的回收目标是针对常量池的回收和对类型的卸载

运行时常量池

运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。

5. 堆

虚拟机启动时自动创建,用于存放对象实例,几乎所有对象实例以及数组都在堆上分配内存。java堆只要逻辑上连续即可,可以物理不连续

当对象无法在该空间申请到内存,堆也无法扩展时抛出OutOfMemoryError异常

是垃圾收集器管理的主要区域。

可通过-Xmx -Xms参数来分别指定最大堆,最小堆

Java堆还可以细分为:新生代和老年代。新生代再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

eden 区、s0 区、s1 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

从内存分配的角度看,线程共享的java堆可能划分出多个线程私有的分配缓冲区
堆.png

直接内存

直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是这部分内存也被频繁使用,也可能出现OutOfMemoryError

JDK1.4加入NIO(new input/ouput)类,引入了一种基于通道和缓存区的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景显著提高性能,因为避免了在java堆和native堆中来回复制数据

三. HotSpot虚拟机对象揭秘

对象的创建

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

  • 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象
    所需内存的大小在类加载完成后便可完全确定
    jvm_create.png

  • 为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。

  • 如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

  • 选择哪种分配方式由Java堆是否规整决
    定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

  • 对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

  • 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  • 接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类
    的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  • 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始——<init>方法还没有执行,所有的字段都还为零。所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对
    象才算完全产生出来。

    对象的内存布局

  • 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

    对象头

    HotSpot虚拟机的对象头包括两部分信息

  • 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为”Mark Word”。对象需要存储的运行时数据很多,其实已经超出了
    32位、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,MarkWord被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的
    32bit空间中的25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,而在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容见表

1548563345042.png

  • 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
    实例数据部分
    是对象真正存储的有效信息,也是在程序代
    码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、
    bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变
    量的空隙之中。
    对齐填充
    并不是必然存在的,也没有特别的含义,它仅仅
    起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问

Java程序需要通过栈上的reference数据来操作堆上的具体对象。

句柄访问

Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
wpid-dcdfb554be82428073f802d4628207d9_0.831678370013833.png

优势:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

直接指针访问

如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
20190714054758621_WNHEGT.jpg
优势:速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本

OutOfMemoryError异常

堆溢出 Java heap space

  1. 设置虚拟机参数JVM堆初始堆大小,最大堆大小都为20m,不可扩展

    1
    -Xms20M -Xmx20M
  2. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package OOM;
    import java.util.ArrayList;
    import java.util.List;
    public class HeapOOM{
    static class OOMObject{
    }
    public static void main(String[]args){
    List<OOMObject> list=new ArrayList<OOMObject>();
    while(true){
    list.add(new OOMObject());
    }
    }
    }
  3. 运行结果
    当出现Java堆内存溢出时,异常堆栈信息”java.lang.OutOfMemoryError”会
    跟着进一步提示”Java heap space”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    "C:\Program Files\Java\jdk-9.0.4\bin\java.exe" -Xms20M -Xmx20M "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2018.1.8\lib\idea_rt.jar=14378:C:\Program Files\JetBrains\IntelliJ IDEA 2018.1.8\bin" -Dfile.encoding=UTF-8 -classpath D:\myfiles\workspace\java\java_data_structure\target\classes OOM.HeapOOM
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.base/java.util.Arrays.copyOf(Arrays.java:3719)
    at java.base/java.util.Arrays.copyOf(Arrays.java:3688)
    at java.base/java.util.ArrayList.grow(ArrayList.java:237)
    at java.base/java.util.ArrayList.grow(ArrayList.java:242)
    at java.base/java.util.ArrayList.add(ArrayList.java:467)
    at java.base/java.util.ArrayList.add(ArrayList.java:480)
    at OOM.HeapOOM.main(HeapOOM.java:16)
    Process finished with exit code 1
  4. 解决方案
    要解决这个区域的异常,一般的手段是先通过内存映像分析工具
    (Eclipse有Eclipse Memory Analyzer;IDEA的JProfiler;jmeter)对Dump出来的堆转储快照进行分析,重
    点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了
    内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

    垃圾收集器与内存分配策略

    垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线
    程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。

    判断对象是否存活

    1. 引用计数算法

    为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被
    回收。

在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存
在,因此 Java 虚拟机不使用引用计数算法。

1
2
3
4
5
6
7
8
9
public class Test {
  public Object instance = null;
  public static void main(String[] args) {
    Test a = new Test();
    Test b = new Test();
    a.instance = b;
    b.instance = a;
 }
}
  1. 可达性分析算法
    以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。

xeKfNyU.png
可以作为GC Root的对象

  • 虚拟机栈中的引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中JNI(native方法)的引用的对象

    引用

    无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关

引用分为强引用
(Strong Reference)、软引用(Soft Reference)、弱引用(Weak
Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐
渐减弱。

强引用

就是指在程序代码之中普遍存在的,类似Object obj=new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉
被引用的对象。

软引用

是用来描述一些还有用但并非必需的对象。对于软引用关联
着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回
收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛
出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引
用。

1
2
3
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联
弱引用

也是用来描述非必需对象的,但是它的强度比软引用更弱一
些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾
收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的
对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

1
2
3
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
虚引用

也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

1
2
3
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;

回收条件

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将
会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收

回收方法区

方法区中进行垃圾收集的“性价比”一般比较低

垃圾回收算法

新生代:复制算法
F3Mz2q.png
缺点:空间折半

优点:简单,没有内存碎片

老年代:标记整理算法
标记整理算法.png

老年代:标记清理算法
1ewzykxlrj.png

常见回收器

  • Serial Garbage Collector:单线程GC
  • Parallel Garbage Collector:多线程GC
  • CMS Garbage Collector:多线程GC
  • G1 Garbage Collector:jdk7引进GC,多线程,高并发,低暂停,逐步取代CMS GC

执行引擎


什么是JVM

JVM JDK JRE区别

JVM指令集

javap -c app.class app.txt

JVM性能调优:堆

jvm 分代回收

如何判断对象是否需要回收:
引用计数算法
GC ROOT

JVM性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//该程序的stop work 永远不会被输出
public class App{
boolean stop=false;
public void shutDown(){
stop=true;
}
public void doWork(){
while(!stop){

}
System.out.printf("stop work");
}
public static void main String[]args){
App app=new App();
new Thread(()->{app.doWork();}).start();
new Thread(()->{app.shutDown();}).start();
}
}

每一个线程都会放在一个虚拟机栈中
.png
volatile是C/C++实现的
java并发

CAS 线程池
线程安全
AQS
并行与并发

如下几种情况,java虚拟机将结束生命周期:

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致java虚拟机进程终止

参考:

JVM内存模型与JMM(Java内存模型)不是一回事

java的三种移位运算符 :>>>,>> 和<<

计算机里面的带符号数用补码表示

  • << : 带符号左移,
  • >> : 带符号右移,
  • >>> : 无符号右移,忽略符号位,空位都以0补齐

<<

  • -12 在计算机中存储为二进制补码:1111 1111 1111 1111 1111 1111 1111 0100
  • -12 << 3 即带符号左移3位,结果是:1111 1111 1111 1111 1111 1111 1010 0000
  • 十进制:-96

>> 移位之后,正数依然是正数,负数依然是负数

  • -12 在计算机中存储为二进制补码:1111 1111 1111 1111 1111 1111 1111 0100
  • -12 >> 3 即带符号右移3位,符号位=1移动时高位补1,结果是:1111 1111 1111 1111 1111 1111 1111 1110
  • 十进制为: -2;

    >>> 移位之后一定是正数

  • -12 >>> 3 就是右移三位,高位补零,为:0001 1111 1111 1111 1111 1111 1111 1110,十进制为:536870910。

正数的>>和>>>结果是一样的,因为移动时都是高位补0

应用:

二分查找中 int mid = (left + right) >>> 1;
可以保证left + right的和如果溢出也能得到正确结果

移位运算符妙用

要判断两个数符号是否相同时,可以对符号位进行异或运算

return ((a >> 31) ^ (b >> 31)) == 0;


无论是左移还是右移(包括无符号右移),都有一个共同的规则。 如果移动的位数超过规定的bit数,都会与最大移位数取模之后进行计算。

例如:
int型,32bits,如果是5<<33,其实就是5<<1


在JDK中,整形类型是有范围的,最大值为Integer.MAX_VALUE,即2147483647,最小值为Integer.MIN_VALUE -2147483648。


Spring IOC概念及作用

概念

  • 将创建对象的控制权放弃,交给工厂/框架,所以称为控制反转
  • 包括依赖注入和依赖查找

    作用

  • 解耦,降低程序间的依赖关系

基于XML的IOC环境搭建

spring5 版本是用 jdk8 编写的,所以要求 jdk 版本是 8 及以上。
时 同时 tomcat 的版本要求 8.5 及以上

1.导入spring核心容器必须的jar包

根据Spring的结构图可知,spring核心容器包括Beans,Core,Context,SpEL,此外还需要一个日志jar包

  • 所需jar包截图

jar包.png

  • Spring结构图

spring-overview.png

  • 在maven的pom.xml文件中只需要导入context jar包就可以,maven会自动添加依赖包

pom.png

  • 可以在maven project窗口中项目jar包之间的依赖关系

依赖关系.png

  • context依赖的包都会被导入,jcl包中封装了日志包,aop是使用注解时需要的包

依赖关系.png

2.创建业务层接口及实现类

1
2
3
4
5
6
7
8
9
10
11
12
public interface IAccountService {
//模拟保存账户
void saveAccount();
}

public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao = new AccountDaoImpl();// 此处的依赖关系有待解决
@Override
public void saveAccount() {
accountDao.saveAccount();
}
}

3.创建持久层接口及实现类

1
2
3
4
5
6
7
8
9
10
public interface IAccountDao {
void saveAccount();
}

public class AccountDaoImpl implements IAccountDao {
@Override
public void saveAccount() {
System.out.println("保存了账户");
}
}

4.创建spring配置文件applicationContext.xml

配置dao和service,将对象的创建交给spring管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 配置 service -->
<bean id="accountService" class="com.service.impl.AccountServiceImpl">
</bean>

<!-- 配置 dao -->
<bean id="accountDao" class="com.dao.impl.AccountDaoImpl"></bean>

</beans>

5.测试

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
//通过加载配置文件获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
//根据 bean 的 id 获取对象,这里两种获取方式都可
IAccountService aService = (IAccountService) ac.getBean("accountService");
System.out.println(aService);

IAccountDao aDao = (IAccountDao) ac.getBean("accountDao");
System.out.println(aDao);
}
}

理解部分

ApplicationContext的三个实现类

  • 鼠标放在ApplicationContext上,右键->diagram->show diagram,可以看到其继承关系,BeanFactory就是spring核心容器的顶层接口

ApplicationContext.png

  • 在ApplicationContext上再次右键->show implementation可以看到接口的实现

接口实现.png

  • 常用的三个:
    • ClassPathXmlApplicationContext:可加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。(更常用)
    • FileSystemXmlApplicationContext:可以加载磁盘任意路径下的配置文件(必须有访问权限)ApplicationContext ac = new FileSystemXmlApplicationContext("C:\\Users\\HASEE\\Desktop\\application.xml");
    • AnnotationConfigApplicationContext:用于读取注解创建容器的

BeanFactory 和 ApplicationContext 的区别

  • BeanFactory 才是 Spring 容器中的顶层接口,ApplicationContext 是它的子接口。
  • 创建对象的时间点不一样,实际开发中根据具体情况选择
    • ApplicationContext:创建对象采取的策略是采用立即加载的方式,只要一读取配置文件,默认情况下就会创建对象。(也可以延迟加载)
    • BeanFactory:创建对象采取的策略是采用延迟加载的方式,什么时候使用什么时候创建对象。
1
2
3
4
5
//--------BeanFactory----------
Resource resource = new ClassPathResource("applicationContext.xml");
BeanFactory factory = new XmlBeanFactory(resource);
IAccountService as = (IAccountService)factory.getBean("accountService");
System.out.println(as);
应用情况
  • ApplicationContext: 单例对象适用 (更多采用此接口)
  • BeanFactory: 多例对象使用

Spring对bean的管理细节

创建bean的三种方式

1.使用默认构造函数创建。

  • 在spring的配置文件中使用bean标签,配以id和class属性之后,且没有其他属性和标签时。
    采用的就是默认构造函数创建bean对象,此时如果类中没有默认构造函数,则对象无法创建。

    1
    <bean id="accountService" class="com.service.impl.AccountServiceImpl"></bean>

    2.使用普通工厂中的方法创建对象(使用某个类中的方法创建对象,并存入spring容器)

    1
    2
    <bean id="instanceFactory" class="com.factory.InstanceFactory"></bean>
    <bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService"></bean>

    3.使用工厂中的静态方法创建对象(使用某个类中的静态方法创建对象,并存入spring容器)

    1
    <bean id="accountService" class="com.factory.StaticFactory" factory-method="getAccountService"></bean>
  • 第二,三种方式应用场景:使用jar包中或者别人写的类时,不能确定是否含义默认构造函数

bean对象的作用范围

bean标签的scope属性:

  • 作用:用于指定bean的作用范围
  • 取值: 常用的就是单例的和多例的
    • singleton:单例的(默认值)
    • prototype:多例的
    • request:作用于web应用的请求范围
    • session:作用于web应用的会话范围
    • global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,它就是session
      1
      2
      3
      4
      5
      <bean id="accountService" class="com.service.impl.AccountServiceImpl" scope="prototype"></bean>

      //以下语句用singleton只会创建一次,as1=as2,用prototype会创建两次as1!=as2
      IAccountService as1 = (IAccountService)ac.getBean("accountService");
      IAccountService as2 = (IAccountService)ac.getBean("accountService");
全局session用于session在多台服务器处理时共享

全局session.png

bean对象的生命周期

单例对象

出生:当容器创建时对象出生(立即加载)
活着:只要容器还在,对象一直活着
死亡:容器销毁,对象消亡
总结:单例对象的生命周期和容器相同

多例对象

出生:当我们使用对象时spring框架为我们创建(延迟加载)
活着:对象只要是在使用过程中就一直活着。
死亡:当对象长时间不用,且没有别的对象引用时,由Java的垃圾回收器回收
1
2
3
4
5
<bean id="accountService" class="com.service.impl.AccountServiceImpl"
scope="prototype" init-method="init" destroy-method="destroy"></bean>
</beans>
//
init destroy方法需要在实现类中定义

Spring的依赖注入 Dependency Injection

依赖关系的管理,都交给spring来维护.在当前类需要用到其他类的对象,由spring为我们提供,我们只需要在配置文件中说明。依赖关系的维护,就称之为依赖注入。

能注入的数据:有三类

  • 基本类型和String
  • 其他bean类型(在配置文件中或者注解配置过的bean)
  • 复杂类型/集合类型

注入的方式:有三种

  • 使用构造函数提供
  • 使用set方法提供
  • 使用注解提供

1. 构造函数注入:实际开发不常用

标签:constructor-arg
位置:bean标签的内部
标签属性
  • type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型
  • index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引的位置是从0开始
  • name:用于指定给构造函数中指定名称的参数赋值 (直接明确,常用)

以上用于找到构造函数参数

  • value:用于提供基本类型和String类型的数据
  • ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //service实现类构造器
    public AccountServiceImpl(String name,Integer age,Date birthday){
    this.name = name;
    this.age = age;
    this.birthday = birthday;
    }
    //配置文件
    <bean id="accountService" class="com.service.impl.AccountServiceImpl">
    <constructor-arg name="name" value="泰斯特"></constructor-arg>
    <constructor-arg name="age" value="18"></constructor-arg>
    <constructor-arg name="birthday" ref="now"></constructor-arg>
    </bean>
    <!-- 配置一个日期对象 -->
    <bean id="now" class="java.util.Date"></bean>
优势:
  • 在获取bean对象时,注入数据是必须的操作,否则对象无法创建成功。
    弊端:
  • 改变了bean对象的实例化方式,使我们在创建对象时,如果用不到这些数据,也必须提供。

    2. set方法注入 (常用)

    标签:property
    位置:bean标签的内部
    标签属性
  • name:用于指定注入时所调用的set方法名称
  • value:用于提供基本类型和String类型的数据
  • ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象
优势:
  • 创建对象时没有明确的限制,可以直接使用默认构造函数
弊端:
  • 如果有某个成员必须有值,则获取对象是有可能set方法没有执行。
set方法 基本类型,bean类型注入
1
2
3
4
5
6
7
8
9
10
//set方法
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
1
2
3
4
5
6
//bean配置
<bean id="accountService" class="com.service.impl.AccountServiceImpl">
<property name="name" value="TEST" ></property>
<property name="age" value="21"></property>
<property name="birthday" ref="now"></property>
</bean>
set方法 注入集合数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//实现类的set方法
public void setMyStrs(String[] myStrs) {
this.myStrs = myStrs;
}
public void setMyList(List<String> myList) {
this.myList = myList;
}
public void setMySet(Set<String> mySet) {
this.mySet = mySet;
}
public void setMyMap(Map<String, String> myMap) {
this.myMap = myMap;
}
public void setMyProps(Properties myProps) {
this.myProps = myProps;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//配置文件
<bean id="accountService" class="com.service.impl.AccountServiceImpl">
<property name="myStrs">
<set>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</set>
</property>

<property name="myList">
<array>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</array>
</property>

<property name="mySet">
<list>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</list>
</property>

用于给List结构集合注入的标签:list array set
用于个Map结构集合注入的标签:map props
结构相同,标签可以互换

<property name="myMap">
<props>
<prop key="testC">ccc</prop>
<prop key="testD">ddd</prop>
</props>
</property>

<property name="myProps">
<map>
<entry key="testA" value="aaa"></entry>
<entry key="testB">
<value>BBB</value>
</entry>
</map>
</property>
</bean>

基于注解的IOC(11个注解)

环境搭建步骤与前面类似,只需要将配置文件xml配置换成注解

  • XML的配置:
    1
    2
    3
    <bean id="accountService" class="com.service.impl.AccountServiceImpl" scope=""  init-method="" destroy-method="">
    <property name="" value="" | ref=""></property>
    </bean>

创建对象(4个)

作用就和在XML配置文件中编写一个<bean>标签实现的功能是一样的

  • Component:
    • 作用:用于把当前类对象存入spring容器中
    • 属性:
      value:用于指定bean的id。当我们不写时,它的默认值是当前类名,且首字母改小写。
  • Controller:一般用在表现层
  • Service:一般用在业务层
  • Repository:一般用在持久层
    以上三个注解他们的作用和属性与Component是一模一样。
    他们三个是spring框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰

修改配置文件

  • 加入context名称空间,配置创建容器时要扫描的包,否则Component无效
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!--告知spring在创建容器时要扫描的包,配置所需要的标签不是在beans的约束中,而是一个名称为context名称空间和约束中-->
<context:component-scan base-package="com"></context:component-scan>
</beans>

实现类写法

1
2
3
4
5
6
7
8
//所有写法作用一样
@Service("accountService")
//@Service(value="accountService")
//@Component("accountService")
////@Component(value="accountService")
public class AccountServiceImpl implements IAccountService {
//...
}

注入数据(4个)

作用就和在xml配置文件中的bean标签中写一个<property>标签的作用是一样的

Autowired:

1
2
3
4
5
6
7
8
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao = null;
public void saveAccount(){
accountDao.saveAccount();
}
}
  • 作用:自动按照类型注入。
    • 只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功
    • 如果ioc容器中没有任何bean的类型和要注入的变量类型匹配,则报错。
    • 如果Ioc容器中有多个类型匹配时:在匹配的类型中在对变量名称也就是bean的id匹配
  • 出现位置:
    可以是变量上,也可以是方法上
  • 细节:
    在使用注解注入时,set方法就不是必须的了。
  • 自动按照类型注入的查找过程
    自动按照类型注入.png

Qualifier:

  • 作用:在按照类中注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以
    • 属性:value:用于指定注入bean的id。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Service("accountService")
      public class AccountServiceImpl implements IAccountService {
      @Autowired
      @Qualifier("accountDao1")
      private IAccountDao accountDao = null;

      public void saveAccount(){
      accountDao.saveAccount();
      }
      }

Resource

  • 作用:直接按照bean的id注入。它可以独立使用
  • 属性:
    name:用于指定bean的id。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Service("accountService")
    public class AccountServiceImpl implements IAccountService {
    @Resource(name = "accountDao2")
    private IAccountDao accountDao = null;

    public void saveAccount(){
    accountDao.saveAccount();
    }
    }

以上三个注入都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。
另外,集合类型的注入只能通过XML来实现。

Value

  • 作用:用于注入基本类型和String类型的数据
  • 属性:
    • value:用于指定数据的值。它可以使用spring中SpEL(也就是spring的el表达式)
    • SpEL的写法:${表达式}
      1
      2
      @Value("${jdbc.driver}")
      private String driver;

改变作用范围(1个)

他们的作用就和在bean标签中使用scope属性实现的功能是一样的

Scope

  • 作用:用于指定bean的作用范围
  • 属性:value:指定范围的取值。常用取值:singleton prototype,默认单例
1
2
3
4
5
6
7
8
9
@Service("accountService")
@Scope("prototype")
public class AccountServiceImpl implements IAccountService {
@Resource(name = "accountDao2")
private IAccountDao accountDao = null;
public void saveAccount(){
accountDao.saveAccount();
}
}

生命周期相关(2个) 了解

  • 作用:和在bean标签中使用init-method和destroy-methode的作用是一样的

PreDestroy

  • 作用:用于指定销毁方法

PostConstruct

  • 作用:用于指定初始化方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Resource(name = "accountDao2")
private IAccountDao accountDao = null;

@PostConstruct
public void init(){
System.out.println("初始化方法执行了");
}

@PreDestroy
public void destroy(){
System.out.println("销毁方法执行了");
}

public void saveAccount(){
accountDao.saveAccount();
}
}

Spring新注解(5个)

以上注解只能注入自己开发的类而不能注入jar包中的类

bean中遗留配置

1
2
3
4
5
6
7
8
9
10
11
<context:component-scan base-package="com"></context:component-scan>
<bean id="dbAssit" class="com.dbassit.DBAssit">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///spring_day02"></property>
<property name="user" value="root"></property>
<property name="password" value="1234"></property>
</bean>

@Configuration

  • 作用:指定当前类是一个配置类
  • 细节:当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。
    • 属性:
      value:用于指定配置类的字节码
1
2
3
@Configuration
public class SpringConfiguration {
}
1
2
ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfiguration.class);
IAccountService as=ac.getBean("accountService",IAccountService.class);

@ComponentScan

  • 作用:用于通过注解指定spring在创建容器时要扫描的包
  • 属性:
    • value:它和basePackages的作用是一样的,都是用于指定创建容器时要扫描的包。
      我们使用此注解就等同于在xml中配置了:
      <context:component-scan base-package="com"></context:component-scan>
      1
      2
      3
      4
      5
      @Configuration
      @ComponentScan(basePackages={"com"})
      //@ComponentScan("com")
      public class SpringConfiguration {
      }

Bean

  • 作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中
  • 属性:
    • name:用于指定bean的id。当不写时,默认值是当前方法的名称
  • 细节:当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象。查找的方式和Autowired注解的作用是一样的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean(name="runner")
@Scope("prototype")
public QueryRunner createQueryRunner(@Qualifier("ds2") DataSource dataSource){
return new QueryRunner(dataSource);
}

@Bean(name="ds2")
public DataSource createDataSource(){
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
return ds;
}catch (Exception e){
throw new RuntimeException(e);
}
}

@Import

  • 作用:用于导入其他的配置类
  • 属性:
    • value:用于指定其他配置类的字节码。
  • 当我们使用Import的注解之后,有Import注解的类就父配置类,而导入的都是子配置类
    1
    2
    3
    4
    @ComponentScan("com")
    @Import(JdbcConfig.class)
    public class SpringConfiguration {
    }

@PropertySource

  • 作用:用于指定properties文件的位置
  • 属性:
    • value:指定文件的名称和路径。
    • 关键字:classpath,表示类路径下
1
2
3
4
5
@ComponentScan("com")
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration {
}

总结

最方便的方式是xml+注解形式,自己写的类用注解更便捷,jar包中的类用xml更简洁

Spring整合Junit

问题

在测试类中,每个测试方法都有以下两行代码:

1
2
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = ac.getBean("accountService",IAccountService.class);
  • 这两行代码的作用是获取容器,如果不写的话,直接会提示空指针异常。所以又不能轻易删掉。

Junit分析

  • 应用程序的入口:main方法
  • junit单元测试中,没有main方法也能执行
    • junit集成了一个main方法
    • 该方法就会判断当前测试类中哪些方法有 @Test注解
    • junit就让有Test注解的方法执行
  • junit不会管我们是否采用spring框架,所以不会为我们读取配置文件/配置类创建spring核心容器
  • 由以上三点可知:当测试方法执行时,没有Ioc容器,就算写了Autowired注解,也无法实现注入

Spring整合junit的配置

  • 使用Junit提供的一个注解把原有的main方法替换了,替换成spring提供的:@Runwith
  • 告知spring的运行器,spring和ioc创建是基于xml还是注解的,并且说明位置

@ContextConfiguration
- locations:指定xml文件的位置,加上classpath关键字,表示在类路径下
- classes:指定注解类所在地位置

  • 当我们使用spring 5.x版本的时候,要求junit的jar必须是4.12及以上

配置步骤

1.导入Junit jar包

2.使用@RunWith

1
2
3
@RunWith(SpringJUnit4ClassRunner.class)
public class AccountServiceTest {
}

3.使用@ContextConfiguration 指定 spring 配置文件的位置

1
2
3
4
5
6
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= {"classpath:bean.xml"})
//@ContextConfiguration(classes = SpringConfiguration.class)

public class AccountServiceTest {
}

4.使用@Autowired 给测试类中的变量注入数据

1
2
3
4
5
6
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= {"classpath:bean.xml"})
public class AccountServiceTest {
@Autowired
private IAccountService as ;
}

为什么到 不把测试类配到 xml

测试类配置在xml中也可以使用

  • 当我们在 xml 中配置了一个 bean,spring 加载配置文件创建容器时,就会创建对象。
  • 测试类只是我们在测试功能时使用,而在项目中它并不参与程序逻辑,也不会解决需求上的问
    题,所以创建完了,并没有使用。那么存在容器中就会造成资源的浪费。
  • 基于以上两点,我们不应该把测试配置到xml

jQuery.ajax的post提交默认的请求头的Content-Type: application/x-www-form-urlencoded 。相当于(username=”admin”&password=123)来传递数据(这是GET请求的固定格式)

而axios.post提交的请求头是Content-Type: application/json。

不同的请求头发送给springmvc处理方式也不相同

application/x-www-form-urlencoded

前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var user= {
"username" : username,
"password" : password,
"rememberMe":rememberMe
};
$.ajax({
url : "http://...../jsontest.do",
type : "POST",
async : true,
data : user,
dataType : 'json',
success : function(data) {
}
});

后端

1
2
3
4
5
6
7
8
@RequestMapping("/jsontest.do")
public void test(User user,String username,String password,Boolean rememberMe){
System.out.println(user);
System.out.println("username: " + username);
System.out.println("password: " + password);
System.out.println("rememberMe: " + rememberMe);

}
优点:
  1. 前端传递数据不用转换为json字符串:JSON.stringify(user)
  2. 后端接受的参数很灵活,即可以封装为User对象,亦可以使用单个参数username,rememberMe,甚至User对象和单个rememberMe参数混合使用都可以

application/json

前端:必须要将JSON对象转换为JSON字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var user= {
"username" : username,
"password" : password
};
$.ajax({
url : "http://...../jsontest.do",
type : "POST",
async : true,
contentType: "application/json; charset=utf-8",
data : JSON.stringify(user),
dataType : 'json',
success : function(data) {
}
});

后端 所有的参数都只能封装在对象中,不能单独设置参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping("/jsontest")
public void test(@RequestBody User user ){
String username = user.getUsername();
String password = user.getPassword();
}

或者

@RequestMapping("/jsontest")
public void test(@RequestBody Map map ){
String username = map.get("username").toString();
String password = map.get("password").toString();
}

或者

public void test(@RequestBody String jsonData) {
JSONObject jsonObject = JSON.parseObject(jsonData);
String username= jsonObject.getString("username");
String username= jsonObject.getString("password");
}

ajax改成axios

ajax

1
2
3
4
5
6
7
8
9
10
var data = {age: 18};
$.ajax({
url: '',
type: 'POST',
data: data
dataType: 'json',
success: function(result) {
// do something
}
})

axios

1
2
3
4
5
6
7
8
9
10
11
12
13
import axios from 'axios';
import qs from 'qs';

var data = {age: 18};
var url = '';

axios.post(
url,
qs.stringify(data),
{headers: {'Content-Type': 'application/x-www-form-urlencoded'}}
).then(result => {
// do something
})

axios POST数据三种请求方式

Content-Type: application/json

1
2
3
4
5
6
import axios from 'axios'
let data = {"code":"1234","name":"yyyy"};
axios.post(`${this.$url}/test/testRequest`,data)
.then(res=>{
console.log('res=>',res);
})

Content-Type: multipart/form-data

1
2
3
4
5
6
7
8
import axios from 'axios'
let data = new FormData();
data.append('code','1234');
data.append('name','yyyy');
axios.post(`${this.$url}/test/testRequest`,data)
.then(res=>{
console.log('res=>',res);
})

Content-Type: application/x-www-form-urlencoded

1
2
3
4
5
6
7
8
9
import axios from 'axios'
import qs from 'Qs'
let data = {"code":"1234","name":"yyyy"};
axios.post(`${this.$url}/test/testRequest`,qs.stringify({
data
}))
.then(res=>{
console.log('res=>',res);
})

定义路由的时候就需要多添加一个自定义字段requireAuth,用于判断该路由的访问是否需要登录。如果用户已经登录,则顺利进入路由,
否则就进入登录页面。

const routes = [
    {
        path: '/repository',
        name: 'repository',
        meta: {
            requireAuth: true,  // 添加该字段,表示进入这个路由是需要登录的
        },
        component: Repository
    }
];

安装

  • 安装odoo和pycharm

  • windows:下载odoo的.exe文件按照默认指引安装就行,注意安装目录不能带有中文和空格,否则之后打开网页可能会报500 internal server error

  • odoo安装,pycharm安装以及配置,模块的安装详细步骤:https://pan.baidu.com/s/1Bfl-1ACbC8viabinKir9sg
    提取码:krxy

    开发

    用脚手架创建新模块

    用Pycharm打开安装odoo目录下的server文件夹,在Terminal中输入

    1
    python .\odoo-bin scaffold todo mymoudles
  • todo是新模块的名字,mymoudles是文件夹名

  • 出现依赖包不存在的报错信息就pip install安装依赖就行

  • 创建成功会增加如下文件夹
    目录.png

  • 也可以手动创建文件夹和文件来创建模块

    项目结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    todo
    ├── \_\_init__.py
    ├── \_\_manifest__.py
    ├── controllers
    │ ├── \_\_init__.py
    │ └── controllers.py
    ├── demo
    │ └── demo.xml
    ├── models
    │ ├── \_\_init__.py
    │ └── models.py
    ├── security
    │ └── ir.model.access.csv
    └── views
    ├── templates.xml
    └── views.xml
  • 每一个python包下都有一个init.py文件,一个odoo模块也是一个python包

    init.py
    1
    2
    3
    4
    # -*- coding: utf-8 -*-
    <!-- 引入了 controllers 和 models 这两个包 -->
    from . import controllers
    from . import models
manifest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# -*- coding: utf-8 -*-
{
'name': "todo",

'summary': """
Short (1 phrase/line) summary of the module's purpose, used as
subtitle on modules listing or apps.openerp.com""",

'description': """
Long description of module's purpose
""",

'author': "My Company",
'website': "http://www.yourcompany.com",

# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/master/odoo/addons/base/module/module_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',

# any module necessary for this one to work correctly
'depends': ['base'],

# always loaded
'data': [
# 'security/ir.model.access.csv',
'views/views.xml',
'views/templates.xml',
],
# only loaded in demonstration mode
'demo': [
'demo/demo.xml',
],
}
  • 用于声明一个 Odoo 模块以及指定它的元数据(metadata)

  • 文件里只包含了一个单独的 Python 字典,里面默认只列出了 9 项最基本的配置项,包含了模块(或应用)名,模块的简介和详细介绍,作者和网站,模块的所属分类、版本,还有就是这个模块依赖于其他的哪些 Odoo 模块,需要加载哪些数据文件以及演示数据。

  • 除了这里列出的配置项外,还有一些高级配置项

    mvc
  • models,controllers,views文件夹分别存放控制层,模型层,视图层的代码

    demo/demo.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <odoo>
    <data>
    <!-- <record id="object0" model="todo.todo"> -->
    <!-- <field name="name">Object 0</field> -->
    <!-- <field name="value">0</field> -->
    <!-- </record> -->

    <!-- <record id="object1" model="todo.todo"> -->
    <!-- <field name="name">Object 1</field> -->
    <!-- <field name="value">10</field> -->
    <!-- </record> -->
    </data>
    </odoo>
  • 存放演示数据,在使用演示模式时,初始化一些演示数据可以帮我们节省不少的时间

    security/ir.model.access.csv
    1
    access_todo_todo,todo.todo,model_todo_todo,,1,0,0,0
  • 与安全有关

  • 用于定义不同的角色组对应于不同模型的相关权限,包括读(read),写(write),创建(create)和删除(unlink)权限,拥有相关权限则为 1,反之为 0。

  • 默认角色组的定义和模型权限定义在同一目录下,角色组的定义使用.xml 文件

创建模型

  • models/models.py将注释内容删掉,不要修改文件顶部引入的包,添加以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class TodoTask(models.Model):
    _name = 'todo.task'
    _description = '待办事项'
    <!-- name 和 is_done 两个字段,它们的类型分别是 Char 和 Boolean,并且我们指定字段 name 是必填的(添加了 required=True) -->
    name = fields.Char('描述', required=True)
    is_done = fields.Boolean('已完成?')
    <!--Selection会显示下拉框选项,('todo', '待办')左边的是数据库中存储的值,右边的是一个用于界面显示的描述。
    默认值 todo,可以为任意类型的字段添加默认值-->
    priority = fields.Selection([
    ('todo', '待办'),
    ('normal', '普通'),
    ('urgency', '紧急')
    ], default='todo', string='紧急程度')
    <!-- 选择日期 -->
    deadline = fields.Datetime(u'截止时间')
  • _name : 模型的名称,在外键或者实例化模型对象时会用到,是模型的唯一标识,是定义一个新的模型时必须要有的属性

  • _description : 模型的描述,描述模型的作用,一般情况下不会主动使用到

  • 除了上面列出的两个特殊属性外,还有一些其他属性,例如更改默认显示名称时会用到 _rec_name,继承现有模型时会用到 _inherit 等

  • 模型改变后需要对模块进行升级才能看到变化

    创建菜单

  • 菜单就是能够在网页安装模块后打开该模块应用的入口

  • 在模块里的 views 目录下创建一个 menus.xml 文件,然后输入以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?xml version="1.0" encoding="utf-8"?>
    <odoo>
    <data>
    <!-- 主菜单定义 id是菜单唯一标识,name是菜单显示的名字-->
    <menuitem id="menu_todo" name="Todo"/>
    <!-- 菜单动作定义 model是动作类型,act_window 表示我们定义的这个动作是和窗口有关-->
    <record id="action_todo_task" model="ir.actions.act_window">
    <field name="name">待办事项</field>
    <field name="res_model">todo.task</field>
    <field name="view_type">form</field>
    <field name="view_mode">tree,form</field>
    <field name="target">current</field>
    </record>
    <!-- 子菜单定义 action 的值是我们定义的动作的 id,点击这个菜单,就执行对应 id 的动作。sequence 用于指定当前菜单的位置-->
    <menuitem action="action_todo_task" id="submenu_todo_task" name="待办事项"
    parent="menu_todo" sequence="10"/>
    </data>
    </odoo>
  • 把menus.xml放到manifest.py 的 data 列表中

    1
    2
    3
    4
    5
    6
    'data': [
    # 'security/ir.model.access.csv',
    'views/views.xml',
    'views/templates.xml',
    'views/menus.xml',
    ],

创建视图

  • 默认在列表页只显示了一个 name 字段,我们可以根据自己的需求定义视图

  • 打开views/views.xml,把里面注释掉的内容都删掉,然后添加以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <odoo>
    <data>
    <record id="todo_task_view_tree" model="ir.ui.view">
    <field name="name">todo.task.view_tree</field>
    <field name="model">todo.task</field>
    <field name="type">tree</field>
    <field name="arch" type="xml">
    <tree string="Todo">
    <field name="name"/>
    <field name="is_done"/>
    </tree>
    </field>
    </record>
    </data>
    </odoo>
  • 创建视图相关的内容,我们需要指定属性 model=”ir.ui.view”

  • type:列表视图,填写 tree,表单视图, form,除了这两种视图外还有其他类型的视图

  • arch,显示视图内容,列表视图用tree 包裹,表单视图form,需要在列表视图中将要显示的字段列举出来

参考&更多