Skip to the content.

第三部分 加强

虽然 md 名称是加强,但是实际上还是 JavaSE 基础巩固。

第一章 枚举

枚举的使用Demo

public String judge(String str){
    if("AAA".equals(str)){
        return "AAA";
    }else if("BBB".equals(str)){
        return "BBB";
    }else if("CCC".equals(str)){
        return "CCC";
    }else if("DDD".equals(str)){
        return "DDD";
    }
}

用枚举替代 if else

// 直接用枚举
enum RoleOperation1 {
    ADMIN_POWER,
    NORMAL_POWER,
    SUPER_POWER
}

// 因为有返回值 所以这样定义
enum RoleOperation2 {
    ADMIN_POWER() {
        @Override
        public String toString() {
            return "Admin power";
        }
    },
    NORMAL_POWER() {
        @Override
        public String toString() {
            return "Normal power";
        }
    },
    SUPER_POWER() {
        @Override
        public String toString() {
            return "Super power";
        }
    }
}

// 因为有统一的方法,所以用接口定义规则
interface Operation {
    String op();
}

// 漂亮的枚举代码,虽然看起来长,复杂,但是拓展性特别强!
// 下面就是见证奇迹的时刻,优雅地用枚举替代if else。
public enum RoleOperation implements Operation {
    ADMIN_POWER() {
        @Override
        public String op() {
            return "Admin power";
        }
    },
    NORMAL_POWER() {
        @Override
        public String op() {
            return "Normal power";
        }
    },
    SUPER_POWER() {
        @Override
        public String op() {
            return "Super power";
        }
    }
}
public class Demo1 {
    // 如此优雅的代码!!
    // 还有用工厂模式 策略模式的。感觉都不如枚举来的优雅。
    public String judge(String role) {
        return RoleOperation.valueOf(role).op();
    }
}

枚举的常用方法

values() 以数组形式返回枚举类型的所有成员
valueOf() 将普通字符串转换为枚举实例
compareTo() 比较两个枚举成员在定义时的顺序
ordinal() 获取枚举成员的索引位置
package org.example.enumeration;

import org.junit.jupiter.api.Test;

// 枚举中一些常用方法
public class SomeFunc {
    @Test
    public void func1() {
        Color[] values = Color.values();
        for (Color c : values) {
            System.out.println(c);
        }
    }

    @Test
    public void func2() {
        //  将普通字符串实例转换为枚举
        Color blue = Color.valueOf("BLUE");
        System.out.println(blue);
    }

    @Test
    public void func3() {
        System.out.println(Color.BLUE.ordinal());
    }

    @Test
    public void func4() {
        // RED 和 BLUE 比较, RED 小于 BLUE 返回负数 ;equals 返回 0;大于返回 正数
        System.out.println(Color.RED.compareTo(Color.BLUE)); // -1
        System.out.println(Color.RED.compareTo(Color.GREEN));// -2
    }

    @Test
    public void func() {
        System.out.println(Color.RED);
        // output RED
    }
}

enum Color {
    RED, BLUE, GREEN
}

第二章 比较对象

Comparator 和 Comparable

Comparable 接口/ Comparator 接口

第三章 单元测试

单元测试的优点

保证的程序代码的正确性【语法上了逻辑上】。

单元测试的使用

@Test

public class JunitDemo {
    private OutputStream outputStream;

    @Before
    public void init() throws FileNotFoundException {
        System.out.println("IO 流初始化完毕了");
        outputStream = new FileOutputStream("junit.txt");
    }

    @Test
    /**
     * 单元测试判断数据的正确性,一般用Assert里面的方法
     */
    public void fn1(){
        // 断言不为null  不是null则成功
        Assert.assertNotNull(outputStream);
    }

    @After
    public void destory() throws IOException {
        System.out.println("IO 流关闭了");
        outputStream.close();
    }
}

第四章 反射

反射概述

反射可以把类的各个组成部分封装为其他对象。流行的框架基本都是基于反射的思想写成的。

Java 反射机制是在程序的运行过程中,对于任何一个类,都能够知道它的所有属性和方法;对于任意一个对象,都能够知道它的所有属性和方法,这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

Java 反射机制主要提供了以下这几个功能:

反射的基本操作

获取成员变量

获取构造方法

获取成员方法

获取类名

几个重要的类

Class 类

每定义一个java class 实体都会产生一个 Class 对象。我们编写一个类,编译完成后,在生成的 .class 文件中,就会产生一个 Class 对象,这个 Class 对象用于表示这个类的类型信息。Class 中没有公共构造器,即 Class 对象不能被实例化。

Field 类

Field 类提供类或接口中单独字段的信息,以及对单独字段的动态访问。

Method 类

invoke(Object obj, Object... args)

ClassLoader 类

ClassLoader 类加载器!类加载器用来把类(class)装载进 JVM 的。ClassLoader 使用的双亲委派模型来搜索加载类的,这个模型也就是双亲委派模型。

ClassLoader 的类继承图如下:

动态代理

作用

运行时,动态创建一组指定的接口的实现类对象!(在运行时,创建实现了指定的一组接口的对象)

动态代理对比其他方法增强方式

基本Demo

interface A{}
interface B{}
Object o = 方法(new Class[]{ A.class, B.class })
// o 它实现了A 和 B 两个接口!
Object proxyObject = Proxy.newProxyInstance(ClassLoader classLoader, Class[] interfaces, InvocationHandler h);

动态代理基本Demo

interface IBase {
    public void say();
    public void sleep();
    public String getName();
}
public class Person implements IBase {
    public void say() { System.out.println("hello"); }
    public void sleep() { System.out.println("sleep"); }
    public String getName() { return "getName"; }
}
public class ProxyDemo1 {
    public static void main(String[] args) {

        Person person = new Person();
        // 获得类加载器
        ClassLoader classLoader = person.getClass().getClassLoader();
        // 获得被代理对象实现的接口
        Class[] interfaces = person.getClass().getInterfaces();
        // 实例化一个处理器 用于增强方法用的
        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                method.invoke(person, args);
                return null;
            }
        };
        IBase p = (IBase) Proxy.newProxyInstance(classLoader, interfaces, h);
        // 获得代理类的名称 com.sun.proxy.$Proxy0
        System.out.println(p.getClass().getName());
        p.say();
    }
}

invoke解释

public Object invoke(Object proxy, Method method, Object[] args);

这个 invoke 什么时候被调用?

参数解释


public class ProxyDemo2 {
    public static void main(String[] args) {
        Person person = new Person();
        ClassLoader classLoader = person.getClass().getClassLoader();
        Class[] interfaces = person.getClass().getInterfaces();
        System.out.println(interfaces.length);
        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object retVal = method.invoke(person, args);
                 // 这个返回了,方法才有返回值 
                return retVal; 
            }
        };
        IBase p = (IBase) Proxy.newProxyInstance(classLoader, interfaces, h);
        p.say();
        // invoke返回null的话,这里的输出就是null
        System.out.println(p.getName());
    }
}

模拟 AOP

Spring AOP,感受一下什么叫增强内容可变

第五章 注解

注解也叫元数据。是一种代码级别的说明,JDK1.5 引入的特性,与类,接口,枚举是在同一层次。可声明在包,类,字段,方法,局部变量,方法参数等的前面,下面对这些元素进行说明。

作用分类:

内置注解

自定义注解

元注解 public @interface annotationName{}

反编译发现,本质就是一个接口。

import java.lang.annotation.Annotation;

public interface Annotation extends Annotation {}

属性的返回值

基本数据类型、String、枚举、注解、及以上类型的数组

赋值问题

设置默认值 String sex() default "1";

使用注解,数组类型的赋值 str={xx,xx,xx},若数组中只有一个,大括号可省略。回忆 Spring 中的注解

元注解

用于描述注解的注解

@Target:描述注解的位置

@Retention:描述注解是被保留的阶段

@Retention(RetentionPolicy.RUNTIME):当前被描述的注解,会保留到 class 字节码文件中,并被 JVM 读取到

@Documented:描述注解是否被抽取到 API 文档中

@Inherited:描述注解是否被子类继承

注解的解析

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Pro {
    String className();
    String methodName();
}

@Pro(className = "com.bbxx.Demo1",methodName = "show1")
public class RefelectDemo {
    public static void main(String[] args) throws Exception {
        // 解析注解
        Class<RefelectDemo> refelectDemoClass = RefelectDemo.class;
        Pro annotation = refelectDemoClass.getAnnotation(Pro.class);
        String s = annotation.className();
        String s1 = annotation.methodName();
        Class<?> aClass = Class.forName(s);
        Object o = aClass.newInstance();
        Method declaredMethod = aClass.getDeclaredMethod(s1);
        declaredMethod.setAccessible(true);
        declaredMethod.invoke(o);
    }
}

第六章 类加载器

分类

ClassLoad 分类

ClassLoad 有个双亲委派模型,会先问父 类加载器/上级类加载器,向上级委托,没有就自己加载,没找到就抛出 ClassNotFound。永远不会出现类库中的类被系统加载器加载,应用下的类被引导加载。

委托父加载器加载,父可以加载就让父加载。父无法加载时再自己加载。

类加载的顺序

class MyApp{
    public static void main(String[]args){ // 系统加载
        // 也由系统加载
        A a = new A(); 
        // 也由系统加载 (从系统开始匹配,最终会委托上去, ...由引导加载)
        String s = new String();
    }
}

class String{ // 引导加载, String类,类库中的
    private Integer i;// 直接引导加载,毕竟无法委托了!
}

其实还得分线程,每个线程都有一个当前的类加载器来负责加载类。

流程

基础阶段 了解,中级阶段 熟悉,高级阶段,不清楚

继承 ClassLoader 类完成自定义类加载器。自定义类加载器一般是为了加载网络上的类,class 在网络中传输,为了安全,那么 class 需要加密,需要自定义类加载器来加载(对 class 做解密工作)

ClassLoader 加载类都是通过 loadClass() 方法来完成的。loadClass() 方法的工作流程如下:

我们要自定义一个类加载器,只需要继承 ClassLoader 类。然后重写它的 findClass() 方法即可。在 findClass() 中我们需要完成如下的工作!

自定义类加载器

文件类加载器

public class MyClassLoader extends ClassLoader {
    private String directory;

    public MyClassLoader(String _directory, ClassLoader paraent) {
        super(paraent);
        this.directory = _directory;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 把类名转为目录
            String file = directory + File.separator + name.replace(".", File.separator) + ".class";
            // 构建输入流
            InputStream fis = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int len = -1;
            while ((len = fis.read(buf)) != -1) {
                baos.write(buf, 0, len);
            }
            byte[] byteArray = baos.toByteArray();
            fis.close();
            baos.close();

            return defineClass(name, byteArray, 0, byteArray.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

热部署,越过双亲委派,就是不用 loadClassfindClass

复杂例子

package org.example.classloader;

import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Method;

/**
 * 类加载器学习
 * 注意maven中的单元测试只能写在 test下面!
 * 字节码文件请自己生成一个 然后调用对应的方法哦!!
 */
public class ClassLoaderDemo extends ClassLoader {

    // 类加载器的地盘,指明加载那个地方的class文件
    private String classpath;

    public ClassLoaderDemo() {}

    public ClassLoaderDemo(String classpath) {
        this.classpath = classpath;
    }

    public static void main(String[] args) throws Exception {
        ClassLoaderDemo classLoaderDemo = new ClassLoaderDemo();
        classLoaderDemo.fun2();
    }

    // 执行字节码的非静态方法
    public void fun1() throws Exception {
        ClassLoaderDemo classLoaderDemo = new ClassLoaderDemo("D:\\");
        Class<?> clazz = classLoaderDemo.loadClass("org.example.classloader.ClassLoaderTest");
        // loaderSay是一个非静态方法,需要一个实例调用
        Method loaderSay = clazz.getMethod("loaderSay");
        ClassLoaderTest o = (ClassLoaderTest) clazz.newInstance();
        // 非静态方法需要一个实例进行调用
        loaderSay.invoke(o);
    }


    // 执行字节码的静态方法
    public void fun2() throws Exception {
        ClassLoaderDemo classLoaderDemo = new ClassLoaderDemo("D:\\");
        Class<?> clazz = classLoaderDemo.loadClass("org.example.classloader.ClassLoaderTest");
        // loaderSay是一个非静态方法,需要一个实例调用
        Method loaderSay = clazz.getMethod("loaderStaticFunction");
        // 静态方法不用实例
        String result = (String) loaderSay.invoke(null);
        System.out.println(result);
    }

    // 重写这个方法即可
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 自定义的方法,通过类名找到class文件,把文件加载到一个字节数组中
            byte[] datas = getClassData(name);
            if (datas == null) {
                throw new ClassNotFoundException("类没有找到:" + name);
            }
            return this.defineClass(name, datas, 0, datas.length);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类找不到:" + name);
        }
    }

    private byte[] getClassData(String name) {
        // 把名字换成文件夹的名字
        name = name.replace(".", "\\") + ".class";
        File classFile = new File(classpath, name);
        System.out.println(classFile.getAbsoluteFile());
        return readClassData(classFile);
    }

    private byte[] readClassData(File classFile) {
        if (!classFile.exists()) return null;
        byte[] bytes = null;
        try {
            FileInputStream fis = new FileInputStream(classFile);
            bytes = fis.readAllBytes();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bytes;
    }
}

Tomcat类加载器

tomcat 提供了两种类加载器。

第一种 服务器类加载器

第二种 应用类加载器

总结

tomcat 破坏了双亲委派模型

引导

扩展

系统

服务器类加载器:先自己动手,然后再去委托

应用类加载器:先自己动手,然后再去委托

第七章 并发

注意

不要调用 Thread 类或 Runnable 对象的 run 方法。直接调用 run 方法会在同一个线程中执行—-不会启动新的线程。调用 Thread.start() 方法会创建一个执行 run 方法的新线程。

线程的六种状态

//Thread内部的枚举类
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

ReentrantLock()

Condition,用 ReentrantLock() 的实例对象获得 Condition 对象

线程就是一个单独的资源类,没有任何附属的操作

线程局部变量 ThreadLocal

第八章 网络编程

采用 windows 的 telent 工具作为客户端进行发起连接。

入门

Client

/**
 * 测试服务器连接
 */
public class SocketTest {

    public static void fun1() {
        // jdk 7 try catch用法
        try (var socket = new Socket("time-a.nist.gov", 13)) {
            var scanner = new Scanner(socket.getInputStream());
            while (scanner.hasNextLine()) {
                System.out.println(scanner.nextLine() + "==");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void fun2() throws UnknownHostException, UnsupportedEncodingException {
        String host = "www.bilibili.com";
        InetAddress[] localhosts = InetAddress.getAllByName(host);
        for (InetAddress tmp : localhosts) {
            System.out.println(tmp.getHostAddress());
            System.out.println(tmp);
        }
    }

    public static void fun3() throws IOException {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("time-a.nist.gov", 13), 10000);
        Scanner scanner = new Scanner(socket.getInputStream());
        // Scanner类不是很熟悉
        while (scanner.hasNextLine()) {
            System.out.println(scanner.nextLine());
        }
    }

    public static void main(String[] args) throws IOException {
        fun3();
    }
}

Server

public class EchoServer {
    /**
     * 服务器端的 inputStream 和 outPutStream
     * inPutStream 输入流,输入到Server
     * outPutStream 输出流,输出到client
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8189);
        Socket accept = serverSocket.accept();
        // 控制台读入数据
        Scanner in = new Scanner(accept.getInputStream(), StandardCharsets.UTF_8);
        // 输出 IO流还是不熟悉 类的组合太复杂了
        // PrintWriter out = new PrintWriter(new OutputStreamWriter(accept.getOutputStream(), StandardCharsets.UTF_8), true);
        OutputStreamWriter out = new OutputStreamWriter(accept.getOutputStream(), StandardCharsets.UTF_8);
        out.write("connected");
        out.flush();

        boolean done = false;
        while (!done && in.hasNextLine()) {
            // 控制台输入数据
            String line = in.nextLine();
            // 输出到客户端
            out.write("Echo:" + line);
            out.flush();
            if ("BYE".equals(line.trim())) done = true;
        }
    }
}

第九章 Servlet3.0

使用型特性就是在保护你的 Java 职业生涯。

注解替代xml

@WebServlet("/index.do")
public class IndexServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setAttribute("data", fakeData());
        request.getRequestDispatcher("/demo.jsp").forward(request, response);
    }

    public ArrayList<User> fakeData() {
        ArrayList<User> users = new ArrayList<>();
        users.addAll(Arrays.asList(
                new User("111", "111"),
                new User("222", "222"),
                new User("333", "333")));
        users.forEach(System.out::println);
        return users;
    }
}

异步响应

异步响应如果不设置编码格式 可能会导致异步失败(有乱码,异步可能会失败;主要是告诉它响应文本是什么。)测试了一下,的确是设置好响应文本即可。

异步响应如果过滤器这些东西没有设置为异步状态,也会导致异步失败

 * 类型 异常报告
 * 消息 当前链的筛选器或servlet不支持异步操作。
 * 描述 服务器遇到一个意外的情况,阻止它完成请求
 
 错误的原因就是过滤器没有设置  asyncSupported = true

代码案例

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    private char[] getOutPutChar(String str) {
        return str == null ? "   2020年 10月24日,祝各位程序员节日快乐! 2020-1024=996,想不到吧!".toCharArray() : null;
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 不加设置响应的类型的话,就无法异步。
        response.setContentType("text/html");
        AsyncContext asyncContext = request.startAsync(request, response);
        threadOutPut(asyncContext, response, getOutPutChar(null));
    }

    /**
     * @param asyncContext
     * @param response
     * @param outputStr    需要输出给浏览器的数据
     */
    private void threadOutPut(AsyncContext asyncContext, HttpServletResponse response, char[] outputStr) {
        asyncContext.start(() -> {
            try {
                PrintWriter print = response.getWriter();
                TimeUnit.MILLISECONDS.sleep(600);
                for (char c : outputStr) {
                    TimeUnit.MILLISECONDS.sleep(180);
                    print.print(c); print.flush();
                }
                asyncContext.complete();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                asyncContext.complete();
            }
        });
    }
}

文件上传

几个重要的 API

- request.getPart("file_name") // 获得文件对象Part
- part.getName() // 获得文件上传时的 name <input name="xx"> 中的name
- part.getSize() // 获得文件的大小
- part.getSubmittedFileName() // 获得提交的文件的名字。上传的是 demo.txt 那么得到的就是 demo.txt
- part.getInputStream(); // 获得文件输入流。

获取文件输入流后在用输出流 存入磁盘

文件上传的简单 Demo

文件上传用绝对路径【公司】

@WebServlet("/upload")
@MultipartConfig // 表示它支持文件上传
public class FileUpload extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Part part = request.getPart("file_name");
        System.out.println(part.getName());
        System.out.println(part.getSize());
        System.out.println(part.getSubmittedFileName());
        InputStream inputStream = part.getInputStream();
        // new FileOutputStream("filename") 这样是无法定位位置的,不能正常存储?
        //D:\citespace.projects.txt
        FileOutputStream fos = new FileOutputStream("D://" + part.getSubmittedFileName());
        // citespace.projects.txt
        // FileOutputStream fos = new FileOutputStream(part.getSubmittedFileName());
        byte[] bys = new byte[1024];
        int len = 0;
        while ((len = inputStream.read(bys)) != -1) {
            fos.write(bys, 0, len);
        }
        inputStream.close();
        fos.close();
    }
}
<html>
<head>
    <title>Title</title>
</head>
<body>
    enctype 说明有文件要提交过去
<form action="/Tomcat/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file_name">
    <input type="submit">
</form>
</body>
</html>

第十章 双亲委派

基本概念

class 文件通过类加载器装载至 JVM 中的。为了防止内存中存放在多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向上,避免重复加载字节码)

JDK 中的本地方法类一般由根加载器(Bootstrap loader)装载;JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader)实现装载;而程序中的类文件则由系统加载器(AppClassLoader)实现装载。

打破双亲委派机制

只要加载类的时候,不是从 App ClassLoader –> Ext ClassLoader –> BootStrap ClassLoader 这个顺序查找,就是打破了双亲委派机制。

加载 class 核心的方法在 LoaderClass 类的 loadClass 方法上(双亲委派机制的核心实现),只要我们定义个 ClassLoader,重写 loadClass 方法(不按照往上开始寻找类加载器),那就算是打破双亲委派机制了。

Tomcat 打破双亲委派机制

我们部署传统 JavaWeb 项目是,把 war 包放到 tomcat 的 webapp 下,这意味着一个 tomcat 可以运行多个 web 应用程序;

假设有两个 Web 应用程序,它们都有一个类,叫做 User,并且它们的类全限定名都一样,如都是 com.yyy.User,但是他们的具体实现是不一样的。那么 tomcat 如何保证它们是不会冲突的?tomcat 为每个 Web 应用创建一个类加载实例(WebAppClassLoader),该加载器重写了 loadClass 方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找,这样就做到了 Web 应用层级的隔离。

并不是 Web 应用程序下的所有依赖都需要隔离的,比如 Redis 就可以 Web 应用之间共享(有需要的话),因为如果版本相同,没必要每个 Web 应用程序都独自加载一份。具体做法是:Tomcat 在 WebAppClassLoader 上加了个父类加载器 (Shared ClassLoader),如果 WebAppClassLoader(加载指定目录下的类) 自身没有加载到某个类,就委托 SharedClassLoader 去加载(把需要应用程序之间需要共享的类放到一个共享目录下,Share ClassLoader 读共享目录的类即可)。

为了隔绝 Web 应用程序与 Tomcat 本身的类,又有类加载器(CatalinaClassLoader)来装载 Tomcat 本身的依赖。如果 Tomcat 本身的依赖和 Web 应用还需要共享,那么还有类加载器(CommonClassLoader)来装载而达到共享。各个类加载器的加载目录可以到 Tomcat 的 catalina.properties 配置文件上查看。

JDBC 破坏了双亲委派?

JDBC 定义了接口,具体实现类由各个厂商进行实现。

类加载的规则如下:如果一个类由类加载器 A 加载,那么这个类的依赖类也是由相同的类加载器加载。

使用 JDBC 的时候,是使用 DriverManager 进而获取 Connection,DriverManager 在 java.sql 包下,显然是由 BootStrap 类加载器进行装载。当我们使用 DriverManager.getConnection() 时,得到的一定是厂商实现的类,但 BootStrap ClassLoader 无法加载到各个厂商实现的类,因为这些实现类没在 java 包中。DriverManager 的解决方案是在 DriverManager 初始化的时候, 得到线程上下文加载器,去获取 Connection 的时候,是使用线程上下文加载器去加载 Connection 的,而这里的线程上下文加载器实际上还是 App ClassLoader,所以在获取 Connection 的时候,还是先找 Ext ClassLoader 和 BootStrap ClassLoader,只不过这两加载器肯定加载不到的,最终会由 App ClassLoader 进行加载!

有人觉得本应由 BootStrao ClassLoader 进行加载的 却改成 线程上下文加载器加载 就觉得破坏了。

有人觉得虽然改成了线程上下文加载器 但是依旧遵守 依次往上找父类加载器进行加载,都找不到时才由自己加载,认为原则上是没变的。

不重要好吧!理解为什么重要!

小结

前置知识:JDK 中默认类加载器有三个:AppClassLoader、Ext ClassLoader、BootStrap ClassLoader。AppClassLoader 的父加载器为 Ext ClassLoader、Ext ClassLoader 的父加载器为 BootStrap ClassLoader。这里的父子关系并不是通过继承实现的,而是组合。

什么是双亲委派机制:加载器在加载过程中,先把类交由父类加载器进行加载,父类加载器没找到才由自身加载。

双亲委派机制目的:为了防止内存中存在多份同样的字节码(安全)

类加载规则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。

如何打破双亲委派机制:自定义 ClassLoader,重写 loadClass 方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)

打破双亲委派机制案例:Tomcat

  1. 为了 Web 应用程序类之间隔离,为每个应用程序创建 WebAppClassLoader 类加载器
  2. 为了 Web 应用程序类之间共享,把 ShareClassLoader 作为 WebAppClassLoader 的父类加载器,如果 WebAppClassLoader 加载器找不到,则尝试用 ShareClassLoader 进行加载
  3. 为了 Tomcat 本身与 Web 应用程序类隔离,用 CatalinaClassLoader 类加载器进行隔离,CatalinaClassLoader 加载 Tomcat 本身的类
  4. 为了 Tomcat 与 Web 应用程序类共享,用 CommonClassLoader 作为 CatalinaClassLoader 和 ShareClassLoader 的父类加载器
  5. ShareClassLoader、CatalinaClassLoader、CommonClassLoader 的目录可以在 Tomcat 的 catalina.properties 进行配置

线程上下文加载器:由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader 无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。

第十一章 Java内存模型

Java 内存模型概述

Java 的内存模型 Java Memory Model,简称 JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式

JMM 关于同步的规定:

Java 内存模型三大特性

JMM 的三大特性,volatile 只保证了两个,即可见性和有序性,不满足原子性

为什么需要内存模型

背景

多核计算机,每个核心都会有高速缓存。高速缓存的为了解决 CPU 与内存(主存)直接的速度差异,L1,L2 缓存一般是【每个核心独占】一份的。L3 缓存一般是多核共享的。

为了让 CPU 提高运算效率,处理器可能会对代码进行【乱序执行】,即指令重排序,可以会议下计算机组成原理的流水线执行。

计算机中的一些操作往往是非原子性的,如 i++ 在执行的时候需要多个指令才能完成 i++ 这个操作。在单线程下,是不会存在什么问题的,因为单线程意味着无法并发。且在单线程下,编译器 /runtime/ 处理器 必须遵守 as-if-serial 语义,即它们不会对数据依赖关系的操作做重排序

缓存数据不一致

多个线程同时修改 【共享变量】,CPU 核心下的高速缓存是 【不共享】的,多个 cache 与内存直接的数据同部如何进行的?

MESI协议

MESI 协议,M(Modified)E(Exclusive)S(Share)I(Invalid)

缓存一致性协议锁的是缓存行进行加锁。缓存行是高速缓存存储的最小单位。

MESI 原理(计组那块的知识)

当每个 CPU 读取共享变量之前,会先识别数据的对象状态(修改、共享、独占、无效)。

独占:说明 CPU 将要得到的变量数据是最新的,没有被其他 CPU 同时读取。

共享:说明 CPU 将要得到的变量数据还是最新的,有其他 CPU 在读取,但是还没被修改。

修改:说明当前 CPU 正在修改该变量的值,同时会向其他 CPU 发送该数据状态为 invalid(无效)的通知,得到其他 CPU 响应后(其他 CPU 将数据状态从共享(share)变成invilid(无效)),会当前 CPU 将高速缓存的数据写到主存,并把自己的状态从 modify 变成 exclusive。如果 CPU 发现数据是 invilid 则需要从主存重新读取最新的数据。

MESI 协议做的就是判判断对象状态, 根据对象状态来采取不同的策略。在某个 CPU 在对数据进行修改时,需要同步通知其他 CPU ,表示这个数据被我修改了,你们不能用了。对比锁总线,MESI协议的“锁粒度”更小,性能更高。

CPU 优化

同步,意味着等待,什么都做不了,浪费 CPU 资源。解决方案是把 同步 变成 异步。修改时同步告诉其他 CPU ,而现在则把最新修改的值写到 store buffer 中,并通知其他 CPU 记得要修改状态,随后 CPU 就直接返回做其他事了。等收到其他 CPU 发过来的响应消息,再将数据更新到高速缓存中。

其他 CPU 接收到 invalid 通知时,也会把接收到的消息放入 invalid queue 中,只要写到 invalid queue 就会直接返回告诉修改数据的 CPU 已将状态置为 invalid。

异步的问题在于:现在 CPU 修改为 A 值,写到 store buffer 了,CPU 可以做其他事,如果该 CPU 又 接收指令需要修改 A 值,但上一次修改的值 还在 store buffer 中,未修改至高速缓存。 所以 CPU 在读取的时候,需要去 store buffer 看看存不存在,存在则直接取,不存在才读主存的数据。

CPU 乱序执行

如果是不同核心的 CPU 读它们共享的高速缓存,还是可能出现读旧值的问题。CPU1 修改了 A 值,把修改后值写到 store buffer 并通知 CPU2 对该值进行 invalid 操作,而 CPU2 可能还没收到 invalid 通知,就去做其他操作了,导致 CPU2 读到的还是旧值。这称之为 CPU 乱序执行。为了解决乱序问题,引出了内存屏障

内存屏障

内存屏障实际上是为了解决异步优化导致 CPU 乱序执行/缓存不及时可见 的问题,解决方案就是把异步优化禁用了。

内存屏障可分为:

屏障:操作数据时,往数据插入一条“特殊的指令”。只要遇见这条指令,那前面的操作都得【完成】。

写屏障:CPU当发现写屏障指令时,会把该指令之前存在于 store Buffer 所有写指令刷入高速缓存。通过这种方式就可以让 CPU 修改的数据可以马上暴露给其他 CPU,达到写操作可见性的效果。

读屏障:CPU 当发现读屏障指令时,会把该指令之前存在于 invalid queue 所有的指令都处理掉,通过这种方式就可以确保当前 CPU 的缓存状态是准确的,达到读操作一定是读取最新的效果。

深入 Linux 内核架构一书中,读、写屏障的解释:

Java内存模型

由于不同 CPU 架构的缓存体系不一样,缓存一致性协议不一样、重排序的策略不一样、所提供的内存屏障指令也有差异,为了简化 Java 开发人员的工作,Java 封装了一套规范:Java 内存模型

Java 内存模型希望屏蔽各种硬件和操作系统的访问差异,保证了 Java 程序在各种平台下对内存的访问都能得到一致的效果。目的是解决多线程存在的原子性、可见性(缓存一致性)以及有序性的问题。

小结

从源码到执行

流程概述

编译—>加载—>解释—>执行

加载

装载

查找并加载类的二进制数据,在 JVM 堆中创建一个 java.lang.Class 类的对象,并将类相关的信息存储在 JVM 方法区中。

装载后,class 文件就装载到了 JVM 中,并创建出了对应的 Class 对象和类信息,并这 Class 对象和类信息存储到了方法区中。

连接

对 class 的信息进行验证、为类变量分配内存空间并对其赋默认值。

连接的细化步骤为:验证—>准备—>解析

通过连接,对 class 信息做了校验并分配了内存空间和默认值。

初始化

为类的静态变量赋予正确的初始值。

过程:收集 class 的静态变量、静态代码块、静态方法至 clinit() 方法,随后从上往下开始执行

如果 实例化对象 则会调用方法对实例变量进行初始化,并执行对应的构造方法内的代码。(先系统默认初始化,再执行构造方法初始化)

解释

初始化完成后,执行某个类的方法时,会找到对应方法的字节码信息。然后交由解释器去将这些字节码信息解释成系统可以识别的指令。

字节码变成机器码的方式

JVM 对热点代码做编译,非热点代码直接进行解释。运行频繁的数据会被解释为热点代码。热点代码使用热点探测来检测是否为热点代码。热点探测一般两种方式:

HotSpot 使用的是计数器的方式进行探测,为每个方法准备了两类计数器:方法调用计数器和回边计数器。这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。即时编译器把热点方法的指令码保存起来,下次执行的时候就无需重复的进行解释,直接执行缓存的机器语言。

执行次数大于 100 万次(好像是 1w 次)的代码会被编译成热点代码

执行

操作系统把解释器解析出来的指令码,调用系统的硬件执行最终的程序指令。

小结

Java内存模型

Java 内存模型希望屏蔽各种硬件和操作系统的访问差异,保证了 Java 程序在各种平台下对内存的访问都能得到一致的效果。目的是解决多线程存在的原子性、可见性(缓存一致性)以及有序性的问题。Java 内存模型时一种规范,JVM 会实现这种规范。

主要内容概述

Java 内存模型的抽象结构

Java 内存模型定义了 Java 线程对内存数据进行交互的规范。

线程之间的共享变量存储在主内中,每个线程都有自己私有的本地内存,本地内存存储了该线程以读\写共享变量的副本。

本地内存是 Java 内存模型的抽象概念,并不是真实存在的。

Java 内存模型规定了:线程对变量的所有操作都必须在本地内存进行,不能直接读写主内存的变量。

Java 内存模型定义了 8 种操作来完成 变量如何从主内存到本地内存,以及变量如何从本地内存到主内存。分别是 read/load/use/assign/store/writer/lock/unlock 操作。

happen-before

happen-before 也是一套规则。目的是阐述“操作之间”的内存“可见性”。在 happen-before 规则下,我们写的代码只要前一个操作的结果对后续操作是可见的,是不会发生重排序的。

volatile

在 volatile 前后加了内存屏障,使得编译器和 CPU 无法进行重排序,并且写 volatile 变量对其他线程可见。

在汇编层面,是通过 lock 前缀指令来实现的(实现的内存屏障?),而不是各种 fence 指令(因为大部分平台都支持 lock 指令,而 fence 指令 是 x86 平台的)

// 内存屏障的实现
#ifdef AMD64
  __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
  __asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64

lock 指令能保证:禁止 CPU 和编译器的重排序(保证了有序性)、保证 CPU 写核心的指令可以立即生效且其他核心的缓存数据失效(保证了可见性)

第十二章 JVM内存结构

概述

JVM 的内存结构,往往指的就是 JVM 定义的运行时数据区域。

JVM 内存结构分为 5 块:方法区、堆、程序计数器、虚拟机栈、本地方法栈。

程序计数器

一块较小的内存空间,当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。程序计数器时程序控制流的指示器,分支、循环、跳转、异常处理、线程回复等基础功能都依赖于程序计数器完成。

可以理解为计算机组成原理中的程序计数器。指向下一条需要执行的指令。如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法,这个计数器值则应为空。

每个线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。程序计数器这块区域为线程私有,是线程安全的。

虚拟机栈

描述的是 Java 方法执行的线程内存模型。每一条 Java 虚拟机线程都有自己私有的 Java 虚拟机栈,这个栈与线程同时创建,每次方法调用都会创建一个 栈帧。

每个栈帧会包含几块内容:局部变量表、操作时栈、动态连接和返回地址

Java 虚拟机栈的作用与传统语言中的栈非常类似,用于存储局部变量与一些尚未算好的结果。

本地方法栈

与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到本地方法服务。HotSpot VM 直接把虚拟机栈和本地方法栈合二为一了。

方法区

Java 虚拟机规范中的解释

方法区是可供各个线程共享的运行时内存区域。存储了每一个类的结构信息,如:运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。虚拟机可以自行选在在不在方法区实现垃圾回收算法。

HotSpot 虚拟机

在 HotSpot 虚拟机,会常常提到永久代这个词。HotSpot 虚拟在 JDK8 前用永久代实现了方法区,而很多其他厂商的虚拟机其实是没有永久代的概念的。Java 虚拟机把方法区描述为堆的一个逻辑部分,但是它有一个别名叫作“非堆”,目的是为了与 Java 堆区分开来。

采用永久代实现方法区这种设计导致了 Java 应用更容易遇到内存溢出的问题(永久代有 -XX:MaxPermSize 的上限,即使不设置也有默认大小,而 J9 和 JRockit 只要没有触碰到进程可用内存的上限,如 32 位系统位 4GB,就不会出现问题。)在 JDK6 的时候 HotSpot 开发团队就有放弃永久代,逐步改为本地内存来实现方法区的计划了。

方法区主要用来存放已被虚拟机加载的“类相关信息”:包括类信息、常量池。

常量池:

但自从在 JDK7 以后,就已经把运行时常量池和静态常量池转移到了堆内存中进行存储。到了JDK 8,终于完全废弃了永久代的概念,改用与 JRockit、J9 一样在本地内存中实现的元空间(Meta-space)来代替,把 JDK7 中永久代还剩余的内容(主要是类型信息)全部移到元空间中。对于物理分区来说运行时常量池和静态常量池就属于堆。(逻辑分区和物理实际存储的位置是不一样的)

元空间存储不在虚拟机中,而是使用本地内存,JVM 不会再出现方法区的内存溢出,以往永久代经常因为内存不够用导致 OOM 异常。

小结

HotSpot VM:HotSpot VM JDK7 以前永久代实现的方法区。JDK7 以前常量池在永久代(方法区)中,永久代容易 OOM,JDK7 把常量池从永久代(方法区) 移动到了 JVM 堆中。 JDK8 开始,不在用永久代实现方法区了,而是用元空间实现方法区,永久代中剩余的内容(主要是类型信息)被移到了元空间。

Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。

堆涉及到了垃圾回收。以往的垃圾回收多数是基于“经典分代”来设计,需要新生代、老年代收集器搭配才能工作;HotSpot 里面也出现了不采用分代设计的新垃圾收集器。现在以传统的分代思想介绍下堆的划分。

堆被划分为新时代和老年代,新时代又被进一步划分为 Eden 和 Survivor 区,Surivivor 由 From Survivor 和 To Survivor 组成。

一般情况下 Eden:from:to = 8:1:1

小结

JVM 内存结构组成:JVM 内存结构又称为「运行时数据区域」。

主要由五部分组成:虚拟机栈、本地方法栈、程序计数器、方法区和堆。

其中方法区和堆是线程共享的。虚拟机栈、本地方法栈以及程序计数器是线程隔离的。