Java底层知识汇总

Spring中的问题

Spring中的循环依赖

什么是循环依赖?

很简单,就是A对象依赖了B对象,B对象依赖了A对象。

比如:

// A依赖了B
class A{
    public B b;
}

// B依赖了A
class B{
    public A a;
}

那么循环依赖是个问题吗?

如果不考虑Spring,循环依赖并不是问题,因为对象之间相互依赖是很正常的事情。

比如

A a = new A();
B b = new B();

a.b = b;
b.a = a;

这样,A,B就依赖上了。

但是,在Spring中循环依赖就是一个问题了,为什么?

因为,在Spring中,一个对象并不是简单new出来了,而是会经过一系列的Bean的生命周期,就是因为Bean的生命周期所以才会出现循环依赖问题。当然,在Spring中,出现循环依赖的场景很多,有的场景Spring自动帮我们解决了,而有的场景则需要程序员来解决,下文详细来说。

要明白Spring中的循环依赖,得先明白Spring中Bean的生命周期。

Bean的生命周期

这里不会对Bean的生命周期进行详细的描述,只描述一下大概的过程。

Bean的生命周期指的就是:在Spring中,Bean是如何生成的?

被Spring管理的对象叫做Bean。Bean的生成步骤如下:

  1. Spring扫描class得到BeanDefinition
  2. 根据得到的BeanDefinition去生成bean
  3. 首先根据class推断构造方法
  4. 根据推断出来的构造方法,反射,得到一个对象(暂时叫做原始对象)
  5. 填充原始对象中的属性(依赖注入)
  6. 如果原始对象中的某个方法被AOP了,那么则需要根据原始对象生成一个代理对象
  7. 把最终生成的代理对象放入单例池(源码中叫做singletonObjects)中,下次getBean时就直接从单例池拿即可

可以看到,对于Spring中的Bean的生成过程,步骤还是很多的,并且不仅仅只有上面的7步,还有很多很多,比如Aware回调、初始化等等,这里不详细讨论。

可以发现,在Spring中,构造一个Bean,包括了new这个步骤(第4步构造方法反射)。

得到一个原始对象后,Spring需要给对象中的属性进行依赖注入,那么这个注入过程是怎样的?

比如上文说的A类,A类中存在一个B类的b属性,所以,当A类生成了一个原始对象之后,就会去给b属性去赋值,此时就会根据b属性的类型和属性名去BeanFactory中去获取B类所对应的单例bean。如果此时BeanFactory中存在B对应的Bean,那么直接拿来赋值给b属性;如果此时BeanFactory中不存在B对应的Bean,则需要生成一个B对应的Bean,然后赋值给b属性。

问题就出现在第二种情况,如果此时B类在BeanFactory中还没有生成对应的Bean,那么就需要去生成,就会经过B的Bean的生命周期。

那么在创建B的Bean的过程中,如果B类中存在一个A类的a属性,那么在创建B的Bean的过程中就需要A类对应的Bean,但是,触发B类Bean的创建的条件是A类Bean在创建过程中的依赖注入,所以这里就出现了循环依赖:

ABean创建–>依赖了B属性–>触发BBean创建—>B依赖了A属性—>需要ABean(但ABean还在创建过程中)

从而导致ABean创建不出来,BBean也创建不出来。

这是循环依赖的场景,但是上文说了,在Spring中,通过某些机制帮开发者解决了部分循环依赖的问题,这个机制就是三级缓存

三级缓存

三级缓存是通用的叫法。

一级缓存为:singletonObjects

二级缓存为:earlySingletonObjects

三级缓存为singletonFactories

先稍微解释一下这三个缓存的作用,后面详细分析:

  • singletonObjects中缓存的是已经经历了完整生命周期的bean对象。
  • earlySingletonObjects比singletonObjects多了一个early,表示缓存的是早期的bean对象。早期是什么意思?表示Bean的生命周期还没走完就把这个Bean放入了earlySingletonObjects。
  • singletonFactories中缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的。

解决循环依赖思路分析

先来分析为什么缓存能解决循环依赖。

上文分析得到,之所以产生循环依赖的问题,主要是:

A创建时—>需要B—->B去创建—>需要A,从而产生了循环

image.png

那么如何打破这个循环,加个中间人(缓存)

image.png

A的Bean在创建过程中,在进行依赖注入之前,先把A的原始Bean放入缓存(提早暴露,只要放到缓存了,其他Bean需要时就可以从缓存中拿了),放入缓存后,再进行依赖注入,此时A的Bean依赖了B的Bean,如果B的Bean不存在,则需要创建B的Bean,而创建B的Bean的过程和A一样,也是先创建一个B的原始对象,然后把B的原始对象提早暴露出来放入缓存中,然后在对B的原始对象进行依赖注入A,此时能从缓存中拿到A的原始对象(虽然是A的原始对象,还不是最终的Bean),B的原始对象依赖注入完了之后,B的生命周期结束,那么A的生命周期也能结束。

因为整个过程中,都只有一个A原始对象,所以对于B而言,就算在属性注入时,注入的是A原始对象,也没有关系,因为A原始对象在后续的生命周期中在堆中没有发生变化。

从上面这个分析过程中可以得出,只需要一个缓存就能解决循环依赖了,那么为什么Spring中还需要singletonFactories呢?

这是难点,基于上面的场景想一个问题:如果A的原始对象注入给B的属性之后,A的原始对象进行了AOP产生了一个代理对象,此时就会出现,对于A而言,它的Bean对象其实应该是AOP之后的代理对象,而B的a属性对应的并不是AOP之后的代理对象,这就产生了冲突。

B依赖的A和最终的A不是同一个对象

那么如何解决这个问题?这个问题可以说没有办法解决。

因为在一个Bean的生命周期最后,Spring提供了BeanPostProcessor可以去对Bean进行加工,这个加工不仅仅只是能修改Bean的属性值,也可以替换掉当前Bean。

举个例子:

@Component
public class User {
}
@Component
public class LubanBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        // 注意这里,生成了一个新的User对象
        if (beanName.equals("user")) {
            System.out.println(bean);
            User user = new User();
            return user;
        }

        return bean;
    }
}

** **

public class Test {
    public static void main(String[] args) {

        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(AppConfig.class);
        
        User user = context.getBean("user", User.class);
        System.out.println(user);

    }
}

运行main方法,得到的打印如下:

com.luban.service.User@5e025e70
com.luban.service.User@1b0375b3

所以在BeanPostProcessor中可以完全替换掉某个beanName对应的bean对象。

而BeanPostProcessor的执行在Bean的生命周期中是处于属性注入之后的,循环依赖是发生在属性注入过程中的,所以很有可能导致,注入给B对象的A对象和经历过完整生命周期之后的A对象,不是一个对象。这就是有问题的。

所以在这种情况下的循环依赖,Spring是解决不了的,因为在属性注入时,Spring也不知道A对象后续会经过哪些BeanPostProcessor以及会对A对象做什么处理

Spring到底解决了哪种情况下的循环依赖

虽然上面的情况可能发生,但是肯定发生得很少,我们通常在开发过程中,不会这样去做,但是,某个beanName对应的最终对象和原始对象不是一个对象却会经常出现,这就是AOP。

AOP就是通过一个BeanPostProcessor来实现的,这个BeanPostProcessor就是AnnotationAwareAspectJAutoProxyCreator,它的父类是AbstractAutoProxyCreator,而在Spring中AOP利用的要么是JDK动态代理,要么CGLib的动态代理,所以如果给一个类中的某个方法设置了切面,那么这个类最终就需要生成一个代理对象。

一般过程就是:A类—>生成一个普通对象–>属性注入–>基于切面生成一个代理对象–>把代理对象放入singletonObjects单例池中。

而AOP可以说是Spring中除开IOC的另外一大功能,而循环依赖又是属于IOC范畴的,所以这两大功能想要并存,Spring需要特殊处理。

如何处理的,就是利用了第三级缓存singletonFactories

首先,singletonFactories中存的是某个beanName对应的ObjectFactory,在bean的生命周期中,生成完原始对象之后,就会构造一个ObjectFactory存入singletonFactories中。这个ObjectFactory是一个函数式接口,所以支持Lambda表达式:() -> getEarlyBeanReference(beanName, mbd, bean)

上面的Lambda表达式就是一个ObjectFactory,执行该Lambda表达式就会去执行getEarlyBeanReference方法,而该方法如下:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

该方法会去执行SmartInstantiationAwareBeanPostProcessor中的getEarlyBeanReference方法,而这个接口下的实现类中只有两个类实现了这个方法,一个是AbstractAutoProxyCreator,一个是InstantiationAwareBeanPostProcessorAdapter,它的实现如下:

// InstantiationAwareBeanPostProcessorAdapter
@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
    return bean;
}
// AbstractAutoProxyCreator
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    return wrapIfNecessary(bean, beanName, cacheKey);
}

所以很明显,在整个Spring中,默认就只有AbstractAutoProxyCreator真正意义上实现了getEarlyBeanReference方法,而该类就是用来进行AOP的。上文提到的AnnotationAwareAspectJAutoProxyCreator的父类就是AbstractAutoProxyCreator。

那么getEarlyBeanReference方法到底在干什么?

首先得到一个cachekey,cachekey就是beanName。

然后把beanName和bean(这是原始对象)存入earlyProxyReferences中

调用wrapIfNecessary进行AOP,得到一个代理对象。

那么,什么时候会调用getEarlyBeanReference方法呢?回到循环依赖的场景中

image.png

左边文字

这个ObjectFactory就是上文说的labmda表达式,中间有getEarlyBeanReference方法,注意存入singletonFactories时并不会执行lambda表达式,也就是不会执行getEarlyBeanReference方法

右边文字

从singletonFactories根据beanName得到一个ObjectFactory,然后执行ObjectFactory,也就是执行getEarlyBeanReference方法,此时会得到一个A原始对象经过AOP之后的代理对象,然后把该代理对象放入earlySingletonObjects中,注意此时并没有把代理对象放入singletonObjects中,那什么时候放入到singletonObjects中呢?

我们这个时候得来理解一下earlySingletonObjects的作用,此时,我们只得到了A原始对象的代理对象,这个对象还不完整,因为A原始对象还没有进行属性填充,所以此时不能直接把A的代理对象放入singletonObjects中,所以只能把代理对象放入earlySingletonObjects,假设现在有其他对象依赖了A,那么则可以从earlySingletonObjects中得到A原始对象的代理对象了,并且是A的同一个代理对象。

当B创建完了之后,A继续进行生命周期,而A在完成属性注入后,会按照它本身的逻辑去进行AOP,而此时我们知道A原始对象已经经历过了AOP,所以对于A本身而言,不会再去进行AOP了,那么怎么判断一个对象是否经历过了AOP呢?会利用上文提到的earlyProxyReferences,在AbstractAutoProxyCreator的postProcessAfterInitialization方法中,会去判断当前beanName是否在earlyProxyReferences,如果在则表示已经提前进行过AOP了,无需再次进行AOP。

对于A而言,进行了AOP的判断后,以及BeanPostProcessor的执行之后,就需要把A对应的对象放入singletonObjects中了,但是我们知道,应该是要A的代理对象放入singletonObjects中,所以此时需要从earlySingletonObjects中得到代理对象,然后入singletonObjects中。

整个循环依赖解决完毕。

总结

至此,总结一下三级缓存:

  1. singletonObjects:缓存某个beanName对应的经过了完整生命周期的bean
  2. earlySingletonObjects:缓存提前拿原始对象进行了AOP之后得到的代理对象,原始对象还没有进行属性注入和后续的BeanPostProcessor等生命周期
  3. singletonFactories:缓存的是一个ObjectFactory,主要用来去生成原始对象进行了AOP之后得到的代理对象,在每个Bean的生成过程中,都会提前暴露一个工厂,这个工厂可能用到,也可能用不到,如果没有出现循环依赖依赖本bean,那么这个工厂无用,本bean按照自己的生命周期执行,执行完后直接把本bean放入singletonObjects中即可,如果出现了循环依赖依赖了本bean,则另外那个bean执行ObjectFactory提交得到一个AOP之后的代理对象(如果有AOP的话,如果无需AOP,则直接得到一个原始对象)。
  4. 其实还要一个缓存,就是earlyProxyReferences,它用来记录某个原始对象是否进行过AOP了。

源码解读

首先,有两种Bean的注入方式,构造器注入和属性注入,对于构造器注入的循环依赖,Spring处理不了,会直接抛出BeanCurrentlyCreationException异常,对于属性注入的循环依赖(单例模式下)是通过三级缓存来处理循环依赖的。而非单例对象的循环依赖则无法处理,下面分析单例模式下属性注入的循环依赖怎么处理的。首先bean单例对象的初始化大致分为三步;

  1. createBeanInstance:实例化bean,使用构造方法创建对象,为对象分配内存
  2. populateBean:进行依赖注入
  3. initializeBean:初始化bean

Spring为了解决单例的循环依赖的问题,使用了三级缓存:

  1. singletonObjects:完成了初始化的单例对象map
  2. earlySingletonObjects:完成实例化未初始化的单例对象map
  3. singletonFactories:单例对象工厂map,单例对象实例化完成后加入singletonFactories。

在调用createBeanInstance进行实例化后,会调用addSingletonFactories,将单例对象放到singletonFactories中。假如A依赖了B的实例对象,同时B也依赖了A的实例对象。

  1. A首先完成了实例化,并且将自己添加到singletonFactories中
  2. 接着继续宁依赖注入,发现自己依赖对象B,此时就会尝试去get(B)
  3. 发现B还没有被实例化,对B进行实例化
  4. 然后B在初始化时发现自己依赖了对象A于是调用get(A),在一级缓存singletonObjects和二级缓存earlySingletonObjects中进行查找找不到尝试从三级缓存singletonFactories中查找,由于A初始化的时候把自己加到singletonFactories中了,所以B可以拿到A对象,然后将A从三级缓存移动到二级缓存中
  5. B拿到A的对象后顺利完成了初始化,然后将自己放到一级缓存中,
  6. 此时返回A,A此时能拿到B的对象,顺利完成初始化

所以属性注入的循环依赖主要是通过将实例化完成的bean添加到singletonFactories中来实现的,而使用构造器依赖注入的bean在实例化的时候进行依赖注入,不会被添加到singletonFactories中

bean的初始化方法

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
...............
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                                  isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    if (logger.isTraceEnabled()) {
        logger.trace("Eagerly caching bean '" + beanName +
                     "' to allow for resolving potential circular references");
    }
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); //存放到SingletonFactorys中
}
..............
    if (earlySingletonExposure) {
        Object earlySingletonReference = getSingleton(beanName, false);
        if (earlySingletonReference != null) {
            if (exposedObject == bean) {
                exposedObject = earlySingletonReference;
            }
            ...........
        }
    }
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
            exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    return wrapIfNecessary(bean, beanName, cacheKey);
}
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
		Assert.notNull(singletonFactory, "Singleton factory must not be null");
		synchronized (this.singletonObjects) {
			if (!this.singletonObjects.containsKey(beanName)) {
				this.singletonFactories.put(beanName, singletonFactory);
				this.earlySingletonObjects.remove(beanName);
				this.registeredSingletons.add(beanName);
			}
		}
	}
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Quick check for existing instance without full singleton lock
    // 先从singletonObjects中找
    Object singletonObject = this.singletonObjects.get(beanName);
    // singletonObjects中没有,并且是正在创建的bean
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 从earlySingletonObjects中获取
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // Consistent creation of early reference within full singleton lock
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 从 singletonFactories中获取执行getObject方法获取早期经过aop的代理对象
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

正在创建的bean

public boolean isSingletonCurrentlyInCreation(String beanName) {
	return this.singletonsCurrentlyInCreation.contains(beanName);
}

在属性注入的时候,会先从singletonObject中获取,如果里面没有,并且这个bean正在创建(有一个map中存放了所有正在创建的bean),表示发生了循环依赖。然后会从earlySingletonObjects中获取,如果这里也没有,并且允许创建早期对象的话,会从singletonFactories中获取,如果有旧调用getObject方法,创建一个早期的并且经过aop的对象,并把它加到earlySingletonObjects中,移出singletonFactories

Spring中一些概念的总结

对象和Bean的区别?

个人观点:所谓的bean也是一个java对象,只不过这个对象是通过spring定义的,而一开始就是通过标签定义的,所以叫做bean。

普通对象和Bean对象还有其他区别,因为Bean对象是由Spring生成的,Spring在生成Bean对象的过程中,会历经很多其他步骤,比如属性注入,aop,new实例,调用初始化方法。

如何理解BeanDefinition?

顾名思义,BeanDefinition是用来描述一个Bean的,Spring会根据BeanDefinition来生成一个Bean。

BeanFactory和FactoryBean的区别

BeanFactory

BeanFactory是Spring IOC容器的顶级接口,其实现类有XMLBeanFactory,DefaultListableBeanFactory以及AnnotationConfigApplicationContext等。BeanFactory为Spring管理Bean提供了一套通用的规范。接口中提供的一些方法如下:

boolean containsBean(String beanName)

Object getBean(String)

Object getBean(String, Class)

Class getType(String name)

boolean isSingleton(String)

String[] getAliases(String name)

可以通过BeanFactory获得Bean。

FactoryBean

FactoryBean首先也是一个Bean,但不是简单的Bean,而是一个能生产对象的工厂Bean,可以通过定义FactoryBean中的getObject()方法来创建生成过程比较复杂的Bean。

如何理解BeanFactoryPostProcessor?

BeanFactoryPostProcessor也叫做BeanFactory后置处理器。这里包括两个概念,一个是BeanFactory,一个是后置处理器。

BeanFactory表示Bean工厂,可以基于BeanDefinition来生成Bean对象,所以在BeanFactory中存在所有的BeanDefinition。

后置处理器可以理解为:当某物品生产好了以后,可以进一步通过后置处理器来对此物品进行处理。

所以BeanFactoryPostProcessor可以理解为,可以得到BeanFactory对象并对它进行处理,比如修改它其中的某个BeanDefinition,或者直接向BeanFactory中添加某个对象作为bean。

如何理解BeanDefinitionRegistryPostProcessor?

BeanDefinitionRegistryPostProcessor是一个接口,继承了BeanFactoryPostProcessor,所以它也是一个BeanFactory后置处理器,所以它可以操作BeanFactory。

它特殊的地方在于,它拥有的功能比BeanFactoryPostProcessor多,比如BeanFactoryPostProcessor是不能向BeanFactory中添加BeanDefinition的(只能添加Bean对象),但是BeanDefinitionRegistryPostProcessor是可以向BeanFactory中添加BeanDefinition的。

如何理解@Import与ImportBeanDefinitionRegistrar?

Import注解

@Import首先是一个注解,在Spring中是用来向Spring容器中导入Bean的。换个角度理解,就是我们一般都是通过在某个类上加上@Component注解来标志一个bean的,但如果我们希望以一种更灵活的方式去定义bean的话,就可以利用@Import注解。

@Import注解所指定的类,在Spring启动过程中会对指定的类进行判断,判断当前类是不是实现了比较特殊的接口,比如ImportBeanDefinitionRegistrar,如果存在特殊的接口就执行特殊的逻辑,如果没有则生成该类对应的BeanDefinition并放入BeanFactory中。

ImportBeanDefinitionRegistrar

通过Import注解可以注册bean,虽然它也支持同时注册多个bean,但是不方便,特别是当我们想通过实现一些复杂逻辑来注册bean的话,仅仅通过Import注解是不方便的,这时就可以使用ImportBeanDefinitionRegistrar这个接口来动态的注册bean了,我这里说的注册bean指的是:通过生成BeanDefinition,并且把BeanDefinition放入BeanFactory中。

如何理解BeanDefinitionRegistry和BeanFactory?

BeanFactory表示Bean工厂,可以利用BeanFactory来生成bean。

BeanDefinitionRegistry表示BeanDefinition的注册表,可以用来添加或移除BeanDefinition。

Spring依赖注入原理分析(未完成)

Spring中有几种依赖注入的方式?

** **

这是一个面试高频题,但是我在面程序员的时候,听过很多种答案。那么标准答案是什么?我们先不说,一步步来分析。

什么是依赖注入

首先,我们得知道什么是依赖注入?就是填充属性。

一个对象通常都会有属性,比如:

public class OrderService {
    private UserService userService;
    
    public UserService getUserService() {
        return userService;
    }
}

OrderService中有一个属性UserService, UserService就是OrderService的依赖。

那么Spring的依赖注入,就是Spring框架去进行属性的填充。那么我们就要站在Spring的角度去思考:如果你是Spring的开发者,如果实现对一个对象的属性进行填充?

** **

在进行属性填充之前,我们得先知道:在一个对象中,哪些属性可以进行填充?

肯定是需要业务开发者去告诉程序员的,比如在属性上加一个特定的注解,比如Spring中的@Autowired。当Spring遇到该属性时,发现该属性存在这个注解,Spring就会对当前这个属性进行填充。那么怎么填充呢?

填充其实就是对属性进行赋值,那么Spring能怎么对这个属性进行赋值呢?赋的什么值呢?

假设赋的值是xx。我们先考虑如果把这个xx赋值给OrderService对象。

可能我们立马能想到的就是:

orderService.userService = xx;

这个思路没错,但是不能满足所有情况,因为orderService这个对象是在Spring中实例化的,userService这个属性的权限修饰符是private,所以在Spring中不能直接进行赋值,但是可以通过反射,比如:

// 随便new一个表示xx,Spring中寻找要注入的值是一个比较复杂的过程
UserService xx = new UserService();

Class c= Class.forName("com.luban.service.OrderService");
Object cInstance = c.newInstance();
Field[] fields = c.getDeclaredFields();
fields[0].setAccessible(true);  // fields[0]表示的就是userServce属性
fields[0].set(cInstance, xx);
System.out.println(((OrderService)cInstance).getUserService());

这样,通过反射就能对属性进行赋值了。那么怎么寻找到准确的应该赋值给该属性的值呢?

上文中的xx肯定也是一个对象,也就是说是Spring中的一个bean,那么Spring该如何根据当前属性去找到对应的bean呢?只有两种方式:

  1. 根据属性的名字
  2. 根据属性的类型

根据属性的名字去Spring容器中去找bean,要么找不到,要么就能找到一个bean,因为Spring中的beanName是唯一的。

格局属性的类型去Spring容器中去找bean,要么找不到,要么可能找到一个或多个bena,因为Spring容器中的bean实际就是一个对象,而一个类型是可以有多个对象的,在Spring容器中也是如此。

那么Spring针对这两种方式会如何选择呢?二选一,还是二合一。

答案很明显,肯定是二合一,一种方式找不到就利用另外一个方式去找。那么两种方式中会优先利用哪种方式去找?

答案很明显,肯定是先利用属性的名字,因为利用名字去找更精确。

byName

根据属性名,去

byType

Spring整合Mybatis原理

在介绍Spring整合Mybatis原理之前,我们得先来稍微介绍Mybatis的工作原理。

Mybatis的基本工作原理

在Mybatis中,我们可以使用一个接口去定义要执行sql,简化代码如下:

定义一个接口,@Select表示要执行查询sql语句。

public interface UserMapper {
  @Select("select * from user where id = #{id}")
  User selectById(Integer id);
}

以下为执行sql代码:

InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();

// 以下使我们需要关注的重点
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Integer id = 1;
User user = mapper.selectById(id);

Mybatis的目的是:使得程序员能够以调用方法的方式执行某个指定的sql,将执行sql的底层逻辑进行了封装。

** **

这里重点思考以下mapper这个对象,当调用SqlSession的getMapper方法时,会对传入的接口生成一个代理对象,而程序要真正用到的就是这个代理对象,在调用代理对象的方法时,Mybatis会取出该方法所对应的sql语句,然后利用JDBC去执行sql语句,最终得到结果。

分析需要解决的问题

Spring和Mybatis时,我们重点要关注的就是这个代理对象。因为整合的目的就是:把某个Mapper的代理对象作为一个bean放入Spring容器中,使得能够像使用一个普通bean一样去使用这个代理对象,比如能被@Autowire自动注入。

** **

比如当Spring和Mybatis整合之后,我们就可以使用如下的代码来使用Mybatis中的代理对象了:

@Component
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public User getUserById(Integer id) {
        return userMapper.selectById(id);
    }
}

** **

UserService中的userMapper属性就会被自动注入为Mybatis中的代理对象。如果你基于一个已经完成整合的项目去调试即可发现,userMapper的类型为:org.apache.ibatis.binding.MapperProxy@41a0aa7d。证明确实是Mybatis中的代理对象。

好,那么现在我们要解决的问题的就是:如何能够把Mybatis的代理对象作为一个bean放入Spring容器中?

要解决这个,我们需要对Spring的bean生成过程有一个了解。

Spring中Bean的产生过程

Spring启动过程中,大致会经过如下步骤去生成bean

  1. 扫描指定的包路径下的class文件
  2. 根据class信息生成对应的BeanDefinition
  3. 在此处,程序员可以利用某些机制去修改BeanDefinition
  4. 根据BeanDefinition生成bean实例
  5. 把生成的bean实例放入Spring容器中

假设有一个A类,假设有如下代码:

一个A类:

@Component
public class A {
}

一个B类,不存在@Component注解

public class B {
}

执行如下代码:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(context.getBean("a"));

输出结果为:com.luban.util.A@6acdbdf5

A类对应的bean对象类型仍然为A类。但是这个结论是不确定的,我们可以利用BeanFactory后置处理器来修改BeanDefinition,我们添加一个BeanFactory后置处理器:

@Component
public class LubanBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("a");
        beanDefinition.setBeanClassName(B.class.getName());
    }
}

这样就会导致,原本的A类对应的BeanDefiniton被修改了,被修改成了B类,那么后续正常生成的bean对象的类型就是B类。此时,调用如下代码会报错:

context.getBean(A.class);

但是调用如下代码不会报错,尽管B类上没有@Component注解:

context.getBean(B.class);

并且,下面代码返回的结果是:com.luban.util.B@4b1c1ea0

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(context.getBean("a"));

之所以讲这个问题,是想说明一个问题:在Spring中,bean对象跟class没有直接关系,跟BeanDefinition才有直接关系。

那么回到我们要解决的问题:如何能够把Mybatis的代理对象作为一个bean放入Spring容器中?

** **

在Spring中,如果你想生成一个bean,那么得先生成一个BeanDefinition,就像你想new一个对象实例,得先有一个class。

** **

解决问题

继续回到我们的问题,我们现在想自己生成一个bean,那么得先生成一个BeanDefinition,只要有了BeanDefinition,通过在BeanDefinition中设置bean对象的类型,然后把BeanDefinition添加给Spring,Spring就会根据BeanDefinition自动帮我们生成一个类型对应的bean对象。

所以,现在我们要解决两个问题:

  1. Mybatis的代理对象的类型是什么?因为我们要设置给BeanDefinition
  2. 我们怎么把BeanDefinition添加给Spring容器?

** **

注意:上文中我们使用的BeanFactory后置处理器,他只能修改BeanDefinition,并不能新增一个BeanDefinition。我们应该使用Import技术来添加一个BeanDefinition。后文再详细介绍如果使用Import技术来添加一个BeanDefinition,可以先看一下伪代码实现思路。

假设:我们有一个UserMapper接口,他的代理对象的类型为UserMapperProxy。

那么我们的思路就是这样的,伪代码如下:

BeanDefinitoin bd = new BeanDefinitoin();
bd.setBeanClassName(UserMapperProxy.class.getName());
SpringContainer.addBd(bd);

但是,这里有一个严重的问题,就是上文中的UserMapperProxy是我们假设的,他表示一个代理类的类型,然而Mybatis中的代理对象是利用的JDK的动态代理技术实现的,也就是代理对象的代理类是动态生成的,我们根本无法确定代理对象的代理类到底是什么。

所以回到我们的问题:Mybatis的代理对象的类型是什么?

** **

本来可以有两个答案:

  1. 代理对象对应的代理类
  2. 代理对象对应的接口

** **

那么答案1就相当于没有了,因为是代理类是动态生成的,那么我们来看答案2:代理对象对应的接口

如果我们采用答案2,那么我们的思路就是:

BeanDefinition bd = new BeanDefinitoin();
// 注意这里,设置的是UserMapper
bd.setBeanClassName(UserMapper.class.getName());
SpringContainer.addBd(bd);

但是,实际上给BeanDefinition对应的类型设置为一个接口是行不通的,因为Spring没有办法根据这个BeanDefinition去new出对应类型的实例,接口是没法直接new出实例的。

那么现在问题来了,我要解决的问题:Mybatis的代理对象的类型是什么?

两个答案都被我们否定了,所以这个问题是无解的,所以我们不能再沿着这个思路去思考了,只能回到最开始的问题:如何能够把Mybatis的代理对象作为一个bean放入Spring容器中?

总结上面的推理:我们想通过设置BeanDefinition的class类型,然后由Spring自动的帮助我们去生成对应的bean,但是这条路是行不通的。

终极解决方案

那么我们还有没有其他办法,可以去生成bean呢?并且生成bean的逻辑不能由Spring来帮我们做了,得由我们自己来做。

FactoryBean

有,那就是Spring中的FactoryBean。我们可以利用FactoryBean去自定义我们要生成的bean对象,比如:

@Component
public class LubanFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        Object proxyInstance = Proxy.newProxyInstance(LubanFactoryBean.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (Object.class.equals(method.getDeclaringClass())) {
                    return method.invoke(this, args);
                } else {
                    // 执行代理逻辑
                    return null;
                }
            }
        });

        return proxyInstance;
    }

    @Override
    public Class<?> getObjectType() {
        return UserMapper.class;
    }
}

** **

我们定义了一个LubanFactoryBean,它实现了FactoryBean,getObject方法就是用来自定义生成bean对象逻辑的。

执行如下代码:

public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        System.out.println("lubanFactoryBean: " + context.getBean("lubanFactoryBean"));
        System.out.println("&lubanFactoryBean: " + context.getBean("&lubanFactoryBean"));
        System.out.println("lubanFactoryBean-class: " + context.getBean("lubanFactoryBean").getClass());
    }
}

将打印:

lubanFactoryBean: com.luban.util.LubanFactoryBean$1@4d41cee

&lubanFactoryBean: com.luban.util.LubanFactoryBean@3712b94

lubanFactoryBean-class: class com.sun.proxy.$Proxy20

从结果我们可以看到,从Spring容器中拿名字为”lubanFactoryBean”的bean对象,就是我们所自定义的jdk动态代理所生成的代理对象。

所以,我们可以通过FactoryBean来向Spring容器中添加一个自定义的bean对象。上文中所定义的LubanFactoryBean对应的就是UserMapper,表示我们定义了一个LubanFactoryBean,相当于把UserMapper对应的代理对象作为一个bean放入到了容器中。

但是作为程序员,我们不可能每定义了一个Mapper,还得去定义一个LubanFactoryBean,这是很麻烦的事情,我们改造一下LubanFactoryBean,让他变得更通用,比如:

@Component
public class LubanFactoryBean implements FactoryBean {

    // 注意这里
    private Class mapperInterface;
    public LubanFactoryBean(Class mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public Object getObject() throws Exception {
        Object proxyInstance = Proxy.newProxyInstance(LubanFactoryBean.class.getClassLoader(), new Class[]{mapperInterface}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                if (Object.class.equals(method.getDeclaringClass())) {
                    return method.invoke(this, args);
                } else {
                    // 执行代理逻辑
                    return null;
                }
            }
        });

        return proxyInstance;
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }
}

改造LubanFactoryBean之后,LubanFactoryBean变得灵活了,可以在构造LubanFactoryBean时,通过构造传入不同的Mapper接口。

实际上LubanFactoryBean也是一个Bean,我们也可以通过生成一个BeanDefinition来生成一个LubanFactoryBean,并给构造方法的参数设置不同的值,比如伪代码如下:

BeanDefinition bd = new BeanDefinitoin();
// 注意一:设置的是LubanFactoryBean
bd.setBeanClassName(LubanFactoryBean.class.getName());
// 注意二:表示当前BeanDefinition在生成bean对象时,会通过调用LubanFactoryBean的构造方法来生成,并传入UserMapper
bd.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class.getName())
SpringContainer.addBd(bd);

特别说一下注意二,表示表示当前BeanDefinition在生成bean对象时,会通过调用LubanFactoryBean的构造方法来生成,并传入UserMapper的Class对象。那么在生成LubanFactoryBean时就会生成一个UserMapper接口对应的代理对象作为bean了。

到此为止,其实就完成了我们要解决的问题:把Mybatis中的代理对象作为一个bean放入Spring容器中。只是我们这里是用简单的JDK代理对象模拟的Mybatis中的代理对象,如果有时间,我们完全可以调用Mybatis中提供的方法区生成一个代理对象。这里就不花时间去介绍了。

Import

到这里,我们还有一个事情没有做,就是怎么真正的定义一个BeanDefinition,并把它添加到Spring中,上文说到我们要利用Import技术,比如可以这么实现:

定义如下类:

public class LubanImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
        beanDefinition.setBeanClass(LubanFactoryBean.class);
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
        // 添加beanDefinition
        registry.registerBeanDefinition("luban"+UserMapper.class.getSimpleName(), beanDefinition);
    }
}

并且在AppConfig上添加@Import注解:

@Import(LubanImportBeanDefinitionRegistrar.class)
public class AppConfig {

这样在启动Spring时就会新增一个BeanDefinition,该BeanDefinition会生成一个LubanFactoryBean对象,并且在生成LubanFactoryBean对象时会传入UserMapper.class对象,通过LubanFactoryBean内部的逻辑,相当于会自动生产一个UserMapper接口的代理对象作为一个bean。

总结

总结一下,通过我们的分析,我们要整合Spring和Mybatis,需要我们做的事情如下:

  1. 定义一个LubanFactoryBean
  2. 定义一个LubanImportBeanDefinitionRegistrar
  3. 在AppConfig上添加一个注解@Import(LubanImportBeanDefinitionRegistrar.class)

优化

这样就可以基本完成整合的需求了,当然还有两个点是可以优化的

第一,单独再定义一个@LubanScan的注解,如下:

@Retention(RetentionPolicy.RUNTIME)
@Import(LubanImportBeanDefinitionRegistrar.class)
public @interface LubanScan {
}

这样在AppConfig上直接使用@LubanScan即可

第二,在LubanImportBeanDefinitionRegistrar中,我们可以去扫描Mapper,在LubanImportBeanDefinitionRegistrar我们可以通过AnnotationMetadata获取到对应的@LubanScan注解,所以我们可以在@LubanScan上设置一个value,用来指定待扫描的包路径。然后在LubanImportBeanDefinitionRegistrar中获取所设置的包路径,然后扫描该路径下的所有Mapper,生成BeanDefinition,放入Spring容器中。

所以,到此为止,Spring整合Mybatis的核心原理就结束了,再次总结一下:

  1. 定义一个LubanFactoryBean,用来将Mybatis的代理对象生成一个bean对象
  2. 定义一个LubanImportBeanDefinitionRegistrar,用来生成不同Mapper对象的LubanFactoryBean
  3. 定义一个@LubanScan,用来在启动Spring时执行LubanImportBeanDefinitionRegistrar的逻辑,并指定包路径

以上这个三个要素分别对象org.mybatis.spring中的:

  1. MapperFactoryBean
  2. MapperScannerRegistrar
  3. @MapperScan

JVM底层

Java的跨平台是如何实现的

Java是基于虚拟机栈运行的,

new不是原子性操作

JVM的四大模块

  • 类加载子系统
  • 内存模型
    • 本地方法栈
    • 虚拟机栈
    • 程序计数器
    • 方法区
  • 执行引擎
  • 垃圾回收期

  • CPU位数︰32位、g64位

  • 操作系统派系:Windows派系、Unix派系

    • Unix派系:Mac、Centos、Ubuntu、Android
    • 汇编指令:Intel汇编、AT&T汇编、ARM汇编
    • 内存模型

Java文件首先要编译成class文件,那么在32位机和64位机器上编译的结果肯定是不一样

image-20200923213649372

跨平台的本质就是汇编语言的不同

image-20200923213736220

两种汇编风格(还有ARM汇编:主要用在移动设备上,手提电脑,手机,平板,叫做嵌入式系统)

Java底层的char是基于short的

Java的执行引擎是如何实现的:

  • 字节码解析器:java代码—>C++代码—>汇编:效率太低,不如C++

  • 模板解析器:java字节码–>汇编语言(效率提高了)

Java程序包括了

  • 硬编码
  • java字节码
  • java程序

我们运行java程序run的时候发生了什么

  • 调用javac编译整个java程序
  • 调用java命令

jvm的初始化

  • class文件
    • 硬盘上的.class文件
  • class content
    • 内存中的一块区域,将.class读到内存中。放在直接内存中
  • class对象
    • 类加载子系统基于《Java虚拟机规范》进行解析
    • class对象放在哪?
      • 方法区:InstanceKlass存的是元信息:字符串,常量池,方法信息等
      • 堆区:InstanceMirrorKlass:镜像类,是一个C++对象
  • 对象
    • new生成的对象(或反射等等方法)

方法区

经常会跟永久代,元空间放在一起,他们之间的关系是

  • 方法区是规范,永久代、元空间是具体实现
  • 方法区是接口,永久代、元空间是实现类

永久代、元空间是否同时存在同一个JVM中?否

永久代

  • jdk8以前方法区的实现
  • 堆中的

元空间

  • jdk8以后方法区的实现
  • 直接内存中的

为什么以元空间取代永久代?

  1. oom:元空间存放的是Klass文件信息,不可避免的会占用一些空间
  2. gc:既有对象,又有源信息。垃圾回收判断你很困难
  3. 受硬件限
    1. 32位机4G
      • 内核2G
      • 应用层2G
    2. 64位机= 16 +48(16位作为保留位,48位作为实际使用的位数)
      • 2的64次方
      • 2的48次方256T

元空间有什么缺点?

  • 动态生成

元空间的调优

java -XX:+PrintFlagsFinal -version | grep ThreadStack

最小20.75M

最大 256T

元空间的调优

  1. 最大、最小设置成一样,防止内存抖动
  2. 大小设置成物理机器的1/32
    • arthas、visualyM..
  3. 保存20% -30%的空间空余

本地方法栈

运行JNI程序需要的栈

虚拟机栈

数据结构栈有什么区别

一个是规范,一个是实现

JVM中有几个虚拟机栈?

  • 一个线程一个,随线程的创建而创建,随线程的结束而销毁
  • 线程独享
  • 线程安全

一个虚拟机栈中有几个栈帧?

  • 函数调用次数

栈中也会出现OOM

stackoverflow:所以也需要进行调优

栈中的几个区域

  • 局部变量表:
  • 操作数栈
  • 动态链接 直接地址:该方法在方法区中地址,方法的对象的指针
  • 返回地址 恢复现场:(java中没有指针的概念,不像C和C++那样能直接指向硬件地址)
  • 附加信息

轻量级锁(sync),也是基于栈

image-20200923221926370

程序计数器

软件:OD

Java虚拟机时通过软件模拟出来的,C和C++是直接在硬件上运行的(基于寄存器)

寄存器中有一块区域叫EIP指的就是程序计数器,而java是通过程序模拟出来的我们没有程序计数器,在java中程序计数器就是java字节码前面的索引

image-20200923222350087

程序计数器是由执行引擎进行改动的

main方法调用add方法栈帧发生了变化

  1. 创建add方法的栈帧
  2. main方法的程序计数器压入栈
  3. 局部表开始指针指向add方法栈帧中的局部变量表开始位置
  4. 操作数栈指针,指向add方法站怎中的操作数栈开始位置

逆向操作

add方法执行完后,发生了什么:

  1. 重置操作数栈指针
  2. 重置局部表指针
  3. 程序计数器赋值
  4. 返回值压入main方法的操作数栈中
  5. 销毁栈帧

new一个对象的流程

//slot  插槽_模拟机栈里面每个格子
public class Test4 {
    public static Test4 test4 = newTest4();
    //形参、形式参数
    //实参、实际参数
    public static void main(String[] args) {
        Test4 demo = new Test4();
        //调用默认构造函数
        class<Test4> clazz = Test4.class;
        System.out.println(demo.add());
    }
    public int add() {
        int a = 10;
        int b = 20;
        return a + b;
    }
}

百度搜:字节码手册

0 new #2 <com/qimingnan/jvm/Test4>
	创建空对象
3 dup
	1、赋值栈顶元素
	2、重新压入栈(为什么呢?后面的方法当this指针用)
4 invokespecial #3<com/qimingnan/jvm/Test4.<init>>
	执行默认构造方法
	
	完整对象
7 astore_1
	1、将完整对象的指针pop出来
	2、赋值给局部变量表中index=1的位置

image-20200923230152885

执行完构造方法后,对象在堆中创建

image-20200923230306939

最后一步,pop出来赋值给index为1的位置

image-20200923230443761

而局部变量表index为1的位置变量为demo,既Test4 demo = new Test4();执行完

image-20200923230552655

这就可以解释为什么new 不是线程安全的,在字节码中它是由四步组成的

运行时数据区中的关系

image-20200923231036535

虚拟机栈指向方法区,该怎么理解?动态连接

虚拟机栈指向堆区,该怎么理解?对象引用

堆区指向方法区,该怎么理解?对象的内存布局

new出来的一个对象存储在堆中保存的样子是什么样子的?

其中的类型指针(Klass Pointer)堆区所有对象头中都有一个kp,这个kp指向方法区

image-20200923231425386

方法区指向堆区,该如何理解?静态变量

常量池

  • class文件中的常量池
  • 运行时常量池
  • 全局字符串引用

堆区最主要的就是调优

  • 计算对象大小
  • 指针压缩
  • 调优实战
  • 空间担保机制
  • 动态年龄判断

java的根加载器其实不纯是c++实现的(C++ + Java来实现的)

调优

对象的内存布局

image-20200924205727182

对象头(锁就与它有关)

image-20200924205756580

64位机为例

  • mark word:8字节 64bit
  • 类型指针: 8字节(会变成4字节,在开启指针压缩的时候)
  • 数组长度:4个字节(不是数组对象就为0)
  • 实例数据:类的普通属性(不包括静态变量)
  • 对齐填充:

java中所有的对象都是8字节对齐的

类型指针:对象所属类的class对象的内存地址

对其填充其实有两个部分组成

image-20200924210446373

计算对象大小

指针压缩

jdk6默认是开启的

-XX:+/-UseCompressedOops

空对象占多少字节

没有数据的对象叫空对象(没有普通属性)

  • 开启指针压缩:16—–8(mark word)+4(类型指针)+4(填充空间)
  • 未开启指针压缩:16

image-20200924211341669

普通对象

public class Test{
	int a=20;
	int b=20;
}
  • 开启:24(8+4+(4+4)+4)=24
  • 关闭:24(8+8+(4+4))=24

指针压缩:将内存地址的8字节–>4字节,节省了空间,提升了寻址效率

public class Test{
	int a=20;
	int b=20;
	static int[] arr={0,1,2};
}
  • 开启:32
  • 关闭:40

image-20200924211948989

image-20200924212157878

指针压缩

64bit机下,内存地址占8字节

8—>4到底是怎么存储的?使用过程中做了什么?

实现原理

image-20200924212859919

test1=00 000

test2=10 000

test3=110 000

8字节对齐后三位永远是0

在存储的时候,去除3为,高位补0

test1=00

test2=10

test3=110

再用的时候增加三位,低位补0

test1=00 000

test2=10 000

test3=110 000

那么只是移动的3位,还少一位呢?

8字节–>4字节之后与32位机器又有什么差别呢?

我们使用了这样的技术

  1. 我们的性能一定要高于32位机器
  2. 留有一定的扩容的退路

对象实际上是占35位,按32位存储

oop不是面向对象的意思是对象指针的意思(ordinary object pointer)

oops:

开启指针压缩内存地址占4字节 32位,在使用的时候增加三位32+3

一个oop能支持的最大堆空间是多少?2^35

如何扩容?16字节对齐,

这个扩容是修改操作系统代码还是openjdk代码?openjdk

调优阶段

  • 上线前
  • 项目运行阶段
  • 线上出现OOM

亿级流量秒杀系统JVM调优

一亿次访问

一个uv->20pv

访客=1亿/ 20约=500w

访客数/下单数

转化率是在10%

500w * 10% = 50w

40%的订单是在前两分钟完成下单的

50*30%= 15w订单

15w / 120s= 1200笔

从下单到支付完成,生成的对象占用多少内存

商品信息、订单信息、物流信息、积分信息、优惠券信息….

20KB

每秒系统生成的对象·200M = 1200 *20K

下单到支付对象要存在2s

32G内存机器 堆最大=8G(1/4)

新生代:2.7G

Eden:2.2G

from:270M

to:270M

老年代:5.3G

1G = 1000M

2700M

14s就会出发young gc

400M内存无法回收,from,to区只有200多M无法放下,会空间担保放到老年代中

400M进入老年代

一分钟左右老年代就会充满,出发full gc—

如何做调优?

不触发空间担保

GC

计算机在设计的时候分为了四层,但是在实际的使用的时候(win、linux)实际使用的是第0和第3层即(R0内核态,R3用户态)。那么在分配内存的时候为什么要从用户态切换到内核态呢?

内存中的 数据是存在内存条上面的,内存条是操作系统借助驱动去操作的,而我们应用层程序是没有能力去操作硬件的,所以只要你分配内存读写数据,都需要内核来起到一些作用,那么就必然需要从用户态切换到内核态。当你分配完内存,读写完数据的时候就要返回一些数据,那么就必然需要从内核态转为用户态

image-20200926203920454

物理地址:一套算法将内存条映射成一套物理地址

线性地址:我们现在无论是进行分配内存还是引用传值传到内存啊,其实都是线性地址

线性地址又与物理地址之间有一层映射

内存池的结构

image-20200926204836424

在分配内存的时候是在操作系统的堆上面来分配内存的,

memory pool:内存之,一个概念上的池子

memory chunk:操作系统块,一个内存池可以管理无数个内存块

内存池

class MemoryPool i
/**
*所有需要释放内存的成员
*/
pvate:
	list<MemoryChunk*> m_chunks;//这个池子中所有的内存块
public:
	~MemoryPool();
public:
/**
        *创建新的Chunk
        *@param mem_size
        *@return
        */
MemoryChunk *new_chunk(uint mem_size);
public:
/**
*打印所有中Chunk
*/
	void print_chunks();
/**
*释放所有Chunk占用的内存
*/
	void free_chunks();
}

memory_chunk,真正操作系统分配的块


class MemoryChunk {
private:
    /**
        *创建Chunk的文件名,这个块是在哪一个文件中生成的
        */
    char *m_filename;
    /*
        *创建Chunk的文件位置,块是在哪里被申请的
        */
    uint m_line;
    /**
        *该Chunk的内存大小,内存块总体的大小
        */
    uint m_size;
    /**
	*以多少字节对齐  一般是8字节对齐
	*/
        uint m_align_size;
    /**
	*该Chunk包含多少Cell
	*/
    uint m_cell_num;
    /**
	*被用了的Chunk数量
	*/
    uint m_used_cell_num;
/**
*需要释放内存的数据
*/
private:
	/**
	*存储数据的地方
	*/
    pvoid m_data;
    list<Memorycell *> m_available_table;//记录还有多少内存可用
    list<Memorycell *> m_used_table;//有多少被使用了
}

memory cell

class Memorycell {
private:
    uint m_start;//cell从内存的哪里开始使用
    uint m_end;//到哪里结束
    ╱**
    *cell的数量,每个Cell占8字节
    */
    uint m_size;
}

内存池快被用完了或者已经被用完了该怎么办?两种策略

  1. 进行内存的自动扩容
  2. 进行垃圾回收

GC算法

  • 标记-清除算法:内存碎片化严重
  • 标记-整理算法:会有stw(stop the world)问题
  • 分代收集理论
  • 标记-复制算法
  • 基于Region的GC算法

内存池、GC算法、Java之间的关系

  • JVM的内存结构是由它需要支持的GC算法决定的
  • 根据GC算法理论,JVM的内存结构需要这样设计

GCroot对象,可达性分析

  • 方法区中静态变量引用的对象
  • 方法区中常量引用的对象
  • 虚拟机栈中的局部变量表中引用的对象:把当前还在用的对象及对象引用的对象给扫出来

  • 本地方法JNI引用的对象

Volatile

JVM三大执行引擎

  • 字节码解释器
  • 模板解释器
  • JIT优化技术

证明可见性

public class Test3 i
    //编译成java字节码
    public static volatile int found = 0;
	public static void main(String[] args){
        new Thread(()-> {
            System.out.println("筹基友送笔来...");
            while (e == found){}

            System.out.println("笔来了,开始写字...");
        },"我线程").start();

        new Thread(()->{
            try {
                Thread.sleep(2000);
            }catch (InterruptedException e){
                e.printstackTrace();
            }
            System.out.println("基友找到笔了,送过去...");
            change();
        },"基友线程").start();
    }
	public static void change(
    	found = 1;
    }
}

volatile是如何保证线程的可见性的? 加volatile,会写主内存

执行引擎的一般的结构

while() {
    switch() {
    	case getstatic:
   		break;
   		
    	case putstatic:
    	break;
    }
}

读volatile修饰的变量getstatic

CASE(_getstatic):
    if (cache->is_volatile(){
    .....
    if (tos_type =- atos) {
    ....
    }else if (tos_type == itos){
    SET_STACK_INT(obj->int_field_acquire(field_offset), -12;
}

每次读volatile修饰的共享变量都会从主内存中读取,然后在工作内存中生成一个副本

错误认知:读取volatile修饰的共享变量时会去判断工作内存中是否有这个变量,没有就拷贝生成副本,有就直接用

#define SET_STACK_INT(value,offset)
	(*((jint*)&topOfStack[-(offset)]) = (value))
	

inline jint oopDesc::int_field_acquire(int offset) const {
	return OrderAccess::load_acquire(int_field_addr(offset));
}

inline jbyte OrderAccess ::load_acquire(volatile jbyte* p){
	return*p;
}

这种认知是错误的

第一次读的时候,从主存中读取,然后在工作内存中生成副本以后读,会先判断工作内存中是否有该数值。栈只有入栈和出线,那么该如何去判断栈中有没有这个数值?

写volatile修饰的变量putstatic

内存间交互操作

  • lock(主内存,将一个变量标识为线程独占)
  • unlock(主内存,释放一个处于锁定状态的变量)
  • read(主内存,将变量从主内存传输到工作内存)
  • load(工作内存,将read读到的值在工作内存中生成一个副本)
  • use(工作内存,将工作内存中的变量传递给执行引擎)
  • assign(工作内存,将执行引擎返回的值赋值给工作内存中的变量)
  • store (工作内存,将工作内存中变量的值传输给主内存)
  • ·write(主内存,将store操作从工作内存中传输的值写入主内存的变量中)

image-20200926224803563

openjdk源码

CASE(_putfield):
CASE(_putstatic):
{
//Now store the result
......
int field_offset = cache->f2_as_index();
if (cache->is_volatile(){//判断是不是volatile
    if (tos_type == itos){//是不是int类型的
        obj->release_int_field_put(field_offset,STACK_INT(-1));
    }
    .....
OrderAccess::storeload();内存屏障

1、这边写的是哪个区域:1CPU缓存?2主内存?3工作内存? 2、解释概念︰主内存、工作内存

inline void OrderAccess::release_store(volatile juint* p,juint v){
*p = v;
 }
 
inline void OrderAccess: :storeload{
	fence();
}

inline void OrderAccess:: fence(){
if (os: :is_MP()){
	//always use locked addl since mfence is sometimes expensive
 #ifdef AMD64
	__asm__ volatile ("lock; addl $0,0(%%rsp)": : : "cc" ,"memory");
 #else
	__asm__ volatile ("lock; addl $0,0(%%esp)": : : "cc","memory");
 #endif
	}
}

image-20200926225831924

inline void oopDesc::release_int_field_put(int offset,jint contents){
	OrderAccess::release_store(int_field_addr(offset), contents);
}

cas是原子性操作,会交换8字节 是一个底层的指令,所以会保证原子性

image-20200926225940849

  • 每次都会从主内存中取数据

  • 理解volatile,核心是理解volatile的写

关于内存的几个名词

  • 操作系统层面:操作系统内存、本地内存、native memory、OS内存
  • JVM内存模型层面:堆、方法区、虚拟机栈、本地方法栈、程序计数器
  • Java内存模型层面:主内存、工作内存

指的都是操作系统内存,只不过在不同的场景下面它的名词不一样而已

操作系统层面:内存条,物理内存

JVM内存模型,事实上就是操作系统的一个内存池,而jvm有将这个内存池划分成了各个区域,赋予了新的名词

主内存=堆区+方法区

工作内存=虚拟机栈

JVM内存模型:工作内存、主内存

JMM:java内存模型,在多线程方面。书===JSP-133规范,JMM是基于它的实现

volatile是如何保证有序性的

禁止指令重排

你能不能给我看看进制指令重排长啥样子?

加、不加volatile,你们觉得字节码是不是一样的?一样的

加volatile

0 iconst_1
1 putstatic #11 <com/qimingnan/concurrent/Test3.found>
4 return

不加volatile

0 iconst_1
1 putstatic #11 <com/qimingnan/concurrent/Test3.found>
4 return

那JVM是如何知道我写的是volatile修饰的变量?

内联汇编

asm__volatile ("lock; addl $0,0(%%rsp)" : : : "cc","memory");
即是编译屏障

既是编译屏障

也是内存屏障

class字节码文件

access flag  这个属性
cache->is_volatile()  //会先判断这个变量是不是被volatile所修饰

没加volatile

image-20200927222335471

加了volatile的变量

image-20200927222420377

指令重排

  • 编译期指令重排:

    1. Java字节码层面(没有做)
    2. openJDK源码层面(做了gcc -02这个优化)有些代码我不希望编译器做优化,需要编译器给我提供策略—>编译屏障
  • 运行期指令重排

    ​ 1. CPU乱序执行带来的(DCL+volatile来举例)

DCL为什么要加volatile?

为什么new不是原子操作?

new的四步操作(正常流程,没有被乱序执行)

33 new #4 <com/qimingnan/concurrent/DCLTest>
	在堆区(或TLAB)分配内存,生成一个不完全对象(空壳子)
	将不完全对象的引用压入栈顶
36 dup	
	复制栈顶元素
	将复制的数值压入栈顶(作为this指针来使用)
37 invokespecial #10 <com/qimingnan/concurrent/DCLTest.<init>>
	执行构造方法,this指针
	堆区(TLAB)中的对象就是一个完整的对象(执行了构造方法)
40 putstatic #3 <com/qimingnan/concurrent/DCLTest.instance>
	将完整对象的引用赋值给方法区的共享变量instance

因为40跟37之间没有任何的联系,所以有一定的概率会发生指令重排。

有的线程拿到的对象引用其实是不完全对象的引用

这就是DCL+volatile的原因

CPU乱序执行,带来运行期的指令重排现象

  • 有的代码可以乱序执行
  • 有的代码不能乱序执行

有一个保证:as-if-serial语义

跟在单线程环境下的运行结果是一样的

int a = 0;
int b = 10;
a = 20;
int sum = a + b;

不能是这样的
int a = 0;
int b = 10;
int sum = a + b;
a = 20;

happens-before原则

JVM在设计的时候,有些逻辑的先后顺序是可以执行的,那在JVM实现的那刻,就内置在JVM中了。

比如:new的执行要先于finalize方法前执行完成

有些逻辑是无法提前知晓的

需要JVM提供机制,由程序猿根据业务去控制

这就引出了:内存屏障(jvm是借助cpu提供的机制封装实现出来的)

  • 编译屏障(编译期的指令重排)
  • 内存屏障(运行期的指令重排)

cpu的指令有:

  • fence族:sfence、lfence、mfence(串行化读写,队列化读写)
  • lock指令(JVM采用的)
    1. 领定地址总线
    2. 保证读写有序性

内存屏障锁的就是地址总线

image-20200927224701703

内存屏障作用于CPU内部

为什么要设计内存屏障?

  • 因为CPU写内存的机制(会有一定的延迟)
    1. write through异步写(cpu的主流做法)
      1. CPU讲写请求写入store buffer中
      2. CPU空闲的时候将写的数据输入内存
    2. write back 同步写
      1. CPU将写请求写入store buffer,然后再刷回内存

image-20200927224922225

加了内存屏障,保证了读写的有序性,异步写,达到了同步写的效果,这就是为什么需要内存屏障?内存屏障作用于CPU内部,内存屏障的两个功能

Synchronized

Synchronized锁的一定是对象,而不是代码块。锁加在了对象(对象头,实例数据、对齐填充组成)的对象头

使用jol工具包可以打印一个对象的信息

System.out.println(ClassLayout.parseInstance(obj).toPrintable());

对象头中有什么?(由12个固定长度的字节组成)

  • Markword(64b)
  • Klass pointer(32b)

image-20200928212621752

32位的结构

image-20200928212650291

对象有一些什么状态

  1. 偏向锁
  2. 轻量锁
  3. 重量锁
  4. 无锁
  5. GC标记

由源码得知

image-20200928215136411

image-20200928215229698

java虚拟机默认开启了指针压缩

  • 垃圾回收开销变小
  • cpu执行效率提高

cpu存储又分为大端(高地址高字节)和小端(intel存处理器使用 高地址低字节(字节码是反着来的))

image-20200928220130573

hashcode真的存在吗?(用于确定对象的地址)

为什么分代年龄一但超过15就会被放入老年区(分代年龄占4位,最大表示到15)

锁膨胀是可逆的(批量撤销)