linux使用jmeter进行压力测试

linux使用jmeter进行压力测试

安装

export PATH=/usr/local/jmeter/bin/:$PATH 添加到/etc/profile末尾。

1
`$ wget https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-3.3.tgz$ tar -xvf apache-jmeter-3.3.tgz$ mv apache-jmeter-3.3.tgz jmeter$ mv jmeter /usr/local`

Java-Provider一种写法

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
public class Test {

public static void main(String[] args) {
Map<String, NodeProvider<Node>> map = new HashMap<>();
map.put("ik", TestInner::getNode);

System.out.println(map.get("ik").get("name", 1).getName());
System.out.println(map.get("ik").get("name", 200).getAge());

}
}

interface NodeProvider<T> {
T get(String name, int age);
}

class Node {
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

class TestInner {
public static Node getNode(String name, int age) {
Node node = new Node();
node.setAge(age);
node.setName(name);

return node;
}
}

单例模式

单例模式

单例模式分为了饿汉式和懒汉式,总体来说懒汉式要优于饿汉式,饿汉式不管是否其他线程调用了getInstance,都在类加载阶段创建了实例。而懒汉式则只有在调用的时候,才实例化对象,更加节省系统资源。

饿汉式:

1
2
3
4
5
6
7
8
public class Singleton  {
private static final Singleton INSTANCE = new Singleton();
private Singleton(){}

public static Singleton getInstance() {
return INSTANCE;
}
}

懒汉式-双重检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 懒汉式-双重检查
*/
public class SingletonLazy {
private static SingletonLazy instance = null;

private SingletonLazy() {
}

public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}

懒汉式-内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 懒汉式-内部类
*/
public class SingletonLazy1 {


private SingletonLazy1() {
}

private static class InnerSingleton {
private final static SingletonLazy1 instance = new SingletonLazy1();
}


public static SingletonLazy1 getInstance() {

return InnerSingleton.instance;
}

public static void helloworld() {
System.out.println("hello lazy singleton.");
}
}

执行Main方法测试,从输出结果看,只有执行了SingletonLazy1.getInstance()方法,才开始加载内部类SingletonLazy1$InnerSingleton。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) throws InterruptedException{
SingletonLazy1.helloworld();
Object lock = new Object();
synchronized (lock) {
lock.wait(1000);
}
System.out.println("分割线---------------");
System.out.println(SingletonLazy1.getInstance());
}
}

输出结果:
[Loaded sun.nio.cs.US_ASCII$Decoder from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
分割线---------------
[Loaded sun.misc.VMSupport from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.wsy.learn.designmodel.SingletonLazy1$InnerSingleton from file:/Users/wangsiyuan1/workspace/springtest/target/classes/]
com.wsy.learn.designmodel.SingletonLazy1@27716f4

生产者消费者模式-Java

生产者消费者模式

生产者-消费者模式在服务端编程中,是一种很常见的设计模式,比如消息队列的实现,就是这种思想。本文就是用Java语言编写一个简单的生产者消费者例子,从而引出concurrent包下的阻塞队列和ReentrantLock一些玩法。

##基础知识

首先复习下基础知识,在Java中concurrent包下并发队列分为阻塞队列非阻塞队列,ConcurrentLinkedQueue是非阻塞队列,底层实现用了CAS。阻塞队列包括LinkedBlockingQueue,LinkedBlockingDeque,LinkedTransferQueue,ArrayBlockingQueue,阻塞队列底层是靠ReentrantLock实现。Condition包括await,signal,signalAll,Condition作为条件锁

我们知道Lock的本质是AQS,AQS自己维护的队列是当前等待资源的队列,AQS会在被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行,直到队列为空。

而Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列。

但是,两个队列的作用不同的,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:

  • 1、线程1调用reentrantLock.lock时,尝试获取锁。如果成功,则返回,从AQS的队列中移除线程;否则阻塞,保持在AQS的等待队列中。

  • 2、线程1调用await方法被调用时,对应操作是被加入到Condition的等待队列中,等待signal信号;同时释放锁。

  • 3、锁被释放后,会唤醒AQS队列中的头结点,所以线程2会获取到锁。
  • 4、线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候,线程1 并没有被唤醒,只是被加入AQS等待队列。
  • 5、signal方法执行完毕,线程2调用unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,线程1被唤醒,线程1恢复执行。

所以,发送signal信号只是将Condition队列中的线程加到AQS的等待队列中。只有到发送signal信号的线程调用reentrantLock.unlock()释放锁后,这些线程才会被唤醒。可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。 signal就是唤醒Condition队列中的第一个非CANCELLED节点线程,而signalAll就是唤醒所有非CANCELLED节点线程,本质是将节点从Condition队列中取出来一个还是所有节点放到AQS的等待队列。尽管所有Node可能都被唤醒,但是要知道的是仍然只有一个线程能够拿到锁,其它没有拿到锁的线程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。

##生产者-消费者代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 用阻塞队列实现生产-消费者模式
*/
public class Productor {
static LinkedBlockingQueue<Integer> blockQueue = new LinkedBlockingQueue<>(10);

void provide() throws InterruptedException {
for (int i = 0; i < 10; i++) {
blockQueue.offer(i);
System.out.println("生产:" + i);
Thread.sleep(100);
}
}
}
1
2
3
4
5
6
7
8
9
10
/**
* 基于阻塞队列消费者
*/
public class Consumer {
void cusume() throws InterruptedException {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("消费:" + Productor.blockQueue.take());
}
}
}
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
static void test0() {
//创建生产者
Productor productor = new Productor();
//创建消费者
Consumer consumer = new Consumer();
Thread cusumerThread = new Thread(() -> {
try {
consumer.cusume();
} catch (Exception e) {

}

});
cusumerThread.start();

try {
productor.provide();

cusumerThread.interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}

输出:
生产:0
消费:0
生产:1
消费:1
生产:2
消费:2
生产:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
消费:6
生产:7
消费:7
消费:8
生产:8
生产:9
消费:9
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
/**
* 自己实现的简单的阻塞队列
*/
public class MyBlockQueue<E> {
private LinkedList<E> queue = new LinkedList<>();
//第一种实现方式,采用了Object wait和notify的方式
private Object lock = new Object();
//第二种实现方法,采用了ReentrantLock获取Condition,通过condition await和signal方式实现
private ReentrantLock reentrantLock = new ReentrantLock();
Condition con = reentrantLock.newCondition();

void offer(E e) {
queue.offer(e);
synchronized (lock) {
lock.notifyAll();
}
}

E take() throws InterruptedException {
if (queue.size() == 0) {
synchronized (lock) {
lock.wait();
}
}
return queue.poll();
}

void offer1(E e) throws InterruptedException {

try {
reentrantLock.lockInterruptibly();
queue.offer(e);
con.signalAll();

} finally {
reentrantLock.unlock();
}
}

E take1() throws InterruptedException {
if (queue.size() == 0) {
try {
reentrantLock.lockInterruptibly();
con.await();

} finally {
reentrantLock.unlock();
}
}
return queue.poll();
}
}

Java线程基础知识

Java线程基础

如下图所示,Java中线程可分为NEW,RUNABLE,RUNING,BLOCKED,WAITING,TIMED_WAITING,TERMINATED 共七个状态,一个状态是如何过渡到另一个状态图中标识的很清楚。

img

  • 初始状态(NEW)
    实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

  • 就绪状态(RUNNABLE)
    就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。

    • 调用线程的start()方法,此线程进入就绪状态。
    • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
    • 锁池里的线程拿到对象锁后,进入就绪状态。
  • 运行中状态(RUNNING)
    线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

  • 阻塞状态(BLOCKED)
    阻塞状态是线程阻塞在进入synchronized关键字(当然也包括ReentrantLock)修饰的方法或代码块(获取锁)时的状态。

  • 等待(WAITING)
    处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

  • 超时等待(TIMED_WAITING)
    处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

  • 终止状态(TERMINATED)
    当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

Java线程常用API

img

其中常用的方法为start(),yield(),sleep(),interrupt(),interrupted(),isInterrupted(),isAlive(),join(),

setDaemon(),setName(),setPriority(),其中stop方法和destroy方法,都是被废弃的方法在日常使用中不建议用。除了Thread类下的API,Object类下的wait(),notify(),notifyAll(),这三个方法也经常在多线程场景中出现。本文的目的,主要讲解的就是这些方法的使用和内部原理。

方法 方法说明 Exception
Thread.start 开启执行线程,由虚拟机负责调用run方法。(Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.) IllegalThreadStateException if the thread was already started.
Thread.yield 让出CPU,但是仅仅对同线程级别(Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilise a CPU)
Thread.sleep 使得正在执行的当前线程睡眠。敲黑板!但是不会让出任何锁的所有权( The thread does not lose ownership of any monitors.)这个特性很重要,也就是说在同步块中使用Thread.sleep要谨慎。 当线程中断会抛出InterruptedException异常,并同时清空中断标志位
Thread.interrupt 中断线程,但只是设置了中断标志位,此刻调用isInterrupted返回true。例子请参考下面的示例代码testInterrupt0方法。只会打印到0-9循环跳出 SecurityException if the current thread cannot modify this thread 请教这个异常什么时候会发生呢?
Thread.isInterrupted 查看线程是否处于中断状态.true为中断。调用之后不清除中断标志位。
Thread.interrupted 查看线程是否处于中断状态.true为中断。调用之后清除中断标志位。心细的同学已经发现和isInterrupted的区别了吧。
Thread.isAlive 线程是否存活,A thread is alive if it has been started and has not yet died.
Thread.join 等待线程死亡之后再执行。(Waits for this thread to die) 当线程中断会抛出InterruptedException异常,并同时清空中断标志位
Thread.setDaemon 设置为守护线程。任何非守护线程还在运行,守护线程就不会终止,最典型的守护线程是垃圾回收器的回收线程。 IllegalThreadStateException 当线程状态是alive的时候不能调用setDaemon
Thread.setName 设置线程的name
Thread.setPriority 设置线程的优先级。MIN_PRIORITY为1,MAX_PRIORITY为10,NORM_PRIORITY为5。
Object.wait 如果不指定timeout,则一直阻塞直到其他线程调用notify或者notifyAll。敲黑板!调用wait之后当前线程不在持有对象锁。
Object.notify 随机唤醒同一个对象monitor下的某个线程
Object.notifyAll 唤醒同一个对象monitor下的所有线程。查看testNotify示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void testInterrupt0()  Exception {
int i = 0;
while (!Thread.currentThread().isInterrupted()) {
System.out.println("loop" + i++);
if(i == 10) {
Thread.currentThread().interrupt();
}
}
//echo true
System.out.println(Thread.currentThread().isInterrupted());
//echo true
System.out.println(Thread.currentThread().interrupted());
//echo false
System.out.println(Thread.currentThread().isInterrupted());
}
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
public class MyThread extends Thread {
private Object lock;
private String name;

public MyThread(Object lock, String name) {
this.lock = lock;
this.name = name;
}

@Override
public void run() {
synchronized (lock) {
try {
System.out.println(name + " get lock,interrupt =" + Thread.currentThread().isInterrupted());

lock.wait();

//Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(name + " is interrupt. notify all interrupt =" + Thread.currentThread().isInterrupted());
lock.notifyAll();
}
System.out.println(name + ":notified...");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void testNotify() throws Exception {
MyThread t1 = new MyThread(lock, "t1");
MyThread t2 = new MyThread(lock, "t2");

Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t2);

thread1.start();
thread2.start();

Thread.sleep(1000);
long startTime = System.currentTimeMillis();
synchronized (lock) {
System.out.println("main get lock");
lock.notifyAll();
}

thread1.join();
thread2.join();
long endTime = System.currentTimeMillis();

System.out.println("notify lock.time =" + (endTime - startTime));
}

testNotify执行结果

t1 get lock,interrupt =false
t2 get lock,interrupt =false
main get lock
t2:notified…
t1:notified…

Java对象刨根问底

如何创建一个对象

Java创建一个对象,最简单的方式就是

1
Object a = new Object();

可以,大家有没有想过,在JVM中,创建一个对象的流程是如何呢?其实具体流程入下图所示。

img

###分配内存

对象所需的内存大小,在类加载完成后便可以完全确认,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。那么如何划分,有两种方式:

  • 指针碰撞(Bump The Pointer):假设Java堆中的内存是绝对规整的,所有用过的内存都放到一边,空闲的内存放到另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就是朝着空闲的那一边“挪动”与对象大小一样的距离。
  • 空闲列表(Free List):如果Java堆中的内存不规整,已使用的内存和未使用的内存相互交错,就没办法简单进行指针碰撞了,需要维护一个列表,记录哪些内存块是可用的,在分配的时候,找一块足够大的空间分配给对象,并更新列表的记录。

因此,在使用Serial,ParNew等带Compact过程的收集器时,系统采用的分配方式是指针碰撞法,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用的是空闲列表法。

除了分配方法外,还需要考虑同步的问题。实际上虚拟机采用了CAS配上失败重试的方式保证更新的原子性,另一种是把内存分配的动作按照线程划分在不同的空间之中,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),可以通过-XX:+UseTLAB参数来设定。

对象内存布局

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

对象头

HotSpot虚拟机对象头包括两部分内容:

  • 第一部分

用于存储对象自身的运行数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分长度在32bit和64bit虚拟机中分别为32bit和64bit,官方称它为“Mark Word”。

存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空,不需要记录信息 11 GC标记
偏向线程ID,偏向时间戳,对象分代年龄 01 可偏向
  • 第二部分

类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

对象访问定位

有两种方式,句柄访问直接指针访问。这两种访问方式各有优点,使用句柄的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收移动对象是很常见的事情)时只会改变句柄中的实例数据指针,而reference本身不需要更新。使用直接指针访问方式最大好处就是速度更快,它节省一次指针定位的时间开销,由于对象访问在Java中非常频繁,这类开销积少成多,将是一个比较可观的提升。HotSpot就是采用直接指针法。

img

​ 通过句柄访问对象

img

​ 通过直接指针访问对象

java类加载内部流程

Java类加载机制的七个阶段

而 JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。

##加载

下面是对于加载过程最为官方的描述。

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。

其实加载阶段用一句话来说就是:把代码数据加载到内存中。这个过程对于我们解答这道问题没有直接的关系,但这是类加载机制的一个过程,所以必须要提一下。

验证

当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。这个校验过程大致可以分为下面几个类型:

  • JVM规范校验。JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。例如:文件是否是以 0x cafe bene开头,主次版本号是否在当前虚拟机处理范围之内等。
  • 代码逻辑校验。JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类。

当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。这个过程对于我们解答问题也没有直接的关系,但是了解类加载机制必须要知道有这个过程。

##准备(重点)

当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

  • 内存分配的对象。Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。

1
2
public static int factor = 3;
public String website = "www.cnblogs.com/chanshuyi";
  • 初始化的类型。在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。

例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。

1
public static int sector = 3;

但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。

1
public static final int number = 3;

之所以 static final 会直接被复制,而 static 变量会被赋予零值。其实我们稍微思考一下就能想明白了。

两个语句的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在 Java 中代表不可改变的意思,意思就是说 number 的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。

解析

当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

其实这个阶段对于我们来说也是几乎透明的,了解一下就好。

##初始化(重点)

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

看到上面几个条件你可能会晕了,但是不要紧,不需要背,知道一下就好,后面用到的时候回到找一下就可以了。

##使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以。

##卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。这个阶段也只是了解一下就可以。

总结

从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤:

  • 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
  • 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
  • 初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
  • 初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。

如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。

引用

https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html

Java动态代理

代理模式

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

举一个例子:老师要批作业,可以找学习委员代收,学一委员整理好之后,再送到老师的办公室。这个场景中,学习委员就是一个代理。

##为什么要用代理模式

  • 中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
  • 开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。

静态代理

静态代理,就是采用代理模式思想,通过硬编码定义一个代理类,来实现代理的功能。由于静态代理灵活性较弱,在实际应用中,大多数采用动态代理模式。

动态代理模式

jdk动态代理

Jdk动态代理是基于接口的。

  • Step1 定义一个interface Hello
1
2
3
public interface Hello {
void sayHello(String str);
}
  • Step2 定义HelloImpl
1
2
3
4
5
6
7
8
9
10
11
12
public final class HelloImpl implements Hello {
private int[] array;

public HelloImpl() {
this.array = new int[1024*10];
}

@Override
public void sayHello(String str) {
System.out.println("hello " + str);
}
}
  • Step3 实现InvocationHandler的invoke方法
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
public class LogInvocationHandler implements InvocationHandler {
private Hello hello;

public LogInvocationHandler(Hello hello) {
this.hello = hello;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("sayHello".equals(method.getName())) {
System.out.println("You said: " + Arrays.toString(args));
}
return method.invoke(hello, args);
}


public static void main(String[] args) {
Object proxy = Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), // 1. 类加载器
new Class<?>[]{Hello.class}, // 2. 代理需要实现的接口,可以有多个
new LogInvocationHandler(new HelloImpl()));// 3. 方法调用的实际处理者

Hello hello = (Hello) proxy;
hello.sayHello("world");
System.out.println(hello);
}
}

但是Jdk动态代理使用上,有局限性。不能所有的类,都要实现一个interface,这会大大增加代码的冗余。如果使用继承的话,就能够解决这个问题。

CGLIB

cglib通过继承来实现动态代理,cglib的底层实现,是基于节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。如下是class文件的格式。

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
ClassFile { 
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

magic: 作为一个魔数,确定这个文件是否是一个能被虚拟机接受的class文件,值固定为0xCAFEBABE
minor_versionmajor_version:分别表示class文件的副,主版本号,不同版本的虚拟机实现支持的Class文件版本号不同。
constant_pool_count:常量池计数器,constant_pool_count的值等于常量池表中的成员数加1。
constant_pool:常量池,constant_pool是一种表结构,包含class文件结构及其子结构中引用的所有字符常量、类或接口名、字段名和其他常量。
access_flagsaccess_flags是一种访问标志,表示这个类或者接口的访问权限及属性,包括有ACC_PUBLICACC_FINALACC_SUPER等等。
this_class:类索引,指向常量池表中项的一个索引。
super_class:父类索引,这个值必须为0或者是对常量池中项的一个有效索引值,如果为0,表示这个class只能是Object类,只有它是唯一没有父类的类。
interfaces_count:接口计算器,表示当前类或者接口的直接父接口数量。
interfaces[]:接口表,里面的每个成员的值必须是一个对常量池表中项的一个有效索引值。
fields_count:字段计算器,表示当前class文件中fields表的成员个数,每个成员都是一个field_info
fields:字段表,每个成员都是一个完整的fields_info结构,表示当前类或接口中某个字段的完整描述,不包括父类或父接口的部分。
methods_count:方法计数器,表示当前class文件methos表的成员个数。
methods:方法表,每个成员都是一个完整的method_info结构,可以表示类或接口中定义的所有方法,包括实例方法,类方法,以及类或接口初始化方法。
attributes_count:属性表,其中是每一个attribute_info,包含以下这些属性,InnerClassesEnclosingMethodSyntheticSignatureAnnonation等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 动态代理不能是final类
* 如果是final类,会抛出异常:java.lang.IllegalArgumentException: Cannot subclass final class com.wsy.learn.jvm.autoproxy.cglib.HelloConcrete
*/
public class HelloConcrete {
/**
* 注意,此处如果定义的是final方法,则不会进入intercept方法进行方法增强
* @param str
* @return
*/
public final String sayHello(String str) {
return "HelloConcrete: " + str;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("You said: " + Arrays.toString(args));
return proxy.invokeSuper(obj, args);
}

public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());

HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));
}
}

动态代理最佳实践 - SpringAOP

关于什么是SpringAOP,在此不继续阐述。

springAOP既采用了jdk proxy,也采用了cglib。
spring会根据情况,来选择jdk动态代理还是cglib动态代理。

  • 如果要代理的对象实现了接口,则默认采用jdk proxy。
  • 如果要代理的对象实现了接口,也可以强制使用cglib。需要配置<aop:aspectj-autoproxy proxy-target-class=”true”/>
  • 如果要代理的对象没有实现接口,则采用cglib动态代理。
  • 但是注意,敲黑板!敲黑板!敲黑板!
    • 采用cglib的前提下,final类不能进行动态代理会抛出异常java.lang.IllegalArgumentException: Cannot subclass final class xxx(final类不能被继承,所以你懂得)。
    • 采用cglib的前提下,final方法也不能进行动态代理,不过不会抛出异常,而是直接跳过功能增强。
    • 采用jdk proxy,final类和final方法都能正常工作。

JVM内存区域

JVM内存区域总体介绍​

其中VM Stack(虚拟机栈),Native Method Stack(本地方法栈),Program Counter Register(程序计数器),是线程私有的。Method Area(方法区),Heap(堆)是线程共享的。

注意:没有特殊说明,本文中所讲的都是基于HotSpot虚拟机。

img

##程序计数器

程序计数器是线程私有的,是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中(也仅是概念模型,各个虚拟机可能通过更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

多线程场景下,为了线程切换后能恢复到正确的位置,所以每个线程需要一个独立的程序计数器。

如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器值则为空(Undefined)。次内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

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

局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference类型,它不等同于对象本身,可能是指向对象起始地址的引用指针,也可能是代表对象的句柄)。局部变量表所需的内存空间在编译期间完成分配,方法在帧中分配多大的空间,也是完全确定的。

这个区域存在两种异常情况:

  • 当栈的深度超过虚拟机所允许的深度的时候,抛出StackOverflowError。
  • 如果虚拟机栈允许动态扩展,如果无法申请到足够的内存,则会抛出OutOfMemoryError。

本地方法栈

本地方法栈的功能和Java虚拟机栈很接近,它们的区别是Java虚拟机栈是为虚拟机执行Java代码(字节码)服务,本地方法栈则为虚拟机使用到的Native方法服务。本地方法栈,也存在StackOverflowError和OutOfMemoryError。

Java堆

对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有对象都分配到堆上也变得不那么绝对了。

Java堆是垃圾收集器管理的主要区域,从内存回收角度来看,由于现在收集器基本都采用分代收集算法,Java堆中还分为了新生代和老年代。当堆无法继续扩展的时候,将会抛出OutOfMemoryError。

方法区

方法区(Method Area)也是线程共享区域,它用于存储被虚拟机加载的类元信息、常量、静态变量、及时编译器编译后的代码等数据。JDK8之前,虚拟机采用永久代实现方法区,类元信息、常量、静态变量等都存在堆中,和年轻代、老年代是连续的。

1
2
3
4
5
-XX:PermSize
方法区初始大小
-XX:MaxPermSize
方法区最大大小
超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen

Jdk8之后,官方废弃了永久代,在本地内存中开辟了一块空间,叫做元空间(MetaSpace),专门存储类的元数据信息。常量、静态变量存储到了堆上。

直接内存

很多人忽略了直接内存,举一个例子,JDK NIO中,Buffer调用的Native方法直接分配是堆外内存,也就是直接占用系统内存。所以在调整JVM参数的时候,要给系统留一些Buffer。避免内存动态扩展时出现问题。

ThreadLocal原理分析

ThreadLocal总体介绍

​ ThreadLocal类在并发编程中,每个线程实例,都有一份独立的副本,采用以空间换时间的方式,处理并发。

在Thread类中,ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap就是线程变量的容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  static class ThreadLocalMap {

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}

​ 如上代码可以看到Entry继承了WeakReference,在构造函数中super(key),key就是指ThreadLocal引用本身this。跟WeakHashMap不同的是,ThreadLocal的构造函数,没有指定ReferenceQueue。ThreadLocal用一种不同的方式,来避免内存泄漏,在下面的章节中,会详细介绍ThreadLocal的方式。

​ ThreadLocal的API,就不在此阐述,很简单。

每个线程独立备份

​ 为了达到这个目的,有两个方案。

## 方案A

 ThreadLocal 维护一个 Map,键是 Thread,值是它在该 Thread 内的实例。增加线程与减少线程均需要写 Map,故需保证该 Map 线程安全。线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏。
  • 结论:加锁势必会降低ThreadLocal的性能。猜测JDK为了性能考虑,没有采用此方案

方案B

Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。该方案虽然没有锁的问题,但是由于每个线程访问某 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),则这些 ThreadLocal 不能被回收,可能会造成内存泄漏。
  • 结论:JDK采用了B方案,也有对应的方案来解决内存泄漏问题

#JDK如何解决内存泄漏问题