一.内存区域:
1.内存组成:
1)Java虚拟机栈: 线程私有,生命周期与线程相同。每个方法执行时会有栈帧在栈里负责进栈出栈之中的信息。方法执行完就出栈。存放基本类型数据,对象引用类型。局部变量表所需的内存空间在编译时完全确定,运行时不会改变
2)JAVA堆: 所有线程共享,存放对象实例(new 出来的)。Java回收新生代,老年代指的就是堆。
3)方法区:所有线程共享,存储常量,静态变量等
2.new对象时: Java虚拟机先分配内存。之后就是JAVA程序执行构造方法
3.Java程序通过栈上的引用数据来操作堆上的具体对象
4.内存泄漏: 对象该回收的没有回收
内存溢出:都是该存活的对象,空间小了
二. 垃圾收集器:
1.判断对象是否存活:
1)引用计数法: 一个对象,当一个地方引用它时,计数器加1,引用失效时,计数器减1. 很难解决循环引用问题
2)可达性分析算法: 从root开始,找引用链,在引用链的才是应该存活的对象。
Root: 虚拟机栈引用的对象/方法区中静态属性引用的对象/方法中常量区引用的对象/Native方法引用的对象
2.垃圾回收算法:
1) 标记 - 清除算法: 先标记,标记完成后统一清除
2) 复制算法: 将整个内存分为两块,每次只用其中的一块,当这一块内存用完了,将活的对象复制到剩下的内存上,将原内存里的对象删除
3)分代收集: 根据各个年代不同特点采取不同的策略。存活少的对象用复制算法,对象存活率高的用标记算法
3.内存分配与回收策略:
1) 对象优先在新生代区分配,如果新生代内存不够,那么发起新生代内的GC(Minor GC)
2)大对象直接分配到老年代(很长的字符串及数组)。尽量少用短命大对象,用参数可以设置,这样避免了新生代大量的复制
3)长期存活的对象将进入老年代,每经过一次新生代的GC,标记年龄加一,加到默认的15,就会进入老年代
三. 类的文件结构:
1.各种不同平台的虚拟机与所有平台都统一程序存储格式----字节码是构建平台无关性的基石
四.虚拟机类加载机制:
1.虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
2.Java语言里,类的加载,连接和初始化过程都是在程序运行期间完成的
3.类生命周期: 加载 ---> 连接(验证-->准备-->解析) --->初始化 --->使用--->卸载
1)准备阶段: 为静态变量在方法区分配内存,并将非final属性赋值为0等默认初始值,对于静态final字段(基本类型以及String)设置为设置的值
2)初始化:开始真正执行类中定义的Java程序代码,即构造方法代码,赋程序的初始值
3)类的实例变量会在对象实例化时随着对象一起分配到Java堆中
五.类加载及执行子系统:
1.Web服务器解决的问题:
1)第三方类库在同一服务器的两个Web应用间可共享
2)第三方类库在同一服务器的两个Web应用间可相互隔离(不同版本)
2.Tomcat目录结构:
1)可被Tomcat和所有Web应用程序共同使用
2)类库可被Tomcat使用/对所有Web应用程序不可见
3)可被所有web应用程序使用
4)对Tomcat不可见/仅被此Web应用程序使用
六.Java内存模型与线程:
1.Java内存模型规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存(working memory,是cache和寄存器的一个抽象),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝(大对象不是全拷贝,只是用到的字段等),线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。364
2.工作顺序:
1)变量从主内存到工作内存: read (从主内存读到工作内存)--> load(写入到工作内存)
2)上解锁: lock --- unlock 作用于主内存中的变量,标识为一条线程的独占状态/将一个处于锁定状态的变量释放出来
3)读写工作内存变量: use --- assign 作用于工作内存的变量,将一个工作内存的变量的值传递给引擎/将引擎接收的值赋值给工作变量
4) 变量从工作内存写回主内存: store --- write 作用于工作内存变量,将工作内存的一个变量值传递回主内存---作用于主内存变量,将工作内存得到的变量值写回到主内存的变量中。
3.操作规则
1) read - load / store - write必须顺序,但可不连续。且必须成对出现
2) 变量在工作内存值改变了必须同步到主内存中,且只有在值改变的情况下才会被同步
3) 一个变量同一时刻只允许一条线程对其进行lock操作
4)一个新变量只能在主内存中诞生,然后拷贝到工作内存中
5) 在执行unlock操作操作前,必须把此变量同步回主内存中
4.volatile变量:
1) 可见性: 当一条线程修改了这个变量的值,新值对于其它线程来讲是可以立刻得知的,因为每次线程使用它之前会刷新(普通变量通过回写主内存才能够实现)
2)但是还是会出问题,因为java运算非原子操作,即在刷新时是最新值,但线程1计算过程中可能线程2已经把那个变量更新了。一个简单的++语句在字节码中是四句:取到栈顶(最新),iconst_1,iadd,出栈
3)适用范围:
a. 运算结果并不依赖当前值,或者确保只有单一线程修改变量值(set方法)
4)虚拟机执行时重排序优化是指对应的汇编代码可能提前执行。volatile可以禁止指令重排序化,多线程时可保证变量赋值操作的顺序与程序代码的执行顺序一致。普通变量只保证在该方法的执行过程中所有依赖赋值结果的地方都能取得正确的结果。 ---> 一个线程A做完某事改flag,另一个线程等着那个flag变,后用A的结果,如果改flag提前到做事之前,那么B就拿不到正确的结果
原理是被volatile修饰的变量,被赋值后会有一个lock操作,使重排序时不能把后面的指令重排序到lock之前。那个lock回去执行一次store write操作,写入内存,这样保证对其它变量可见。付完值就写一次,且保证顺序
5) CPU重排序: 根据指令的依赖情况以保证处理结果,相互依赖的顺序一致。而lock的作用就是lock之前的可以重排,lock之后的可以重排,但不能跨lock重排
6)规则:
a.工作内存每次要用(use)volatile变量时,必须先从主内存刷新最新值(即执行load操作)
b.工作内存每次修改(assign)volatile变量时,必须立刻写回(store+write)到主内存中
c. 多个volatile变量,相同操作的顺序相同,不会因为重排序改变
7)普通变量与volatile变量的区别是,volatile变量保证了新值能够立刻同步到主内存,以及每次使用前从主内存刷新
5.先行发生原则: 与真正的时间顺序无关
1)对同一个锁,unlock先发生于原来后面的lock,unlock
2)volatile变量的写操作先于对这个变量的读操作
3)Thread的start先于此线程的每一个动作
4)线程中所有操作都在线程终止之前发生
5)A动作先于B,B先于C,那么A先于C
6)在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
6. as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。
7. 内存的数据不是想要的造成的危害,没有写回,或者因为单线程内部顺序重排导致写回时间不合适(早或者晚)8.Java线程调度:
1)协同式调度: 一个线程自己执行,执行完了通知系统切换到另一个线程
2)抢占式:完全由系统分配时间
七.线程安全与锁优化: 408
1.不可变的对象(final)一定线程安全
2.线程安全的实现方法:
1)互斥同步: 在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用。使用信号量的时候,可以为一些线程。 实现其的主要方式是临界区,互斥量和信号量。悲观策略
2)非阻塞同步:先进行操作,没有冲突,很好。出现冲突,再用其它补偿措施(不断重试)。乐观策略
3)无同步方案:
a.可重入代码: 本身线程安全,例如无共享数据等
b.线程本地存储:共享数据操作都在一个线程内 ThreadLocal
3.synchronized关键字:
1) 如果JAVA程序中synchronized修饰明确指定了对象的参数,那么锁定与解锁的对象就是此对象;如果没有明确指定,那就看修饰的是实例方法还是类方法,去取对应对象的实例或Class对象来作为锁对象。最后锁的都是对象
2)synchronized同步块对同一条线程来讲是可重入的
3)同步块在已进入的线程执行完之前,会阻塞后面其它线程的进入。
4) 一个对象内部只有一个synchronized锁
4.ReentrantLock关键字: 新特性
1) 等待可中断: 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他的事情。
2) 公平锁: 多个线程等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁(之前是都有机会)。 可设置,默认不公平
3) 绑定多个条件: 一个ReentrantLock对象可以同时绑定多个Condition对象