本文是私人MEMO,仅仅是大纲作用。因为时间关系没有整理排版,有时间会进行重写和扩充。
类加载过程为:加载、连接(包括验证、准备、解析)、初始化,类加载在Java中是在程序运行阶段完成的,这也使得Java程序更灵活。
加载:1、通过全限定名获得这个类的二进制字节流
2、将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象
连接:1、验证,保证Class文件中字节流符合JVM要求,不会危害到JVM
2、准备,为类变量分配内存并且初始化变量值为零值
3、解析,JVM将常量池内的符号引用替换为直接引用
初始化:执行类中定义的程序代码<clint>方法
1、启动类加载器(bootstrap classloader) 负责加载<JAVA_HOME>\lib目录
2、扩展类加载器(Extension ClassLoader) 负责<JAVA_HOME>\lib\ext目录
3、系统类加载器(Application ClassLoader) 负责加载用户路径(ClassPath)上指定类库
4、自定义类加载器
双亲加载机制:一个类加载器收到了类加载器的请求,首先不会自己尝试加载这个类,而是委派给父加载器完成,每一层都是如此,只有当父加载器反馈自己无法完成这个加载请求,子加载器才会尝试自己加载。
这种加载机制保证了Java程序文档运行,使得jdk的类在各个类加载器环境下都是同一个类。
可以通过重写loadClass()破坏双亲委派机制,最好不破坏,把类加载逻辑写在findClass().
Java内存区域。
线程共享的区域:
1、方法区 存放类信息、常量、静态变量、即时编译器编译后的代码等数据 会抛出OOME
2、堆 存放对象实例 几乎所有对象实例都在这里分配内存 会抛出OutOfMemoryError
线程不共享的区域:
1、程序计数器 保证线程切换之后能切换回去 没有规定Error
2、Java虚拟机栈 每个方法运行时维护一个栈帧 其中的局部变量表所需内存在编译期间完成分配 有SOME OOME
3、本地方法栈 本地方法的执行栈 SOME OOME
判断对象不可达的方法:
1、从根结点开始对可能被引用的对象进行标记,未被标记的对象就是需要回收的对象。
GC roots的对象包括1、JVM(栈帧中的本地变量表)中引用的对象 2、方法区中静态属性引用的对象 3、方法区中常量引用对象 4、本地方法栈中JNI引用对象
2、引用计数算法,引用次数为0则应该被回收(无法保证互相引用的对象被回收);
分代GC特点1、对象优先在Eden分配,2、大对象直接进入老年代,3、长期存货对象进入老年代
(age计数,相同年龄总和大于等于Survivor空间一半 大于等于该年龄直接进入老年代)
4、空间分配担保 晋升空间Survivor中所有对象大小 不够则计算历届晋升的老年代对象平均大小 如果HandlePromotionFailure设置为true,根据情况进行Minor GC 放不下晋升对象则Full GC 否则直接Full GC。
逃逸分析
当一个对象在方法中被定义时,可能被外部方法所引用(比如通过参数传递到其他方法),称为方法逃逸。有的对象还会被外部线程访问到,称为线程逃逸。
如果能证明不会发生逃逸,则JVM可以做出以下优化:
1、栈上分配 如果能证明一个对象不会逃逸出方法之外,则可以将对象直接在栈上分配,对象占用的内存空间可以随栈帧出栈而销毁。这可以有效减小GC压力。
2、同步清除 线程同步是一个比较耗时的操作,如果能确定一个变量不会发生线程逃逸,那么这个变量的同步措施就可以消除。
3、标量替换 标量就是一个数据无法再被分解成更小的数据。如果证明一个对象不会被外部访问,并且可以被拆散,就直接创建若干个成员变量而不是这个对象来代替。
缓存问题
计算机的存储设备和处理器之间的运算速度有几个数量级的差距,所以现代计算机系统不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存cache来作为内存和处理器之间的缓冲。
将运算需要的数据复制到缓存中,让运算能够快速进行,当运算结束再从还从将数据同步到内存,让处理器无需等待缓慢的内存读写。
这样也带来了一个问题,缓存一致性问题
另外处理器会对输入代码进行乱序执行优化,处理器会在对执行结果重组,保证其与顺序执行结果一致。
因此代码执行的顺序性不能靠代码的先后顺序来保证。JVM的JIT也存在指令重排优化。
Java内存模型
Java内存模型规定了所有的变量存储在主内存,每条线程有自己的工作内存,线程对变量的所有操作必须在工作内存中进行,不能直接读写主内存中的变量(volatile也不例外,只是由于其特殊的操作顺序,看起来像是直接从主内存读写访问一般)
volatile型变量的特殊规则
volatile时JVM提供的最轻量级的同步机制,有一些特殊的访问规则。
1、保证此变量对所有线程的可见性。 这里可见性是指一个线程修改了这个变量的值,新值对于其他线程来说是可以立马得知的。
普通变量无法做到这一点,因为普通变量的值在线程之间传递需要通过主内存来完成。
volatile变量在各个线程中是一致的,在每个线程的工作内存中可能存在该变量不一致的情况,但是每次使用之前都要刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题。
volatile只能保证可见性,对于不是以下情况还是应该使用其他锁:
a、运算结果不依赖变量当前值,或者能确保只有单一线程能修改变量的值
b、变量不需要与其他的状态变量共同参与不变约束
2、禁止指令重排优化。 普通的变量仅仅会保证在该方法的执行过程中所有赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序和程序代码执行顺序一致。单线程程序中这够了,也就是线程内表现为串行的语义。
但是在多线程环境中,可能会导致出现一些奇奇怪怪的现象。
volatile修饰的变量赋值会多一个lock前缀操作,相当于一个内存屏障,使得重排序时不能把后面的指令重排序到内存屏障之前。
多个CPU就需要内存配置来保证一致性,这个lock前缀操作可以保证本CPU的Cache写入内存,该写入内存会引起其他CPU或者内核的Cache无效化,也就使得volatile变量的修改对其他CPU立即可见。
volatile读操作和普通变量差不太多 写稍微慢点
long double之外的基本数据类型都具有原子性 当然现在大多数JVM已经实现为原子性了。
Java内存模型特征:原子性、可见性、有序性
1、原子性 基本数据类型(double和long不要太刻意在意)读写都是具备原子性的 对更大范围的原子性保证,使用锁代码块。
2、可见性 可见性是指一个线程修改了共享变量的值,其他线程能立即得知这个修改。这个的实现方式是通过变量在线程工作内存修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种以来主内存作为传递媒介来实现可见性的。
这个地方volatile和普通变量的区别在于volatile的特殊规则保证了新值能立即同步到主内存,每次线程用之前从线程工作内存中刷新。synchronized和final也可以保证可见性。
3、有序性 在一个线程内观察自己的操作,所有的操作都是有序的,在一个线程中观察另一个线程,所有操作都是有序的。
前半句话,因为线程内表现为串行的语义;
后半句话,因为指令重排序现象和工作内存与主内存同步延迟现象。
volatile和synchronized保证了操作的有序性,volatile保证禁止指令重排序。而synchronized则是由于锁的特性,保证了同一个锁的两个同步快只能串行进入。
happens-before
如果A先行发生于B,那么B之前,A产生的影响能被B观察到。
下面是天然的happens-before:
1、在一个线程内,按照代码执行顺序,控制流前的操作先行于后面的操作;
2、同一个锁的lock操作先行于unlock操作;
3、对于一个volatile的写操作先行于这个volatile变量的读操作;
4、Thread对象的start()方法先行于此线程任何动作;
5、线程中的任何操作都先行于此线程的终止检测(比如Thread.join()和Thread.isAlive());
6、对线程interrupt()方法的调用先行于被中断代码(比如Thread.interrupted()方法返回是否中断并且重置中断位)检测到中断事件的发生;
7、一个对象的初始化完成先行于它的finalize()方法的开始;
8、传递性 A happens-before B Bhappens-before C,则Ahappens-before C。
Java中的线程实现
线程是比进程更轻量级的执行单位。进程是资源共享(内存地址、文件I\O)的最小单位,而线程是CPU调度的基本单位。
java中线程的关键方法都是Native的,实现方式有三种 内核线程、用户线程、用户线程加轻量级进程。
Thread.suspend()和Thread.resume()被废弃的原因:如果suspend()中断的线程是即将要执行resume()尝试去恢复的线程就会产生死锁。
synchronized和ReentrantLock的区别和相似:
都具有可重入特性,一个是API层,一个是原生语法层。
ReentrantLock比synchronized增加了一些高级特性:
1、等待可中断 tryLock()获取锁的过程中可以设置延时 延时结束还没获取到锁则返回false,如果不设置延时获取不到锁直接返回false。
2、可实现公平锁(默认非公平,构造函数传入boolea表示是否是公平锁) tryLock()会导致不公平
当设置true ,在争用下,锁有利于授予访问最长等待的线程。 否则,该锁不保证任何特定的访问顺序。 使用许多线程访问的公平锁的程序可能会比使用默认设置的整体吞吐量(即,更慢,通常要慢得多),但是具有更小的差异来获得锁定并保证缺乏饥饿。
但是请注意,锁的公平性不能保证线程调度的公平性。 因此,使用公平锁的许多线程之一可以连续获得多次,而其他活动线程不进行而不是当前持有锁。 另请注意, 未定义的tryLock()方法不符合公平性设置。 如果锁可用,即使其他线程正在等待,它也会成功。
3、锁可以绑定多个条件(reentrantLock.newCondition()绑定多个条件 不同的条件使用不同的锁)。
私人MEMO--JVM与并发
最新推荐文章于 2024-07-12 10:44:44 发布