整理随笔

数据库

Coalesce函数

COALESCE是一个函数, (expression_1, expression_2, ...,expression_n)依次参考各参数表达式,遇到非null值即停止并返回该值。如果所有的表达式都是空值,最终将返回一个空值。使用COALESCE在于大部分包含空值的表达式最终将返回空值。

解决缩表问题

--查询是否锁表了
select oid from pg_class where relname='可能锁表了的表'
select pid from pg_locks where relation='上面查出的oid'
--如果查询到了结果,表示该表被锁 则需要释放锁定
select pg_cancel_backend(上面查到的pid)

POI

判断一个单元格是否是date类型

  • 可以使用HSSFDataUtil类的isCellDateFormatted方法

设置一列为下拉框

 DVConstraint lydlconstraint = DVConstraint.createExplicitListConstraint(dlarr);//入参就是下拉框的数据,数据类型为数组
            CellRangeAddressList lydlregions = new CellRangeAddressList(1, 200, 4, 4);
            HSSFDataValidation lydldataValidation = new HSSFDataValidation(lydlregions, lydlconstraint);
            lydldataValidation.setSuppressDropDownArrow(false);
            // 设置提示信息
            lydldataValidation.createPromptBox("操作提示", "请选择下拉选中的值");
            // 设置输入错误信息
            lydldataValidation.createErrorBox("错误提示", "请从下拉选中选择,不要随便输入");
            sheet.addValidationData(lydldataValidation);

MVC

  • ajax发送post请求,controller是无法进行重定向的。
  • web.xml文件中,context-param标签的值可通过getServletContext().getInitParameter(“name”)获取;Listener标签指定监视器,用于监听web应用中某些对象、信息的创建、销毁、增加、修改、删除等动作的发生,然后做出相应的相应处理。当范围对象的状态发生变化的时候,服务器自动调用监听器对象中的方法。

ContextLoaderListener和DispatcherServlet的contextConfigLocation区别

ContextLoaderListener指定的是Spring的配置,而DispatcherServlet配置的是SpringMVC的配置环境,用于初始化九大组件。如果在前者的配置文件中配置了@Controller的注解的bean,实际上SpringMVC是扫描不到的。

servlet的url-pattern匹配规则

当servlet容器接收到浏览器发起的一个url请求后,容器会用url减去当前应用的上下文路径,以剩余的字符串作为servlet映射,假如url是http://localhost:8080/appDemo/index.html,其应用上下文是appDemo,容器会将http://localhost:8080/appDemo去掉,用剩下的/index.html部分拿来做servlet的映射匹配。应用的上下文路径对应server.xml中的Context标签的path。

SpringMVC中的重定向和转发

要使用forward重定向就只需把redirect换成forward即可,特别的ModelAndView默认使用forward重定向方式。

j2ee 重定向和转发绝对路径和相对路径的区别

注:r-a.jsp位于webapp目录下;r-a是一个servlet。

1573173440577

tomcat

  • tomcat启动的时候优先配置已经解压出来的项目包(文件夹),其次是项目war包、

js

||

  • a()||b()

    当a函数返回true时,执行b函数,并返回b函数结果

  • a||b

    当a为true时,返回a,当a为false时,返回b的结果

优先级

  • “||”的优先级要高于“+”

es6的扩展运算符

  • 用于浅拷贝一个对象的属性或数组。

    let bar = {a: 1, b: 2};
    let baz = {...bar, ...{a:2, b: 4}}; // {a: 2, b: 4}

jdk提供的工具类

判断一个Object是否为空,为空的话抛出空指针

Objects.requireNonNull(T obj);

public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }

java向上取整,向下取整

向上取整用Math.ceil(double a)

向下取整用Math.floor(double a)



// 举例:

double a=35;
double b=20;
double c = a/b;
System.out.println("c===>"+c);   //1.75
System.out.println("c===>"+Math.ceil(c)); //2.0
System.out.println(Math.floor(c));  //1.0

isAssignableFrom()方法与instanceof关键字的区别

  • isAssignableFrom()方法是从类继承的角度去判断,instanceof关键字是从实例继承的角度去判断。
  • isAssignableFrom()方法是判断是否为某个类的父类,instanceof关键字是判断是否某个类的子类。

父类.class.isAssignableFrom(子类.class)
子类实例 instanceof 父类类型

isInterface()方法用于判断类是否是接口

循环嵌套

对于嵌套循环来说,一个break只能跳出一层

getBytes的字符集问题

默认采用操作系统的字符集,可使用getByte(“UTF-8”)来指定字符集

面向对象原则

  • 单一职责原则:一个类只做该做的事情
  • 开闭原则:软件实体应该对扩展开放,对修改关闭
  • 依赖倒转原则
  • 里氏替换原则:任何时候可以用子类对象替换掉父类对象
  • 接口隔离原则
  • 合成聚合复用原则:优先使用强关联关系而不是继承关系复用代码
  • 最少支持原则:不要给没必然联系的对象发消息

Ubuntu

  • 查看tomcat进程

    ps -ef |grep tomcat

  • 杀死进程

    kill -9 pid

  • 修改字符集编码

    系统支持编码的修改如下:1. 使用如下命令查看系统支持的字符集cat /usr/share/i18n/SUPPORTED说明:查看系统支持的字符集,你需要注意的是支持字符集的格式,如对中文会有以下一些显示(我的系统如此,我不知是否普遍)
    zh_CN.GB18030 GB18030
    zh_CN.GBK GBK
    zh_CN.UTF-8 UTF-8
    zh_CN GB2312
    2.sudo vim /var/lib/locales/supported.d/local说明:打开系统字符集配置文件,将支持的中文字符集添加进去,格式如1中得到所示。
    3.sudo locale-gen说明:更新。如果2中添加正确应该没有问题,如果出问题再次编辑2,后再3直至解决。如果正确此时应该可以使用VIM查看GBK编码的文件了,没有编码。但此时用Gedit还不可以,现在我们添加Gedit的字符集支持。
    1.gconf-editor打开Gnome配置编辑器
    2.app/gedit/preferences/encodings修改键值auto_detected添加入GBK,GB2312,GB18030如果操作成功现在Gedit也没乱码了。

1、打开
sudo vi /var/lib/locales/supported.d/local
在此文件中,添加一行
zh_CN.GBK GBK
2、 sudo locale-gen
会看到系统下载几个文件。
3、修改/etc/environment
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games"
LANG="zh_CN.UTF-8"
LANGUAGE="zh_CN:zh"
添加
LC_ALL="zh_CN.GBK"
4、接下来重启机器就行了

  • 解压

    tar –xvf file.tar //解压 tar包

    tar -xzvf file.tar.gz //解压tar.gz

    tar -xjvf file.tar.bz2 //解压 tar.bz2

    tar –xZvf file.tar.Z //解压tar.Z

    unrar e file.rar //解压rar

    unzip file.zip //解压zip

  • 压缩

    tar –cvf jpg.tar *.jpg //将目录里所有jpg文件打包成tar.jpg

    tar –czf jpg.tar.gz *.jpg //将目录里所有jpg文件打包成jpg.tar后,并且将其用gzip压缩,生成一 个gzip压缩过的包,命名为jpg.tar.gz

    tar –cjf jpg.tar.bz2 *.jpg //将目录里所有jpg文件打包成jpg.tar后,并且将其用bzip2压缩,生成一个bzip2压缩过的包,命名为jpg.tar.bz2

    tar –cZf jpg.tar.Z *.jpg //将目录里所有jpg文件打包成jpg.tar后,并且将其用compress压缩,生成一个umcompress压缩过的包,命名为jpg.tar.Z

    rar a jpg.rar *.jpg //rar格式的压缩,需要先下载rar for linux

    zip jpg.zip *.jpg //zip格式的压缩,需要先下载zip for linux

HttpClient

高并发下性能优化-http连接池

  1. http连接池不是万能的,过多的长连接会占用服务器资源,导致其他服务受阻
  2. http连接池只适用于请求是经常访问同一主机(或同一接口)情况下
  3. 并发数不高情况下资源利用效率低下
  • http连接池优点:
    • 复用http连接,省去3次握手与4次挥手
    • 自动管理tcp连接,不用人为释放/创建连接
  • 创建流程
  1. 创建PoolingHttpClientConnectionManager实例
  2. 给manager设置参数
  3. 给manager设置重试策略
  4. 给manager设置连接管理策略
  5. 开启监控线程,即使关闭被服务器单向断开的连接
  6. 构建httpClient实例
  7. 创建HttpPost/HttpGet实例,并设置参数
  8. 获取响应,做适当处理
  9. 将用完的连接放回连接池
package com.wss.httpclients.factory;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.micrometer.core.instrument.util.IOUtils;
import org.apache.http.*;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.rmi.UnknownHostException;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class HttpConnectionPoolUtil {
    private static final int CONNECT_TIMEOUT = Config.getHttpConnectTimeout();// 设置连接建立的超时时间为10s
    private static final int SOCKET_TIMEOUT = Config.getHttpSocketTimeout();//设置请求数据的超时时间
    private static final int MAX_CONN = Config.getHttpMaxPoolSize(); // 最大连接数
    private static final int Max_PRE_ROUTE = Config.getHttpMaxPoolSize();
    private static final int MAX_ROUTE = Config.getHttpMaxPoolSize();
    private static CloseableHttpClient httpClient; // 发送请求的客户端单例
    private static PoolingHttpClientConnectionManager manager; //连接池管理类
    private static ScheduledExecutorService monitorExecutor;

    private final static Object syncLock = new Object(); // 相当于线程锁,用于线程安全

    /**
     * 对http请求进行基本设置
     * @param httpRequestBase http请求
     */
    private static void setRequestConfig(HttpRequestBase httpRequestBase){
        RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(CONNECT_TIMEOUT)
                .setConnectTimeout(CONNECT_TIMEOUT)
                .setSocketTimeout(SOCKET_TIMEOUT).build();

        httpRequestBase.setConfig(requestConfig);
    }

    //真正获取httpclient
    public static CloseableHttpClient getHttpClient(String url){
        String hostName = url.split("/")[2];
        System.out.println(hostName);
        int port = 80;
        if (hostName.contains(":")){
            String[] args = hostName.split(":");
            hostName = args[0];
            port = Integer.parseInt(args[1]);
        }

        if (httpClient == null){
            //多线程下多个线程同时调用getHttpClient容易导致重复创建httpClient对象的问题,所以加上了同步锁
            synchronized (syncLock){
                if (httpClient == null){
                    httpClient = createHttpClient(hostName, port);
                    //开启监控线程,对异常和空闲线程进行关闭
                    monitorExecutor = Executors.newScheduledThreadPool(1);
                    monitorExecutor.scheduleAtFixedRate(new TimerTask() {
                        @Override
                        public void run() {
                            //关闭异常连接
                            manager.closeExpiredConnections();
                            //关闭5s空闲的连接
                            manager.closeIdleConnections(Config.getHttpIdelTimeout(), TimeUnit.MILLISECONDS);
                        }
                    }, Config.getHttpMonitorInterval(), Config.getHttpMonitorInterval(), TimeUnit.MILLISECONDS);//初始化延时,两次开始执行最小时间间隔
                }
            }
        }
        return httpClient;
    }

    /**
     * 根据host和port构建httpclient实例
     * @param host 要访问的域名
     * @param port 要访问的端口
     * @return
     */
    public static CloseableHttpClient createHttpClient(String host, int port){
        ConnectionSocketFactory plainSocketFactory = PlainConnectionSocketFactory.getSocketFactory();
        LayeredConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create().register("http", plainSocketFactory)
                .register("https", sslSocketFactory).build();

        manager = new PoolingHttpClientConnectionManager(registry);
        //设置连接参数
        manager.setMaxTotal(MAX_CONN); // 最大连接数
        manager.setDefaultMaxPerRoute(Max_PRE_ROUTE); // 路由最大连接数

        HttpHost httpHost = new HttpHost(host, port);
        manager.setMaxPerRoute(new HttpRoute(httpHost), MAX_ROUTE);

        //请求失败时,进行请求重试
        HttpRequestRetryHandler handler = new HttpRequestRetryHandler() {
            @Override
            public boolean retryRequest(IOException e, int i, HttpContext httpContext) {
                if (i > 3){
                    //重试超过3次,放弃请求
                    return false;
                }
                if (e instanceof NoHttpResponseException){
                    //服务器没有响应,可能是服务器断开了连接,应该重试
                    return true;
                }
                if (e instanceof SSLHandshakeException){
                    // SSL握手异常
                    return false;
                }
                if (e instanceof InterruptedIOException){
                    //超时
                    return false;
                }
                if (e instanceof UnknownHostException){
                    // 服务器不可达
                    return false;
                }
                if (e instanceof ConnectTimeoutException){
                    // 连接超时
                    return false;
                }
                if (e instanceof SSLException){
                    return false;
                }

                HttpClientContext context = HttpClientContext.adapt(httpContext);
                HttpRequest request = context.getRequest();
                if (!(request instanceof HttpEntityEnclosingRequest)){
                    //如果请求不是关闭连接的请求
                    return true;
                }
                return false;
            }
        };

        CloseableHttpClient client = HttpClients.custom().setConnectionManager(manager).setRetryHandler(handler).build();
        return client;
    }

    /**
     * 设置post请求的参数
     * @param httpPost
     * @param params
     */
    private static void setPostParams(HttpPost httpPost, Map<String, String> params){
        List<NameValuePair> nvps = new ArrayList<NameValuePair>();
        Set<String> keys = params.keySet();
        for (String key: keys){
            nvps.add(new BasicNameValuePair(key, params.get(key)));
        }
        try {
            httpPost.setEntity(new UrlEncodedFormEntity(nvps, "utf-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    public static JsonObject post(String url, Map<String, String> params){
        HttpPost httpPost = new HttpPost(url);
        setRequestConfig(httpPost);
        setPostParams(httpPost, params);
        CloseableHttpResponse response = null;
        InputStream in = null;
        JsonObject object = null;
        try {
            response = getHttpClient(url).execute(httpPost, HttpClientContext.create());
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                in = entity.getContent();
                String result = IOUtils.toString(in, Charset.forName("utf-8"));
                Gson gson = new Gson();
                object = gson.fromJson(result, JsonObject.class);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try{
                if (in != null) in.close();
                if (response != null) response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return object;
    }

    /**
     * 关闭连接池
     */
    public static void closeConnectionPool(){
        try {
            //httpclient4.5之后httpclients4.5.x版本直接调用ClosableHttpResponse.close()就能直接把连接放回连接池,
            // 而不是关###闭连接,以前的版本貌似要调用其他方法才能把连接放回连接池
            httpClient.close();
            manager.close();
            monitorExecutor.shutdown();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

浏览器相关

  • form中的button,在不指定type的情况下chrome默认为submit,而ie默认为button

HTML

CSS

  • display: inline-flex加在父元素上,同时父元素不指定宽高。能够使父元素根据子元素的宽度自适应

  • transition:all 0.3s;//动画
    -webkit-transform: rotate(90deg); //让一个元素旋转90°
  • css3动画

    https://c.runoob.com/codedemo/3391

  • 单位

    vw:视口的最大宽度,1vw=视口宽度的百分之一;

    vh:视口得最大高度,1vh=视口高度的百分之一;

  • 一个div里面的文字不指定高度直接居中。

    <div style="width: 50%;font-size: 14px;line-height: 40px;">1111111</div>
  • 一个父div根据子元素的高度自适应

    <div style="padding: 0px 5px 0px 5px;background-color: antiquewhite;">
              <div style="height: 60px;background-color: blue"></div>
          </div>
  • 父div不指定宽高,只指定padding,让子元素定位

    <div style="padding: 10px 10px; background-color: aqua;position: fixed;">1111111111111111111</div>
  • span加上padding之后可实现垂直居中的效果

JavaScript

JVM

img

img

java

JAVA中的K、T、V、E、?的含义

  • E ----集合中使用
  • T-----java类
  • K----键
  • V-----值
  • N-----数值类型
  • ?-----无限制通配符

使用commons-cli命令行工具来实现控制程序

JAVA 命令行参数解析,org.apache.commons.cli的使用

<dependency>
    <groupId>commons-cli</groupId>
    <artifactId>commons-cli</artifactId>
    <version>1.2</version>
</dependency>
public class t1 {
    public static void main(String[] args) throws Exception {
        final Options options = new Options();
        final Option option = new Option("f", true, "Configuration file path");
        options.addOption(option);
        
        final CommandLineParser parser = new PosixParser();
        CommandLine cmd = null;
        try {
            cmd = parser.parse(options, args);
        } catch (final ParseException e) {
            throw new Exception("parser command line error",e);
        }
        
        String configFilePath = null;
        if (cmd.hasOption("f")) {
            configFilePath = cmd.getOptionValue("f");
            System.out.println(configFilePath);
        }else{
            System.err.println("please input the configuration file path by -f option");
            System.exit(1);
        }
    }
}

1576484360023

注意:

用eclipse导出的时候导出的是runnable jar file

自定义注解

  • 判断一个类有没有加一个指定的注解

    通过class对象的isAnnotationPresent方法,注意java注解在编译成字节码的时候忽略注解,所以需要把注解的定义类加上@Retention(RetentionPolicy.runtime)才能被java虚拟机识别

利用System.getProperty获取系统变量

利用File.separator来获取文件分割符(和os有关)

RandomAccess接口实现快速访问

实现了这个接口的list使用for遍历会优于用迭代器遍历

通过classloader来获取Inputstream

this.getClass().getClassLoader().getResourceAsStream(path);

this.getClass().getResource("/").getPath();

将一个线程不安全的集合对象转换为安全的

List a = Collections.synchronizedList(new LinkedList<>());

java的移位运算比乘除要快

size >> 1 表示size/2 相当于 size/(2的n次方)

size<<1 表示size*2 相当于size* (2的n次方)

java反射实例

public class Service {
    public boolean login(String username,String password){
        if(username.equals(password)){
            return true;
        }
        return false;
    }

    public String forgetpaddword(){
        return "aaa123";
    }

}
public class Reflect {
    public static void main(String[] args) throws Exception {
        Class<?> serviceClass = Class.forName("col.Service");
        Service service = (Service)serviceClass.newInstance();

        Method login = serviceClass.getMethod("login", String.class, String.class);

        login.setAccessible(true);

        boolean invoke = (boolean) login.invoke(service,"zhouyongxi", "zhouyongxi");

        System.out.println(invoke);
    }
}

基于接口的jdk代理

public interface Animal {
    public void eat();
}
public class Cat implements Animal{
    @Override
    public void eat() {
        System.out.println("吃饱了!");
    }
}
public class JDKDynamicProxy implements InvocationHandler {

    public Object target;

    public JDKDynamicProxy(Object target){
        this.target = target;
    }

    public <T> T getProxy(){
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("主人给食!");
        /*此处必须传target,传入proxy会循环报错 */
        Object invoke = method.invoke(target,args);
        System.out.println("吃饱去散步!");
        return invoke;
    }
}
public class ProxyExample {
    public static void main(String[] args) throws Exception {
        Animal animal = new Cat();
        JDKDynamicProxy jdkDynamicProxy = new JDKDynamicProxy(animal);
        Animal proxy = jdkDynamicProxy.getProxy();
        proxy.eat();
    }
}
public class ProxyExample2 {
    public static void main(String[] args) {
        Animal animal = new Cat();

        JDKDynamicProxy jdkDynamicProxy = new JDKDynamicProxy(animal);
        Animal newProxyInstance = (Animal)Proxy.newProxyInstance(animal.getClass().getClassLoader(), animal.getClass().getInterfaces(), jdkDynamicProxy);
        newProxyInstance.eat();

    }
}

AspectJ实例

<?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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--启用Spring注解,指定使用了注解的包,有多个包时逗号分隔-->
    <context:component-scan base-package="AspectJ" />

    <!--启用AspectJ的注解-->
    <aop:aspectj-autoproxy />

    <bean id="animal" class="AspectJ.Animal" />
</beans>
public class Animal {
    public void speak(){
        //int i = 2 / 0;
        System.out.println("aaa123");
    }
}
@Aspect
@Component
public class PointCut {

    //切点用于给通知传参
    @Pointcut("execution(* AspectJ.Animal.*())")
    private void myPointCut(){}

    //环绕通知,对前后置通知的加强
    @Around("execution(* AspectJ.Animal.*(..))")
    public Object arount(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("1");
        Object proceed = joinPoint.proceed();
        System.out.println("2");
        return proceed;
    }

    //前置通知
    @Before("myPointCut()")
    public void before(){
        System.out.println("Before");
    }

    //最终通知
    @After("myPointCut()")
    public void after(){
        System.out.println("After");
    }

    //后置通知,与异常抛出通知只能运行于一个
    @AfterReturning("myPointCut()")
    public void afterReturning(){
        System.out.println("afterReturning");
    }
    //异常抛出通知,与后置通知只能运行于一个
    @AfterThrowing("myPointCut()")
    public void AfterThrowing(){
        System.out.println("AfterThrowing");
    }
}
public class Amain {
    public static void main(String[] args) {
        ApplicationContext acontext = new ClassPathXmlApplicationContext("spring-aspectJ.xml");
        Animal animal = (Animal)acontext.getBean("animal");
        animal.speak();
    }
}

基于Servlet的异步请求

​ 在servlet3.0中,我们可以从HttpServletRequest对象中获取一个AsyncContext对象,该对象构成余部请求上下文,request和response对象开源从AsyncContext对象中获取,AsyncContext可以从当前线程传给另一个线程,并在新线程中完成对请求处理并返回结果给客户端。

CopyOnWriteArrayList类和CopyOnWriteArraySet类

这2个类是线程安全的,内部使用了ReentrantLock可重入锁来实现线程安全锁。顾名思义,这个类设置值的时候,将原数组的元素拷贝(Array.copyOf浅拷贝)到一个新数组,再给新数组添加值,因此在大量数组增加的时候,会浪费性能。

Unsafe类

Unsafe类可以为我们提供高效线程安全来操作变量,直接和内存交互,并且可以直接开辟堆外内存。

  • 可以通过静态方法Unsafe.getUnsafe()就获取Unsafe实例。

  • //该方法可以获取一个对象的属性相对于该对象在内存当中的偏移量,这样我们就可以根据这个偏移量在对象内存当中找到这个属性。
    long objectOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("value"));
            Object value = unsafe.getObject(new User(), objectOffset);
  • //putOrderedInt 设置值 并且马上写入主存,该变量必须是volatile类型
    /** 
    * 设置 volatile 类型到int值
    *  
    * @param obj 需要更新的对象 
    * @param offset obj中整型field的偏移量 
    * @param expect 希望field中存在的值 
    */
    void sun.misc.Unsafe.putOrderedInt(Object obj, long offset, int expect)
  • //compareAndSwapObject 和上面方法功能一样,只不过是设置Object类型的变量
    public native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);

偏向锁

如果一个线程获得了锁,那么锁就进入了偏向模式。当线程再次获取锁的时候,无须再次同步操作。节省大量申请锁的时间,提高程序性能。但是对于锁竞争比较激烈的情况下,效果比较差。开启偏向锁的方法:

-XX:+UserBiasedLocking开启

克隆

浅克隆:会生成一个新的内存地址给一个引用变量

  • 通过实现CloneAble接口,并重写clone方法,方法内调用super.clone()实现浅克隆。

    java的clone方法的克隆只是浅拷贝,它实现的是在堆内存中复制一份目标对象的地址和数据,并将其指向新的引用变量(栈内存),而实际上新产生对象的的引用属性还是和原对象完全一致(hashcode一致),也就是说仅仅是拷贝了堆内存的地址。如果想要利用clone方法实现深拷贝,那么就得将对象的引用属性也一起调用clone实现拷贝。

    public class CloneTest implements Cloneable{
    
        private int age;
        private String name;
    
        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;
        }
    
        @Override
        public String toString() {
            return "CloneTest{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }
    
        @Override
        protected CloneTest clone() throws CloneNotSupportedException {
            return (CloneTest) super.clone();
        }
    
        public static void main(String[] args) throws CloneNotSupportedException {
            CloneTest c = new CloneTest();
            c.setName("1");
            c.setAge(1);
            CloneTest cloneTest = c.clone();
            //新克隆的对象内的引用变量的地址相同,说明是浅拷贝,如果想改成深拷贝,则需要调用对象内引用属性的clone方法。
            System.out.println(c.name == cloneTest.name);//true
            //说明调用clone方法生成的引用对象的地址是新的,
            System.out.println(c == cloneTest);//false
            System.out.println(cloneTest.getName());
            System.out.println(cloneTest.getAge());
    
            cloneTest.setName("2");
            System.out.println(cloneTest.toString());
            System.out.println(c.toString());
    
        }
    
    }
  • 利用Serializable实现深克隆,在其中利用io流的方式将这个对象写道io流中,然后再从io流中读取。

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
              ObjectOutputStream  oos = new ObjectOutputStream(bos);
              oos.writeObject(this);
              //将当前这个对象写到一个输出流当中,,因为这个对象的类实现了Serializable这个接口,所以在这个类中
              //有一个引用,这个引用如果实现了序列化,那么这个也会写到这个输出流当中
    
              ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
              ObjectInputStream ois = new ObjectInputStream(bis);
              return ois.readObject();
  • BeanUtils.copyProperties(target,obj)将obj的同名属性赋值给target的同名属性。

Iterator和ListIterator区别

  1. ListIterator有add方法,可以向List中添加对象,而Iterator不能。
  2. 二者都有hasNext()和next(),但是ListIterator还有hasPrevious方法和previous方法。
  3. ListIterator有nextIndex()和previousIndex(),而iterator没有。
  4. 都可删除对象,但是ListIterator可以修改,而Iterator不能。
public class ListInerator {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");

        ListIterator<String> listIterator = list.listIterator();

        while (listIterator.hasNext()){
            System.out.println(listIterator.next());
        }

        ListIterator<String> listIterator1 = list.listIterator(1);
        while (listIterator1.hasNext()){
            System.out.println(listIterator1.next());
        }
    }
}

LinkedList和ArrayList区别

LinkedList内部采用双向链表的原理来进行,而ArrayList内部采用数组来进行维护,LinkedList内部保存了头节点和尾节点。

遍历Map

for (Map.Entry<? extends K, ? extends V> e : t.entrySet())

volatile保证了可见性和有序性(防止指令重排)

1578547913704

  • volatile并不能保证原子性。

  • volatile声明的变量,在写入该变量时,同时也会将该变量写入到内存,在读取改变量时,也会直接从内存中读取。可见性的保证遵顼一下原则:

    • 若线程A写入一个volatile变量,线程B随后读取了同样的volatile变量,则线程A在写入volatile变量之前的所有的可见的变量值,在线程B读取volatile变量后也是可见的
    • 若线程A读取一个volatile变量,那么线程A中所有可见的变量也同样从主存重新读取。
    • 1578547360116
  • 可以将volatile的读写理解为一个触发刷新的操作,写入volatile时,线程中所有的变量也都会触发写入内存,而读取volatile变量时,也会触发所有变量从内存中读取。应当尽量把volatile写操作放到最后,而将volatile的读操作写道最前,这样就能连带其他变量进行刷新。

  • 由于指令重排机制的存在,volatile的可见性可能会存在bug:

    1578547861432

    因此volatile在保证可见性的同时,也会提供happen-before的保证。

  • happen-before:

    • 可以把位于写入volatile指令之后的其他指令移到写入volatile之前,而不能把位于写入volatile指令之前的其他指令移到写入volatile之后。(即可后置前,不可前置后;可插队不可排队)
    • 可以把位于读取volatile指令之前的其他指令移到读取volatile之后,而不能把位于读取volatile变量之后的指令移到读取volatile之前。(即可前置后,不可后置前;可排队不可插队)
  • 若2个线程同时修改某个变量,仅使用volatile是不够的,还需要使用synchronized配合。

    1578548667993

JMM

1578548749126

在多核cpu中每个处理器都有高速缓存(L1、L2、L3),而主内存只有一个。

那么如何保证数据的一致性呢?

  1. 总线的方式,这样会降低cpu的吞吐量

  2. 使用缓存一致性,如MESI协议、MSI协议等。

JVM

1578549183477

虚拟机栈:每个方法都有自己的一块区域,存储局部变量。

本地方法栈:主要存储native方法。

堆内存:用于存放对象实例,被所有线程共享。

方法区:存储类信息、常量、静态变量

程序计数器:每个线程有自己的程序计数器。

jvm有很多,Hotspot、JRockit、J9等。jdk8对hotspot和jrockit进行了结合。

JVM主要由3个子系统组成。类加载子系统、运行时数据区、执行引擎。

方法的调用就是栈帧入栈出栈的过程。

synchronized

  • synchronized的特性有:原子性、可见性、有序性

  • synchronized的语义底层时通过monitor的对象来完成,起始wait/notify也依赖于monitor对象。

  • 对象锁: 利用monitorenter和monitorexit命令

    monitorenter:每个对象都有一个监视器锁(monitor),当monitor被占用时,就会处于锁定状态,线程2执行monitorenter指令时尝试获取monitor的所有权。若monitor的进入数为0时,则线程进入monitor,并且将monitor设为1,该线程就是锁的拥有者;若线程已经占有monitor,需要重新进入,只需对monitor+1;若其他线程已经占有monitor,则进入阻塞状态,直到monitor变为0,再次尝试获取monitor所有权。

    monitorexit:执行monitorexit命令的只能是拥有object的monitor的所有者。指令执行时,monitor的数量-1,直到线程减为0,线程退出monitor,不再是这个monitor的拥有者。

  • 方法锁:除了利用monitorenter和monitorexit之外还利用了方法的ACC_SYNCHRONIZED

    当方法被调用时,调用指令会检查方法的ACC_SYNCHRONIZED 访问标志是否被设置,若设置了,线程会去先获取monitor,获取成功后才回去执行方法体,方法执行完,再释放monitor。在方法执行期间任何线程无法获取同一个monitor对象。

都是使用了OS底层的mutex来实现。

img

对象在内存的布局

  • 对象在内存中布局有对象头、实例数据、填充数据

  • synchronized使用的锁就是存储在对象头里面的。对象头有MarkWord和ClassPointer。其中ClassPoint存储了对象对应的类元数据的指针,虚拟机通过这个指针来判断该对象时哪个类的实例。MarkWord主要存储对象自身运行时数据,是实现轻量级锁和偏向锁的关键。

  • 在32位OS中对象头占8字节,64位则是16字节(不考虑压缩指针,开启压缩指针后12字节),

  • 锁状态分为无锁、偏向锁、轻量锁、重量锁

    img

StampedLock的设计思路

  • StampedLock有三种锁:乐观读锁、悲观读锁、写锁。

    乐观读锁:读锁和写锁不是互斥的,因为在日常的系统中读操作存在很多,而相对写操作会少很多,这就会导致写锁饥饿的现象。StampedLock的乐观锁只是在读操作的过程中发现有写操作,就会在去读一次。

    悲观读锁:读写互斥

四种引用类型

  • 强引用

    平时引用的最多,User user = new User();user便是强引用,若user=null,则堆区中的User对象会被GC回收

  • 软引用

    通过SoftReference来定义一个软引用。

    若一个对象只有软引用,内粗足够时,gc不回收,若内存空间不足,就会被回收

  • 弱引用

    通过WeakReference来定义一个弱引用

    若对象只有弱引用,那么GC回收时,不管内存是否足够,都会回收此对象

  • 虚引用

    通过PhantomReference来定义虚引用

    主要用来跟踪对象被垃圾回收的活动。

类加载过程

  • 加载、验证、准备、解析、初始化、使用、卸载。

  • 加载:将java文件转换成class文件,也可以是网络传输的io流,也可以是zip包。

    预加载:java虚拟机启动时,会先加载java_home的lib目录下的rt.class下的class,是java运行时常用的一些类,比如util.和lang.

    运行时加载:首先java虚拟机会先去内存中查找这个class文件,没有被加载会根据这个类的全限定名去加载。

    过程:

    1. 根据类的全限定名获取类的二进制字节流
    2. 将字节流的静态存储结构转换成方法区的运行数据结构
    3. 在内存中生成Class对象,作为方法区中这个类的各种数据的入口。(一般这个class对象存储到堆中,但是hotspot特殊,存储在方法区。)
  • 验证:为了保护JVM

    对class的字节流进行验证,确保class文件符合虚拟机的要求,并且不危害虚拟机自身的安全

    1. 文件格式的校验
    2. 元数据校验
    3. 字节码校验
    4. 符号引用校验
  • 准备:主要是给static变量分配内存(方法区),并设置初值。同时给常量设置值。注意public static int a = 5在准备阶段只是给a赋予初值0

  • 解析:引用关系转换成地址

  • 初始化:调用类的构造方法,不是构造函数。生成规则:static变量的赋值操作+static代码块

  • 使用&卸载

类加载器

  • 分类:启动加载器、扩展加载器、应用程序加载器、用户自定义加载器
  • 1578556632650

双亲委派

  • 每个类加载器收到类加载指令时,自己不会主动去加载,而是交给父加载器,如果父加载器在自己搜索范围内找不到指定的类时,才会自己去加载。
  • 好处:如果有人定义了一个String类,包名类名和jdk时一样的,根据双亲委派,最终交给启动类加载器,启动类加载判断已经加载过了,所以自定义的String类就不会被加载。

GC

https://www.jianshu.com/p/76959115d486

https://blog.csdn.net/lwang_IT/article/details/78650168

我们知道程序计数器、本地方法栈、虚拟机栈随线程而生,随线程而灭。java堆是GC回收的“重点区域”,堆内存放所有对象的实例,gc进行回收前,需要判断哪些对象存活。

  • 为了进行高效的回收,jvm将堆分为3个区域:
    1. 新生代
    2. 老年代
    3. 永久代(1.8已废弃,改为元空间,就不在堆中了)

JVM提供了两个参数来控制JVM堆的大小:-XX:InitialHeapSize(-Xms)-XX:MaxHeapSize(-Xmx)。JVM会根据应用程序使用内存的情况,动态扩展堆内存的大小,上图中的Virtual表示的区域,表示的就是可以扩展的内存空间。

finalize方法

当调用System.gc()方法时,会调用Object.finalize()。

判断对象是否存活算法(什么样的对象需要回收)

https://www.cnblogs.com/duke2016/p/6013519.html

  • 引用计数算法(早期):

简单效率高,给对象添加一个引用计数器,每当对象被引用1次,引用计数就+1,当被引用0次时,代表对象已经不被引用。

缺点:当出现循环引用时,引用计数失效。

  • 可达性分析算法(普遍、商用):

通过一个叫“GC Roots”的对象为起点,搜索所经过的路径为引用链,当一个对象没有任何引用跟他的连接则证明对象是不可用的。

img

  • 可作为GC Roots的对象有4种:
    • 虚拟机栈引用的对象,即平时所指的java对象,存放在堆中
    • 方法区中类静态属性引用的对象,一般指static修饰引用的对象,加载类的时候被加载到内存中
    • 方法区中常量引用的对象
    • native引用的对象

要真正宣告对象死亡需经过两个过程:

  1. 可达性分析后没有发现引用链,那它将被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行fanalize方法,当对象没有覆盖finalize()方法,或者finalize方法已被虚拟机调用过,虚拟机将这2种情况都是为没必要执行
  2. 若这个对象被判定为有必要执行finalize方法,那这个对象就会被放置在一个F-Queuc的队列之中,并在稍后由一个虚拟机自动建立、低优先级的一个线程区执行它。

当我们真正回收一个对象的时候,对象必须经过2次回收过程。当确认没有与GC Roots相关联的链时,这个对象会被第一次标记,并判断对象的finalize方法是否需要执行(在java的Object对象中,有一个finalize()方法,我们创建的对象可以选择是否需要重写),若对象的类没有重写finalize方法或者finalize已经被执行过了,那么就不需再次执行该方法了。

若这个对象的finalize方法需要被执行,那么这个对象就会被加入F-Queue的队列,这个队列是jvm自动创建的低优先级Finalizer线程去消费,去执行对象的finalize方法(虚拟机只会去触发这个方法,不会等待这个方法调用返回,这样做的目的是如果方法执行过程中出现阻塞,性能可能出现问题或者死循环,Finalizer线程仍然不受影响的去消费队列,不影响整个过程)。

稍后GC会对F-Queue队列中的对象进行第二次标记,若在这次标记发生时,队列中的对象确实没有存活(和GC Roots没有链),那么这个对象就确定下来需要被回收了。当然第二次标记时,突然和GC Roots有了引用链,那么这个对象就相当于救活了自己,在第二次标记时,这个对象就被移除待回收对象的集合了。所以通过2次标记的机制,可以通过finalize方法让Gc Roots和对象重新连接,那么这个对象就可以被救活了。

public class FinalizerTest {
    private static Object HOOK_REF;

    public static void main(String ...args) throws Exception {
        HOOK_REF = new FinalizerTest();

        // 将null赋值给HOOK_REF,使得原先创建的对象变成可回收的对象
        HOOK_REF = null;
        System.gc();
        Thread.sleep(1000);

        if (HOOK_REF != null) {
            System.out.println("first gc, object is alive");
        } else {
            System.out.println("first gc, object is dead");
        }

        // 如果对象存活了,再次执行一次上面的代码
        HOOK_REF = null;
        System.gc();
        if (HOOK_REF != null) {
            System.out.println("second gc, object is alive");
        } else {
            System.out.println("second gc, object is dead");
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        // 在这里将this赋值给静态变量,使对象可以重新和GC Roots对象创建引用链
        HOOK_REF = this;
        System.out.println("execute in finalize()");
    }
}

#output:

  execute in finalize()
  first gc, object is alive
  second gc, object is dead

可以看到,第一次执行System.gc()的时候,通过在方法finalize()中将this指针指向HOOK_REF来重建引用链接,使得本应该被回收的对象重新复活了。而对比同样的第二段代码,没有成功拯救的原因是: finalize()方法只会被执行一次 ,所以当第二次将HOOK_REF赋值为null,释放对对象的引用的时候,由于finalize()方法已经被执行过一次了,所以没法再通过finalize()方法中的代码来拯救对象了,导致对象被回收。

垃圾回收算法(怎么回收)

https://www.cnblogs.com/duke2016/p/6013519.html

  1. 标记/清除算法【最基础】
  2. 复制算法(标记/复制算法)
  3. 标记/整理算法

jvm采用“分代收集算法”对不同区域采用不同的回收算法。

新生代采用复制算法,老年代采用标记/清除算法和标记/整理算法

  • 标记清除算法,是最基础的算法,分为2个阶段:标记、清除阶段。在标记阶段判断哪些对象需要被回收,在清除阶段清除这些对象。算法本身存在一些缺陷:首先标记和清除阶段效率并不高,其次直接将标记的对象清除会产生内存碎片,而大量不连续的碎片会影响后续连续内存空间的申请。申请连续的内存空间失败会再次触发垃圾回收机制,影响性能。

  • 复制算法,顾名思义,跟复制操作有关,该算法将内存区域划分为大小相同的2块区域,每次使用都是只是用其中一块,另一块闲置备用。当进行垃圾回收的时候,会将当前用的那块内存上的存活的对象直接复制到另一块闲置的空闲内存上,然后将之前使用的那块内存上的对象全部清除掉。这样处理的好处是可以有效处理标记-清除算法碰到的内存碎片的问题,实现简单、效率高。但是比较耗费内存,实际上另一半内存是在浪费。

  • 标记整理算法,思路是先进行垃圾内存的标记,和标记-清除算法的标记阶段一致,当垃圾对象被回收之后,为了避免内存碎片的问题,标记整理算法会对内存进行整理,将所有存活的对象移到另一端,将一段的空闲内存整理出来,这样就可以得到连续的内存了。这样做可以很方便的申请内存,只要移动内存指针就可以划分区域用以存储新的对象,可以在不浪费内存的情况下高效的分配内存,避免了复制算法中浪费一部分内存的情况

  • 分代收集算法。在现代虚拟机实现中,会将整个内存划分为多个区域,用年龄代表对象的存活时间。并将不同年龄段的对象存放在不同内存区域。这就是老年代和新生代的来源。

    年轻代是指刚出生的对象,老年代是指在内存中存活了一段时间的对象。将对象分代的目的是针对不同种类的对象的垃圾回收采用不同的算法。

    对于年轻代,其中大部分对象的存活时间较短,很多对象撑不过下一次垃圾回收,所以在年轻代中,一般采用”复制算法“实现垃圾回收器。

    1578623415416

    在上图中,我们可以看到"Young Generation"标记的这块区域就是"年轻代"。在年轻代中,还细分了三块区域,分别是:"eden"、"S0"和"S1",其中"eden"是新对象出生的地方,而"S0"和"S1"就是我们在复制算法中说到了那两块相等的内存区域,称为存活区(Survivor Space)

    年轻代区域中,如果按照复制算法中的1:1的方式平分新生代的话,会浪费内存空间,所以将年轻代划分为eden区、S0、S1,每次只是用eden区和其中一块Servivor区,当垃圾回收时会将eden区和已占用的serviror区复制到另一块空闲的serviror区,然后将eden区清空,现在年轻代就可以使用eden区和另一块serviror区,S0和S1区是交替使用的。

    Hotspot区默认Eden区和其中一块servivor区的比例是8:1,通过JVM参数"-XX:SurvivorRatio"控制这个比值。SurvivorRatio的值是一个整数,表示Eden区域是一块Survivor区域的大小的几倍,所以,如果SurvivorRatio的值是8,那么Eden区和其中Survivor区的占比就是8:1,那么总的年轻代的大小就是(Eden + S0 + S1) = (8 + 1 + 1) = 10,所以年轻代每次可以使用的内存空间就是(Eden + S0) = (8 + 1) = 9,占了整个年轻代的 9 / 10 = 90%,而每次只浪费了10%的内存空间用于复制。

    并不是留出越少的空间用于复制操作越好,如果在进行垃圾收集的时候,出现大部分对象都存活的情况,那么空闲的那块很小的Survivor区域将不能存放这些存活的对象。当Survivor空间不够用的时候,如果满足条件,可以通过分配担保机制,向老年代申请内存以存放这些存活的对象。

    对于老年代的对象,由于在这块区域中的对象和年轻代的对象相比较而言存活时间都很长,在这块区域中,一般通过"标记-清理算法"和"标记-整理算法"来实现垃圾收集机制。上图中的Tenured区域就是老年代所在的区域。而最后那块Permanent区域,称之为永久代,在这块区域中,主要是存放类对象的信息、常量等信息,这个区域也称为方法区。在Java 8中,移除了永久区,使用元空间(metaspace)代替了。

    JVM参数NewSizeMaxNewSize分别可以控制年轻代的初始大小和最大的大小。通过设置这两个参数,可以手动控制JVM中年轻代的大小,比如-XX:NewSize=100m将年轻代的大小初始化为100m。除了通过固定值来控制年轻代的大小,还可以通过参数NewRatio来按比例控制年轻代的大小,NewRatio的值表示年轻代和老年代的比值,比如:-XX:NewRatio=6 就表示,年轻代:老年代 = 1:6,所以年轻代占据了堆内存的1/7,而老年代则占据了6/7。

    对于年轻代中的Eden区和Survivor区的大小分配,JVM提供了SurvivorRatio这个参数来控制两块区域的大小。和NewRatio一样,这个值也是用于控制Eden区和两个Survivor区的大小比例的,比如:-XX:SurvivorRatio = 8,那么表示Eden : 一个Survivor = 8 : 1,那么Eden区就占据了年轻代中的8 / 10,而两个Survivor区分别占据了 1 / 10。

    我们通常将年轻的的GC称为Minor GC。

    当对象经历了多次MinorGC后仍存活,当达到一定年龄后(经历一次MinorGC后,加1),仍存活的对象就会被移入到老年代中。

    我们通常将老年代的GC成为MajorGC、

GC收集器

https://www.cnblogs.com/duke2016/p/6250766.html

jvm对堆内存进行分代划分,是为了结合不同对象的状态采用合适的垃圾回收算法。

JVM实现了多种GC收集器,

  • 按单线程多线程来划分,分为:

    • 单线程:Serial和SerialOld,分别用于年轻代和老年代
    • 多线程:Parallel Scavenge和ParallelOld收集器,分别用于新生代和年老代
  • 按照新生代和老年代划分,分为:

    • 新生代:Serial收集器、PartNew收集器和Parallel Scavenge收集器
    • 老年代:Serial Old收集器、ParallelOld收集器和CMS收集器
  • Serial收集器

    是一个年轻代垃圾收集器,使用标记-复制算法,单线程的。只是用一个线程进行垃圾回收工作,进行垃圾回收时,所有工作线程均停止工作,等垃圾回收线程完成之后,其他线程才继续工作。因此系统会出现STW(stop the world)现象,应用程序会出现停顿的现象,若停顿时间过长,会导致系统相应迟钝。

  • PartNew收集器

    PartNew是Serial的多线程版本,使用标记-复制算法,由于使用了多线程其性能要比Serial具有更好的性能,并且它可以和老年代的CMS的老年代收集器一起搭配使用。

    作为多线程收集器,它运行在单CPU机器时,不能发挥多核优势,会出现频繁的上下文切换,导致额外的开销,所以在单CPU的机器上PartNEW的性能不一定好于Serial。

    PartNew收集器默认开启的垃圾手机线程是和当前机器的CPU数量相同,为了控制GC收集线程的数量,可以通过参数-XX:ParallelGCThreads来控制垃圾收集线程的数量。

  • Parallel Scavenge收集器

    他是一个年轻代的处理器,利用标记-复制算法,与PartNew一样,他也是多线程的收集器,与其他收集器不同的是,像PartNew和CMS关注点主要集中于如何缩短垃圾回收时间,而Parallel Scavenge关注的是系统的吞吐量,这里的吞吐量指的是CPU用于运行应用程序的时间和CPU总时间的比值。

  • Serial Old收集器

    是Serial的老年代版本,是标记-整理算法的实践

  • Parallel Old收集器

    Parallel Old收集器是parallel Scavenge的老年代版本,与Parallel Scavenge搭配使用,可实现对堆内存的吞吐量的提高。

  • CMS收集器

    是老年代回收器中最优秀的垃圾回收器,使用标记-清除算法。CMS是一款获取最短停顿时间为目标的收集器.

    1578633999105

cms工作过程分为4个阶段:

  • 初始标记阶段
  • 并发标记阶段
  • 重新标记阶段
  • 并发清除阶段

由图看一看出初始标记阶段和重新标记阶段是只有GC线程参与,用户线程会被停止,所以会产生STW。

  • 初始标记阶段是标记GC Roots可以直接关联的对象,速度快。
  • 并发标记i阶段会从GC Roots出发,标记出所有可达的对象,这个过程会耗费大量时间。但是这个过程不会中断用户线程的进行。
  • 重新标记阶段是对并发标记期间因用户程序运行而导致标记浮动的那部分记录进行修正,耗时比初始阶段耗时较长,单远小于并发标记阶段
  • 并发清理阶段。和并发标记阶段类似,不会中断用户线程,不会对系统运行产生影响。

由于并发标记和并发清理是和应用程序一起执行的,而初始标记和重新标记相对耗时较短,因此CMS收集器是一款并发低停顿收集器

CMS不足之处:

  • CMS收集器对CPU资源非常敏感,对于并发实现的收集器而言,虽然可以利用多核的又是提高垃圾收集的效率,但收集器会占用一部分线程,线程会占用CPU资源所以会影响系统的运行,降低吞吐量。
  • CMS收集器在垃圾回收的过程中会产生浮动垃圾,由于无法处理浮动垃圾,可能会出现Concurrent Mode Failure而导致触发一次Full GC,所谓的浮动垃圾就是并发标记阶段和用户线程一起运行,若在垃圾清理过程中,用户线程产生了垃圾对象,由于过了标记阶段,这些垃圾对象就变成了浮动垃圾,CMS无法再当前阶段处理这些垃圾。出于这个原因,CMS收集器不能像其他收集器那样等到老年代内存区域完全填满后进行垃圾回收,需要预留一部分空间来保存这些浮动垃圾。若CMS运行期间没有空间存储这些浮动垃圾,就会导致Concurrent Mode Failure失败,虚拟机将启动后备方案,临时启动Serial Old来重新进行垃圾收集,这会导致垃圾收集时间过长,特别老年代内存过大。
  • 使用标记-清理算法,会产生很多内存碎片,过多的内存碎片会导致对象的分配,会导致即使老年代空间好友很多但是不得不提前触发垃圾回收,。为了解决这个问题,CMS收集器提供了一个"-XX:+UseCMSCompactAtFullCollection"参数,用于CMS收集器在必要的时候对内存碎片进行压缩整理。由于内存碎片的处理不是并发的,会导致停顿时间过长。虚拟机还提供了一个"-XX:CMSFullGCsBeforeCompaction"参数,来控制进行过多少次不压缩的Full GC以后,进行一次带压缩的Full GC,默认值是0,表示每次在进行Full GC前都进行碎片整理。

synchronized原理(偏向锁、轻量级锁、重量级锁)

1579069705405

markword中涉及锁的字段有该对象是不是偏向锁(0/1)、锁标志位(01/10)。

锁级别的升级:无锁->偏向锁(01)->轻量锁(00)->重量级锁(10)

  • 自旋锁(CAS)

    自旋锁让线程等待一段时间,而不是立即挂起。

  • 偏向锁

    当线程访问同步代码块兵获取锁时,会在对象头和栈帧的锁记录中存储所偏向的线程id,以后该线程再次进入或退出同步块时不需要CAS操作来加锁或解锁,只需简单测试对象头中的MarkWorld中是否存储指向当前线程的偏向锁。若测试成功,表示线程已经获取了锁。如果测试失败,则需要再测试一下MarkWorld中偏向锁的标识是否设置为1;若没有设置,则使用CAS竞争锁;若设置失败了,则尝试使用CAS将对象头的偏向锁指向当前线程。

    偏向锁

    当线程执行到临界区时,将线程id插入到Markword中,同时修改锁标志位和是不是偏向锁。

    1579069782200

  • 轻量级锁

  • 重量级锁

Spring

ExceptionHandler全局异常处理

@ExceptionHandler(Throwable.class)
    public ResponseEntity handleException(Throwable e){
        // 打印堆栈信息
        log.error(ThrowableUtil.getStackTrace(e));
        return buildResponseEntity(ApiError.error(e.getMessage()));
    }

IOC过程及原理

  • 读取配置文件
  • 根据配置文件中的配置找到对应类的配置,并实例化
  • 调用实例化后的实例

需要2个类,一个是读取配置文件,二是根据配置来反射实例化。

DefaultListableBeanFactory是整个Bean加载的核心。

Bean生命周期

  • xml:

    init-method、destroy-method

  • 非xml:

    实现InitializingBean类并实现afterPropertiesSet();实现DisposableBean接口并实现其distroy方法。注意DisposableBean接口的destroy方法是需要调用applicationcontext.registerShutdownHook()来调用的

  • 注解:

    @PostConstruct(初始化)

    @PreDetroy(销毁)

  • beanfactorypostprocessor主要是在容器中中的任意一个bean被new出来之前执行。应用场景:ConfgiurationClassPostProcessor的postProcessorBeanFactory方法针对配置类加上cglib代理。

  • beandefinationregistryposrprocessor。是一个子类,在beanfactorypostprocessor的之前执行。因为源码当中先遍历的是它。应用场景:ConfgiurationClassPostProcessor有一个回调方法,会去扫描包,解析beanmethod

  • importSelector。通过这个方法selectImports返回一个类名(全名),并将其转换成bd,动态添加bd,这个bd是死的。也可以动态扫描

  • importBeandefinationregistrar。实现这个接口的方法,会把beandefinationregistry传给我们,因此我们可以动态改变bd。这也是和importSelector的区别:importBeandefinationregistrar接口的方法可以把beandefinationregistry传给我们。

  • DeferedImportSelector延迟加载。

depends-on(XML,bean标签的属性)

一个类依赖了另一个类,初始化的时候要先初始化依赖类。

lazy-init(xml,bean标签的属性)和@Lazy

用的时候才去实例化,容器启动的时候并不去实例化。

@ComponentScan注解

用于指定扫描执行哪些包下面的注解。

  • excludeFilters属性用于排除哪些包下面的注解。这个属性接收元素为Filter类的数组,Filter注解里面由各种排除的表达式。例如

    @CompnentScan(value="com.wss",excludeFilters={@Filter(type=FilterType.REGEX,pattern="com.wss.test")})

@Primary

@Qualify

@Configuration

设置Profile

beans标签和@Profile注解都可以指定bean的profile

  • 通过applicationContext.getEnvironment().setActiveProfiles("dev")来指定生效的profile

    ApplicationContext的register方法

  • 接受一个class类,用于注册这个类,在调用方法之后需要执行applicationcontext.refresh()(5.0版本已不再需要)

AOP

在ioc中是单例的

  • 连接点(JoinPoint)

    永远是一个方法,目标对象中的一个方法

  • 切点(PointCut)

    连接点的集合

  • 目标对象(target)

    原始对象

  • 织入(weaving)

    将增强的操作附加到连接点

  • 通知(advice)

    • before前置通知

    • after后置通知

    • afterreturning返回通知,在连接点方法正常返回后调用,要求连接点方法没有返回异常

    • afterthrowing异常通知,当连接点方法异常时返回

    • around环绕通知,将覆盖原有方法,允许你通过反射调用原有方法。

    • @DeclareParents 扩展,让目标去实现目标对象

      @DclareParents(value="",defaultImpl="A")

      public static B b;

      这样从ioc容器中获取b时,b可以调用A里面的方法。

  • JoinPoint

    • getTarget(获取目标对象)

    • getThis(获取代理对象)
    • getArgs(连接点参数)

  • ProceedingJoinPoint,继承自JoinPoint

    • proceed(是aop代理链的方法)相当于把连接点执行了一下。它可以接收一个参数作为连接点的参数
  • 切面(Aspect)

    连接点+切点+通知

  • 代理对象(AOP proxy)

    包含了原有对象和增强的操作的代理对象

  • Spring的动态代理默认使用JDK动态代理。

AspectJ

  • 开启Spring AspectJ的方法

    • 注解-@EnableAspectjAutoProxy和@Configuration一起使用
    • xml-
  • @AspectJ

    Spring Aspectj默认支持的是Aspectj是单例的,可以采用下列方法改成多例的

    @Aspectj(perthis(this(XXX)))

    perthis:每个切入点表达式匹配的连接点对应的Aop对象都会创建一个新的切面

  • @PointCut

1578826651269

execution

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
            throws-pattern?)
这里问号表示当前项可有可没有
modifiers-pattern-方法的可见性,如public,private
ret-type-pattern-方法的返回值类型,如int
declaring-type-pattern-方法所在类的全路径名。如com.spring.Aspect
name-pattern-方法名
param-pattern-方法参数类型
throws-pattern-方法异常类型
  • @EnableAspectjAutoProxy中有一个属性proxyTargetClass,其值如果是false,就代表不使用动态代理,如果是true,就使用jdk动态代理。注意如果使用jdk动态代理就代表了从ioc容器里面获取该bean的时候,是代理对象而不是target对象

Redis

支持的数据结构

字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(zset)

内存淘汰机制

若redis的内存已满,后续的写操作如何处理?

  • noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外)
  • allkeys-lru:从所有key中使用LRU算法进行淘汰
  • volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
  • allkeys-random:从所有key中随机淘汰数据
  • volatile-random:从设置了过期时间的key中随机淘汰
  • volatile-ttl:在设置了过期时间的key中,根据key的过期时间进行淘汰,越早过期的越优先被淘汰
  • volatile-lfu:根据key的过期时间,根据LFU算法进行淘汰
  • allkeys-lfu:所有的key使用LFU算法淘汰数据

LRU算法:

最近最少使用,是一种缓存置换算法。

LFU算法:

根据key的最近被访问频率进行淘汰,很少被访问的优先淘汰,被访问多的被留下来

redis集群环境会出现写操作丢失的情况。

Redis集群环境的复制时异步复制

管道技术

时一种基于客户端-服务端模型,以及请求/响应模型的TCP服务。Redis管道技术可以在服务端未响应时,可护短可以继续向服务端发送请求,并最终一次性读取所有服务端响应。

一次性向redis服务提交,一次性读取所有服务端响应。

Redis事务

事务是一次单独的隔离操作,事务中的所有命令都是经过序列化、按顺序执行的,事务在执行过程中不会被其他客户端的命令打断。

  • 开启事务

    multi命令由非事务状态>事务状态。

    不可嵌套使用。

  • 命令入列

    客户端进入事务状态之后,执行的所有redis命令都是入队,命令会FIFO的形式。

  • 执行事务的命令

    exec

  • 废弃事务的命令

    discard

  • 事务错误及回滚

    • 在如果的过程中输入命令之后没有显示Queued,那么说明命令本身有错误,入队失败,此时就会终止事务。
    • 当exec调用之后,有些命令可能会出现错误,redis会继续执行。

    redis不支持事务回滚。目的是:提高运行速度;如果发生语法错误,在开发过程中就已经检查并修复了。

  • watch实现乐观锁

    是一种CAS操作,watch命令用于监听事务中队列的命令,在exec之前,一旦有命令被修改,那整个事务被终止,exec返回null,提示事务失败。

    若watch监听的key在exec执行时未被修改(可以在事务执行过程中修改),那么正常执行事务;否则就不会执行。watch可以接收任意多的key

redis实现分布式锁

  • setNX,设置成功返回1,设置失败返回0

  • 单节点设置分布式锁

    1578721163168

  • 集群设置分布式锁

    客户端1从Master获取了锁,Master宕机了,存储锁的key还未来得及同步到从节点上,

    Slave升级为Master,

    客户端2从新的Master获取了对应统一资源的锁

    客户端1和客户端2同时持有同一资源的锁,锁不再具有安全性。

  • RedLock解决集群分布式锁

Redis持久化

  • RDB是对当前redis的数据进行一次快照。定时更新,缺点:耗时、耗性能(fork+io),容易丢失数

  • AOF会记录redis每一次的写操作。当服务器重启时,会重新执行这些命令来恢复原始数据。缺点是体积大,恢复速度慢。

    通常情况下AOF保存的数据要比RDB的方式要完整。

  • RDB的优点:

    在恢复数据量大的数据时,要比AOF要快。

缓存和数据库不一致的情况

库存可能会修改,修改时既要修改数据库,也要修改缓存中的数据。

问题情况1: 先删除缓存,再修改数据库,读缓存时,读不到就会去数据库读最新数据。如果删除缓存成功,数据库修改失败,数据库里面还是旧值,缓存中数据时空的,出现不一致现象。再去读的时候,缓存中没有就去数据库读旧的数据,然后更新到缓存中。

问题情况2:当修改库存时,先将缓存删除,再修改数据库中的数据,此时还未修改完成,另一个请求过来查询,查询首先先从缓存中获取。

Spring源代码

  • bean的种类:spring内部bean、注解bean、xml

  • 插手bean实例化的过程(AOP):

    用一个类去实现BeanPostProcessor接口,并且实现2个方法,一个是postProcessBeforeInitialization,另一个是postProcessAfterInitialization,方法的参数有2个,一个是原始bean,一个是bean的name,要想要达到效果必须将这个实现类添加到ioc容器中。

    也可以实现BeanFactoryPostProcessor接口,并实现其postProcessBeanFactory方法。通过这个方法我们可以获取beanfactory,获取beandefinationmaps,可以获取其中的beandefination,从而进行对bean的修改。

    也可以实现InitizlizingBean接口的afterproperties方法。

  • spring中applicationcontext的addbeanfactorypostprocessor方法可以手动将一个beanfactorypostprocessor添加到容器中。

  • 我们自定义的beanfactoryprocessor可以有2种方式:1. 实现beandefinationregistryPostProcessor接口(spring自己的)2. 实现beanfactoryprocessor接口(我们自定义的),当然beandeinationregistrypostprocessor接口继承了beanfactorypostprocessor接口,扩展了beanfacotrypostprocessor的能力,它提供了postProcessBeanDefinitionRegistry方法。

  • //这个方法将beandefination注册到defalultlistablebeanfactory中的beandefinationmap中去
    //beandefiantionholder = beandefination+beanname
    BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
  • bean的种类:spring内部bean、注解bean、xml

  • 插手bean实例化的过程(AOP):

    用一个类去实现BeanPostProcessor接口,并且实现2个方法,一个是postProcessBeforeInitialization,另一个是postProcessAfterInitialization,方法的参数有2个,一个是原始bean,一个是bean的name,要想要达到效果必须将这个实现类添加到ioc容器中。

  • spring扩展点:beanpostprocessor、beanfactorypostprocessor、beandefinationregistrypostprocessor

  • spring中判断bean是否已经被处理过,是通过判断beandefination中的CONFIGURATION_CLASS_ATTRIBUTE值是否为null,不为null,则证明已经被处理过

  • spring内置多种bd来描述,有annotatedBeandefination(加了注解的)、Rootbeandefination、ScannedGenericBeanDefiantion(被扫描注入的bean)

  • AnnotationConfigApplicationContext的构造方法中,初始化了一个叫ClassPathBeanDefinitionScanner的对象,该对象有一个scan方法用于扫描包下的bean,但是这个对象并不是spring内部扫描的原理实现。也就是说我们可以通过applicationcontext对象的scan方法可以手动扫描。

  • spring通过bd的构造方法将class转换成某种类型的bd

  • doProcessConfigurationClass处理注解。很难。parse方法、processConfigurationClass方法、

  • 想ioc注册bean:

    • applicationcontext.registry(),需要一个类,没有办法参与由类转换成bd的过程
    • bdmap.put(),需要一个类,没有办法参与将类转换成bd的过程
    • importBeanDefinationRegistrar()可添加一个bd,可参与过程(mapperscan将mapper接口转换成了spring中的对象。)
  • @import注解内属性添加一个实现了importselector接口的类,这个类需要实现selectorImports方法,这个方法的返回值就是需要注入到容器中的class的名字。

  • BeanfefinationBuilder.genericBeandefination(class)可以生成指定类的beandefinationbuilder,通过生成的builder对象的getBeandefination方法来获取beandefination,然后再通过registry.registryBeanddefination就可以将db注册到map中去。

  • 像mybatis那样,如何做到向容器中注入一个接口呢?首先新建接口的代理对象,然后将代理对象设置到接口对应的bd的beanclass属性(通过setBeanClass方法)。那么如何将代理对象设置到属性中呢,可以通过spring提供的factorybean来实现,新建一个factorybean的实现类,实现其getObject方法,当将此实现类注册到容器中时,spring获取到的bean就是getObject方法的实际返回值。但是此时如果factorybean有一个有参构造函数呢?可以通过beandefination.getConstructorArgumentValues().addGenericArgumentValue(name)方法可以将name对应的对象传给构造函数的入参。

  • AnnotationMetadata存在2个实现类,StandardAnnotationMetadata和AnnotationMetadataReadingVisitor,StandardAnnotationMetadata主要是利用java反射原理获取元数据;AnnotationMetadataReadingVisitor主要利用ASM技术分析字节码获取元数据

    //这个方法将beandefination注册到defalultlistablebeanfactory中的beandefinationmap中去
    //beandefiantionholder = beandefination+beanname
    BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
  • 普通的类(被@Component注解的类)是在扫描包时处理,扫描之后注册。

  • spring通过asm技术读取class,将其转换成bd,并将其注册

  • ConfigurationClassPostProcessor的postProcessBeanFactory方法的实现就是扫描注解

1578888996508

微服务时代

session一致性

  • 方案1:nginx+tomcat,利用tomcat的集群模式,但是会占用tomcat带宽,降低吞吐量。

  • 方案2:端存储,保存到浏览器中,服务器存储所有的session,将session存储到浏览器cookie中,一端只存储一个用户消息

  • 方案3:使用nginx的ip_hash让用户的请求都落到一台机器上。

    通过nginx负载均衡算法中的 ip_hash ,也就是ip 绑定方式,让每个客户端的和服务器进行了绑定,A 客户端访问了1号服务器,后面A 客户端发起的请求,都会分发到1号服务器了。

    缺点是没有了负载均衡

  • 方案4:spring-session保存到redis

CGLIB动态代理

动态生成要代理类的子类,子类重写代理类的非final方法,在子类中采用方法拦截的技术拉杰所有父类方法的调用,他比jdk的动态代理要快。

缺点:对于final方法,无法进行代理

jdk动态代理对InvocationHandler接口方法的调用对代理类内的所以方法都有效。

在CGLib回调时可以设置对不同方法执行不同的回调逻辑,或者根本不执行回调。

  • CallbackFilter接口。accept()----过滤方法,返回的值为数字,代表了Callback数组中的索引位置,要到用的Callback。
  • NoOp.INSTANCE:这个NoOp表示no operator,即什么操作也不做,代理类直接调用被代理的方法不进行拦截。
  • FixedValue接口。loadObject()表示锁定方法返回值,无论被代理类的方法返回什么值,回调方法都返回固定值

延时加载对象

说到延迟加载,应该经常接触到,尤其是使用Hibernate的时候,本篇将通过一个实例分析延迟加载的实现方式。 LazyLoader接口继承了Callback,因此也算是CGLib中的一种Callback类型。

另一种延迟加载接口Dispatcher。

Dispatcher接口同样继承于Callback,也是一种回调类型。

但是Dispatcher和LazyLoader的区别在于:LazyLoader只在第一次访问延迟加载属性时触发代理类回调方法,而Dispatcher在每次访问延迟加载属性时都会触发代理类回调方法。

PropertyBean o = (PropertyBean) enhancer.create(PropertyBean.class, new ConcreteClassLazyLoader());

接口生成器InterfaceMaker

public class TestInterfaceMaker {
    public static void main(String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        InterfaceMaker interfaceMaker =new InterfaceMaker();
        //抽取某个类的方法生成接口方法
        interfaceMaker.add(TargetObject.class);
        Class<?> targetInterface=interfaceMaker.create();
        for(Method method : targetInterface.getMethods()){
            System.out.println(method.getName());
        }
        System.out.println("---------------------------------------");
        //接口代理并设置代理接口方法拦截
        Object object = Enhancer.create(Object.class, new Class[]{targetInterface}, new MethodInterceptor(){
            @Override
            public Object intercept(Object obj, Method method, Object[] args,
                                    MethodProxy methodProxy) throws Throwable {
                if(method.getName().equals("method1")){
                    System.out.println("filter method1 ");
                    return "mmmmmmmmm";
                }
                if(method.getName().equals("method2")){
                    System.out.println("filter method2 ");
                    return 1111111;
                }
                if(method.getName().equals("method3")){
                    System.out.println("filter method3 ");
                    return 3333;
                }
                return "default";
            }});
        Method targetMethod1=object.getClass().getMethod("method3",new Class[]{int.class});
        int i=(int)targetMethod1.invoke(object, new Object[]{33});
        System.out.println(i);
        Method targetMethod=object.getClass().getMethod("method1",new Class[]{String.class});
        System.out.println(targetMethod.invoke(object, new Object[]{"sdfs"}));
    }
}

Mybatis

  • 单纯的mybatis源码仅仅是使用了jdk动态代理,分析没有意思。而spring结合mybatis值得探究。
  • spring+mybatis关联点:
    • @MapperScan
    • @Bean SqlSessionFactoryBean

使用了spring中的Import和ImportBeanDefinationRegistar技术来进行扩展。再对比一下@BeanSqlSessionFactoryBean就会知道spring会先去执行ImportBeanDefinationRegistar的registryBeanDefination方法。

在spring中都是mapperFactoryBean。

//spring结合mybatis之后这里获取的mapper实际上是jdk动态代理生成出来的。mybatis通过初始化一个类(MapperProxy),这个类实现InvocationHandler。
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        userMapper.query();
  • spring将mapper转换bean之后其实是mapperfactorybean
  • 如何判断一个类里面的属性spring要不要自动装配。spring默认采用不使用自动装配,在这种情况下,将属性加个@Autowire才能装配;当使用by type时,只要提供set方法,就可以自动装配,但是如果类里面没有提供set方法,他就不能装配。通过GenericBeanDefinition对象的setAutowireMode方法可以指定自动装配类型:(by type、by name、no)

mybatis的一级缓存的各种问题

spring当中为什么会失效

  • 在原始的mybatis中,sqlsession实际上是DefaultSqlSession,而spring结合之后,使用的是sqlSessionTemplate,sqlSessionTemplate的invoke方法在方法体的最后调用了sqlSession的close方法
  • 当使用applicationcontext.getBean(Mapper接口)时,一级缓存会失效,几次调用mapper,就有几次sql;而使用sqlsessionbuilder时,则一级缓存可以有效果。
  • 因为mybatis和spring结合的时候,扩展了一个类sqlsessiontemplate,这个类在spring容器启动时注入给了mapper,这个类替代了原来的defaultsqlsession,sqlsessiontemplate当中所有的查询方法不是直接查询,而是经过一个代理对象,代理对象增强了查询方法,主要是关闭了sqlsession。

mybatis缓存技术的底层原理

@MapperScan做了哪些事情

  • 扫描出所有的Mappper对应的Beandefination
  • 将mapper变成factorybean,mapperfactorybeandefination
  • 为beandefination添加一个构造方法的值,因为mapperfactorybean有一个有参构造方法,spring在实例化这个对象的实例时需要一个构造方法的值,这个值是class,后面spring在实例化过程中根据这个class返回我们的代理对象

执行mapper中的方法原理

  • 实际上调用了mappedStatements(map)的get方法,方法入参就是方法的名字

Mysql

mysql对每条记录的占用的最大空间有限制,占用的字节不超过65535个字节,除blob或者text类型的列之外,,其他所有的列(不包含隐藏列和记录头信息)。我们存储varchar(N)类型的列,需占用3部分的存储空间:

  • 真实数据
  • 真实数据占用字符的长度
  • Null值标识

InnoDB一页大小为16384字节=16kb

当每行数据大小大于一页能够存储的数据时,就出现了页溢出。这个时候会存储一部分真实数据,再存储下一页的地址-----row format(行格式)为Compact

Dynamic和Compressed行格式

  • 行中的列只存储真实数据所在页的地址
  • 和Compact的区别:compact存储真实数据和下一页的地址,而dynamic则只存储真实数据所在的页。Compressed则会采用压缩算法对页面进行压缩。

索引

查询默认排序

select时什么都不加,Innodb默认采用主键的值进行排序。聚集索引(主键索引)

InnoDB在插入数据的时候,会对页中的数据进行排序,并且相邻数据存在指针指向的关系。类似链表。因此查询的时候也就会有排序的现象

索引为什么会提高查询

  • 给定一组数列,如何快速的去查找指定的数字?

    • 首先回去考虑排序

    • 再去采用二分法去排序等等。

链表的结构在查询的时候会特别慢

因此在每一页的结构中会有一个pageRecord(页目录),Innodb会对页中的行进行分组,pagerecord中会记录分组中的数据行的第一条记录的主键id。页目录可以理解为一个数据递增的数组,存储了各个分组第一条记录的主键id。当要进行查找时,只需要利用二分法先去pagerecord中去找,然后再去对应分组中去找。

此时当数据行过多,需要分页,会出现由很多页的情况,现在去查找的时候,需要先确定数据在哪一页。因此同理需要存储每一页的页号和每一页的第一行数据。因此也就出现了和pagerecord一样的数据结构,这个数据结构存储了key为每一页的第一条记录的主键id,value存储了页号(指针)。

推导出的总体数据结构为:

1579952642949

此种数据结构叫做b+树。

  • 当建表的时候,Innodb会初始化一个空页(根页),当插入数据时,会在这个空页(根页)插入数据行,当出现行溢出时,innodb会复制第一页,然后将溢出的数据行插入到另一页,然将之前的第一页(根页)改为目录页

  • 还有一种特殊情况就是页的第一条数据可能相同,那么在排序的时候可能会更加复杂,因此innodb对页目录中还会存储主键即:

    image-20200126135932569

InnoDB对主键的生成策略

优先使用用户自定义的主键作为主键,若没有,则选取一个Unique健作为主键,若连Unique健都没有,Innodb会默认添加一个名为row-id的隐藏列为主键。Innodb会为每条记录都添加transaction-id(事务id)和roll-pointer(回滚id)列,但是row-id是可选的

mysql中utf8和java中有什么不同

mysql中的utf8占0-3字节,Java中占4个字节,mysql中的utf8md4才是和java中一样的。

建立联合索引

create index indexname on table(column,...);

比较的规则是先比较第一个,再比较第二个,再比较第三个

建立索引后的b+树后的叶子节点结构为:

1579955993598

当查询的时候,如果是select *,innodb首先根据条件会去联合索引的树中找到和索引对应的主键,然后再去主键树中查找所有的元素。

索引的缺点:

  • 每建一个索引会建立对应的b+树,最终会存储在一个文件中。空间上的代价
  • 当对记录进行修改和删除时,都会重新维护所有的索引。时间上的代价

什么情况下会用到这个索引

create index ti_index on t1(c,d,e);

  • 全值匹配联合索引字段

    select * from t1 where c = 1 and d = 2 and e =3

    如果我们的搜索条件中的列和索引中的列一致的话,这种情况就是全值匹配。

  • 匹配左边的列

    select * from t1 where c = 1

  • 匹配列前缀

    select * from t1 where c like '%101%' x

    select * from t1 where c like '101%' v

    需要注意的是若只给出后缀或者中间的某个字符串,mysql无法定位记录位置,因为字符串中间有101的字符串并没有排好序,所以只能全表扫描。有时候我们有一些匹配某些字符串后缀的需求,比如某一列存储了url

  • 匹配范围值

    所有记录都是按照索引从小到大排好序的

    select * from t1 where c > 1 and d >5

    上面查询又分为2部分:

    • 通过条件c>1进行范围,查找的结果可能有多条b不同的记录
    • 对于这些b值不同的记录继续通过d>5继续过滤

    这样对于联合索引来说,只能用到c列的部分,而用不到d列的部分,因为只有c值相同的时候才能用到d列的值进行排序,而这个查询中通过c进行范围查找的记录中可能并不是按照d列进行排序的,所以在搜索条件中继续以d列进行查找时用不到b+树索引

  • 排序

    查询时候的排序规则和b+树建立索引的规则是否一致

    select * from t1 order by c,d,e v

    select * from t1 order by d,c,e X

    select * from t1 where c = 1 order by d,e v,因为c=1的记录都是符合d,e的排序来的

    select * from t1 order by c asc,d desc,e X

如何建立索引

  • 考虑索引的选择性

    索引选择性 = 基数/记录数

    基数=记录去除重复值后的数量

    记录数= 数据行数

    选择性越高,代表索引的价值越大。

  • 考虑前置索引

    建立索引时只需要取某列的前几位

查询优化和Explain关键字解析

  • 对于一个sql语句,查询优化器会先看是否能转换成join,再将join进行优化

    优化分为:1.条件优化 2. 计算全表扫描成本 3. 找出所有能用到的索引4. 针对每个索引计算不同访问方式的成本 5.选出成本最小索引以及访问方式

    开启查询优化器日志

  • 开启

    set optimizer_trace='enabled=on'

  • 执行sql

  • 查看日志信息

    select * from information_schema.OPTIMIZER_TRACE

mysql中真正核心的是join

select * from t1,t2

select * from t1 (inner) join t2

select * from t2 (inner) join t1

这3种情况相同

基于成本

  • io成本

    innodb存储引擎都是将数据和索引存储到磁盘上,当我们想要查询表的记录时,需要先将数据或者索引加载到内存后然后再操作。这个从磁盘到内存的加载过程消耗的时间叫做IO成本

  • cpu成本

    读取以及检测记录是否满足对应的搜索条件、对结果集进行排序等操作消耗的时间称之为CPU成本

mysql会为每个表维护一系列统计信息,show Table STATUS语句来查看表的统计信息,

统计信息是异步更新的

ANALYZE TABLE single_table可以手动更新统计数据

SHOW INDEX FROM 表名可以查看某个表中索引的统计信息

统计信息中rows列在myisam引擎中时真实的数据行的数量;而Innodb在是一个估值

datalength标识占用空间大小,对于myisam引擎来说,该值是数据文件大小,而对于Innodb存储引擎来说,该值,就相当于聚簇索引占用的存储空间。data_length = 聚簇索引(主键索引)页面数量*每个页面大小。而在innodb中,每个页面的大小默认为16kb。其单位是byte。

全表扫描成本:聚簇索引页数 乘 1+行数 乘 0.2

in语句优化

索引的统计信息中Cardinality属性表示索引列中不重复值的个数。对于Innodb来说这个值是估计值。

当where条件中in中的值少于200个,那么就会精确计算估计行数

当where条件中in中的值多于200个,那么计算成本如何计算?

首先根据数据行数/不重复值个数得到平均单个值的重复个数,用这个值去*in语句中的参数就可以得到成本

NULL值如何处理

对于null,其实有三种理解:

  • null代表不确定的值,每个null都是独一无二的,在统计列的时候都应当是独立的
  • null在业务上代表没有,所有null的意义都是一样的。在统计不重复数量时算作一个
  • null完全没有意义,在统计时应当忽略。

innodb提供了一个系统变量:
show global variables like '%innodb_stats_method%';
这个变量有三个值:

  1. nulls_equal:认为所有NULL值都是相等的。这个值也是innodb_stats_method的默认值。如果某个索引
    列中NULL值特别多的话,这种统计方式会让优化器认为某个列中平均一个值重复次数特别多,所以倾向
    于不使用索引进行访问。
  2. nulls_unequal:认为所有NULL值都是不相等的。如果某个索引列中NULL值特别多的话,这种统计方式
    会让优化器认为某个列中平均一个值重复次数特别少,所以倾向于使用索引进行访问。
  3. nulls_ignored:直接把NULL值忽略掉。

最好不要在索引列中存放null值才是正解

null值在innodb中比任何值都要小,在索引树里面排在前面

优化join方法

  • mysql提供join-buffer内存空间,来缓存被驱动表,可以适当调整该内存空间大小,并且适当给驱动表增加索引就可以提高查询速度

  • 将sql语句尽量优化成内连接,外连接可以通过增加条件转换成内连接,内连接能够提高查询速度。

in子查询的优化

  • 1.物化表,当in语句的子查询不牵涉外表的时候,可以使用物化表,也就是将子查询的结果建立一个临时表

  • 半连接(semi join)

    • 直接将in语句转换成join语句

    • 利用主键去重

    • 逻辑上的去重

    • 利用索引去重

      什么情况下不能使用semi join进行优化

      • 外层查询的where条件中有其他搜索条件与in子查询组成的布尔表达式使用or连接
      • 使用not in
      • 子查询中包含having、、group by
      • 子查询包含union

对于不能转为semi join优化的查询来说,可以使用其他方式进行优化

  • 将子查询转换成物化表
  • 如果是相关子查询可以使用exists

派生表的优化

  • 物化表,有一个延时物化的机制
  • 将派生表的where条件移到外面去

Explain关键字

  • join基本上是选择行数比较少的表作为驱动表
  • union的原理是先将第一条结果放到一个临时表,再将第二个查询结果放到临时表,最终对这个临时表进行去重

事务

原子性、一致性、隔离性、持久性

  • show variables like 'autocommit' 是否自动提交

设置隔离级别

set session transaction isolation level read uncommitted

设置隔离级别

查看隔离级别

select @@tx_isolation

读未提交

一个事务可以读到另一个事务未提交的数据,容易出现脏读

脏读,一个事务读到另一个事务未提交已经修改过的数据。

读已提交

一个事务读到另一个已经提交的事务修改过的数据,并且其他事务对该数据进行一次修改并提交后,该事务都能查到该值,会出现不可重复读、幻读

幻读,如果一个事务现根据某些条件查出了一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次查询时,能把另一个事务插入的记录也查出来。

可重复读

一个事务第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到
的仍是第一次读到的值,而不是每次都读到不同的数据,这就是可重复读,这种隔离级别解决了不可重复,但是
还是会出现幻读。

mysql在这种隔离级别在底层解决了幻读的问题

串行化

不允许并发操作,读写加锁

原理

readview中的数组(m_ids)是在select之后生成的,会保存没有提交或回滚的事务。(read commit的隔离级别)

  • 可重复读可以用缓存来实现。前提是这2个查询在同一个事务中。可重复读就是2次查询的结果一致。底层原理是2次查询的readview中的数组(m_ids)一致,即使中间另一个事务提交。与读已提交不同的是,读已提交的2次查询每次都取最新的readview中的数组(m_ids)。读已提交就是把别人提交的数据读出来。这就是mvcc的原理,好处是提高并发性

读锁不等于读操作

普通的读操作不存在加读锁

读写锁

select ... lock in share mode加读锁

select ... for update加写锁

  • 当事务中给一条查询加了读锁,那么另一个事务再去修改,会被阻塞,而自己所在的事务进行修改时,不会被阻塞。update会默认加一把写锁。2个事务可以都对同一条记录加读锁,当其中一个事务想对数据进行修改时,由于另一个事务的读锁还未释放,因此修改操作会被阻塞
  • 当事务中给一条查询添加了写锁,其他事务既不能读,也不能写,只有自己能够修改数据

行锁表锁

行锁

读已提交
  • 查询使用的是主键(唯一索引)时,只需要在主键值(唯一索引)对应的那一个条数据加锁即可,对于另一条数据再次加锁还是不会阻塞。如果是用的辅助索引,那么除了辅助索引加锁之外还会对索引所对应的主键索引加锁。辅助索引在查询出来之后还回去回表,也就是辅助索引去找主键,然后再去主键索引上找。
    • 只需要再主键索引上加锁就行了,为什么还要在辅助索引上加锁呢?因为在辅助索引上加锁是为了让其他竞争锁的会话更早的发现。
    • 为什么要在主键索引上加锁呢?因为当尝试使用另一个条件进行加锁时,查询结果对应的主键可能已经加锁,保证了加锁的特性。
    • 需要注意的是,加锁操作如果不是在事务里面,由于自动提交的特性,所会被释放。
    • 查询(条件是非索引字段)的时候对某条记录加锁,另一个事务在插入时不会被阻塞,由于insert会隐式加锁,所以负责查询的事务在查询(for update)被插入的记录时会被阻塞。会出现幻读
  • 在读已提交中,不管使用索引还是不使用索引还是使用辅助索引,都是对查询结果中的记录进行加锁。
可重复读
  • 使用普通索引查询加锁时,实际上会有间隙锁的限制,所以不会出现幻读,因为插入操作根本就插不进去。间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。

  • 没有用到索引的情况。对所有的间隙和记录都加锁

表锁

  • LOCK TABLES t1 READ:对表t1加表级别的S锁。
  • LOCK TABLES t1 WRITE:对表t1加表级别的S锁

悲观锁

乐观锁

select默认加一个version字段

猜你喜欢

转载自www.cnblogs.com/mwss/p/12238527.html