Java虚拟机HotSpot笔记

1 篇文章 0 订阅

第2章 Java内存区域与内存溢出异常

内存划分:a和b是由所有线程共享,其它是线程隔离的 

a.方法区:类信息、常量、静态变量、jit即时编译器编译后的代码等,习惯称为永久代PermGen,但并不等价,这块内存的回收主要针对常量池的回收和对类的卸载。在1.7的HotSpot中已经将字符串常量池从永久代移出; -xx:MaxPermSize=10M

a1.运行时常量池:方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池。String类的intern()方法就是一个运用,jdk1.6中会把首次遇到的字符串实例复制到永久代,1.7开始“去永久代“,不会再复制实例,只是在常量池中记录首次出现的实例引用。

b.堆:对象实例和数组,分为新生代和老年代,新生代易被回收   -Xmx最大 -Xms初始值

c.虚拟机栈:存储栈帧,其包括 > 局部变量表:基本数据类型、对象引用、返回类型   -Xss,可能抛出两种异常;操作数栈;动态分配;方法返回地址。

d.本地方法栈:类似栈,为Native方法服务 

e.程序计数器:当前线程所执行的字节码的行号指示器,每条线程都有一个程序计数器

除了程序计数器,其它都会OOM。一般栈深度能有多少栈帧呢,测到了一个为9782,不过这个是变的,局部变量表内容越多,栈帧越大,栈深度就越小,可以通过 -Xss2m设置栈大小。

Java GC、新生代、老年代http://www.cnblogs.com/yydcdut/p/3959711.html

对象创建的过程:

1.检查方法区常量池是否有这个类的符号引用,如果没有则进行类的加载过程

2.为新生对象分配内存。

3.将分配的内存空间初始化为零值。

4.设置对象头,比如这个对象时哪个类的实例,对象的哈希码等。

5.执行<init>方法,按照程序员的意愿进行初始化。

对象内存布局:

1.对象头:根据虚拟机位数不同长度为32位或者64位。分为两部分,a部分(Mark Word)存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等,b部分类型指针,就是对象指向他的元数据的指针,通过这个指针来确定对象是哪个类的实例,如果是数组对象还会有额外的部分存储数组的长度。

2.实例数据:对象真正存储的有效信息,程序中定义的各种类型的字段内容。

3.对齐填充:不是必然存在的,vm要求对象起始地址必须是8字节的整数倍,对象头是8字节的1或者2倍,所以实例数据没有对齐时,通过对齐填充来补全。

对象的访问定位:

1.句柄访问

2.直接指针,hotspot采取的方式

第3章 垃圾收集器与内存分配策略

1.程序计数器、虚拟机栈、本地方法栈随线程而生,随线程而亡。

2.java堆和方法区则不一样,只有在程序运行期间才能知道会创建哪些对象,所以GC就是说的这部分的内存。

对象已死吗?

引用:??如果存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。

1.引用计数算法:弊端是无法解决互相引用的问题

2.可达性分析算法:通过一系列称为"GC Roots"的对象作为起始点,当一个对象到GC Roots没有任何引用链相连时,则可回收。

可作为root的对象:

a.虚拟机栈(栈帧中的本地变量表)中引用的对象

b.方法区中类静态属性引用的对象

c.方法区中常量引用的对象

d.本地方法栈JNI引用的对象

如何拯救一个对象?

复写finalize()方法,并在方法中重新建立静态变量引用链,不过finalize方法只会被系统自动调用一次,第二次gc时则会被回收掉。不建议复写这个方法去关闭资源,使用try-finally会更及时更好。

垃圾收集算法:

1.标记-清除算法:先标记,后统一清除。有两个缺点,效率问题和内存碎片问题

2.复制算法:容量划分为两块,每次使用一块,当这一块的内存使用完了,就将存活的对象复制到另一块,再把已使用的这一块一次性清理掉,这样的代价就是将内存缩小为了原来的一半。HotSpot将内存分为一块较大的Eden空间和两块较小的Survivor空间,比例为8:1:1,每次使用Eden和其中的一块Survivor1,回收时将存活的对象一次性复制到另一块survivor2上,将Eden和Survivor1清理掉,所以每次新生代可用内存为整个新生代容量的90%。若最后存活的超过10%,则需要老年代进行分配担保。

3.标记-整理算法:标记过程和1一样,但后续不是直接清理对象,而是让所有存活对象都向一端移动,然后清理掉边界以外的内存

4.分代收集算法:java堆分为新生代和老年代,新生代选用2,老年代选用1或者3.

Hotspot算法实现:

OopMap:记录引用的地址,只在安全点有记录

安全点:只有在安全点才能STW

中断方式:

        a抢先式中断:gc发生时,将所有线程中断,发现有线程不在安全点上则恢复线程,让他跑到安全点,一般不采用

       b主动式中断:gc时设置一个标志,各个线程去轮询这个标志,为真时就中断挂起,轮询标志的地方和安全点是重合的

安全区域:sleep和blocked状态的线程不能轮询标志,所以有安全区域,这一段代码中引用关系不会变化。线程执行到安全区域后首先标识自己已经进去安全区,线程要离开安全区时检查是否已经gc完成,如果完成了就继续执行,如果没有就等待信号才执行。

Minor GC & Major GC & Full GC:
1.Minor GC触发条件:
当Eden区满时,触发Minor GC。
2.Major GC触发条件:
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
3.Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

垃圾收集器:

Serial收集器:单线程收集器,需要STW,分为Serial(复制算法)/Serial Old(标记-整理算法)收集器分别回收新生代和老年代,适合Client模式下使用。jdk1.3之前唯一的选择

ParNew收集器(Parallel并行):Serial收集器的多线程版,也需要stw,适合Server模式下使用

Parallel Scavenge(打扫)收集器:新生代收集器,复制算法,并行的多线程收集器,目标为达到一个可控制的吞吐量,充分利用cpu时间。和ParNew收集器两者都是复制算法,都是并行处理,但是不同的是,paralel scavenge 可以设置最大gc停顿时间(-XX:MaxGCPauseMills)以及gc时间占比(-XX:GCTimeRatio)gc日志中它对应的新生代称为”PSYongGen“,1.7,1.8中就是用的这个。

CMS(Concurent Mark Sweep)收集器:jdk1.5发布的,获取最短回收停顿时间为目标的收集器,采用“标记-清除”算法,收集老年代。适合使用在互联网网站的服务端,工作过程如下:

a.初始标记:stw,标记gc roots直接关联到的对象,时间特别短

b.并发(gc、用户线程同时执行)标记:gc roots tracing过程

c.重新标记:修正并发期间已经变动的那一部分对象的标记记录

d.并发清除:清除

由于耗时较长的bd步骤都是并发执行的,所以比较优秀。

cms缺点:

a.因为并发所以对cpu资源非常敏感

b.无法处理浮动垃圾(因为并发所以可能有新的垃圾产生)而导致一次Full GC的产生。其它收集器都是老年代几乎被填满了才开始运作,由于cms并发gc时用户线程也需要资源,所以cms在jdk 1.5老年代使用了68%的空间就会运作起来,1.6升至92%,这个时候假如内存无法满足程序运行,就会启动后备预案:临时启动Serial Old收集器重新进行老年代的收集。

c.内存碎片化,顶不住Full GC时开启内存碎片合并整理过程,这个过程是无法并发的,所以要停顿。

G1收集器:

特点:

a.并发并行,多条gc线程和用户线程同时执行

b.分代收集

c.空间整合,和cms相比采用了“标记-整理“算法,减少了碎片

d.可预测的停顿,允许使用者指定一个长为M的时间段内消耗在gc上的时间不得超过N。原理为化整为零分治思想,将整个java堆划分为多个大小相等的独立区域Region,避免全区域的gc,跟踪各个Region里面垃圾堆积的价值大小维护一个优先列表,每次根据允许的回收时间优先回收价值最大的区域。

内存分配和回收策略:

a.对象优先在新生代的Eden区域分配

b.大对象直接进入老年代,前提要设置-XX:PretenureSizeThrehjold参数

c.长期存活的对象进入老年代:每个对象有一个年龄计数器,在Eden出生,逃过一次Minor GC并进入Survivor空间后年龄就+1,增加到一定程度(默认15岁)后就进入老年代。

d.对象动态年龄判断:并不一定要到15岁,Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于此年龄的对象进入老年代

e.空间分配担保:简单说就是为了保证安全,Minor GC之前需要老年代进行担保,无法担保的话就转为Full GC

第6章 类文件结构

Class文件是一组以8位字节为基础的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。构成部分只有两种数据类型,整个Class文件本质上就是一张表:

无符号数:以u1、u2、u4、u8分别代表1248个字节的无符号数

表:多个无符号数或者其他表作为数据项构成的复合数据类型,以”_info“结尾

整个内容如下:

魔数:0xCAFEBABE

常量池:主要存放字面量(文本字符串,声明为final 的常量值等)和符号引用(类和接口全限定名、字段名称和描述符、方法名称和描述符)。

方法调用指令:

invokevirtual:对象实例方法

invokeinterface:接口方法

invokespecial:需要特殊处理的实例方法,包括实例初始化方法私有方法和父类方法

invokestatic:类static方法

指令:

iload_0:从局部变量表 Slot0 中装载int类型值入栈

istore_2:将栈顶int类型值保存到局部变量2中。

astroe_0:将栈顶引用类型值保存到局部变量0中

https://www.cnblogs.com/longjee/p/8675771.html

第 7章 虚拟机类加载机制

javac代码编译的结果是从 .java 源文件转为 .class 字节码文件。虚拟机类加载机制就是把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,在java语言里这些过程都是在程序运行期间完成的,这样会使类加载时增加一些性能开销,但是保证了高度的灵活性。

类的生命周期:

加载-验证-准备-解析-初始化-使用-卸载,其中验证准备解析三个过程统称为 >连接。

类还未被初始化时,一定会被"初始化"阶段(指上面其中的一个阶段)的5个场景:

a.new 关键字,读取或者设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外,已经被传播优化,放在了自己的常量池中),调用一个类的静态方法时

b.对类进行反射调用时

c.如果父类还没初始化,触发父类的初始化

d.包含main方法的那个主类,就是直接使用java.exe命令来运行某个主类

e.1.7动态语言支持,巴拉巴拉

这五种行为称为对一个类的主动引用,像被动引用的方式就不会对类进行初始化,比如通过子类访问父类的静态变量,子类就不会被初始化。对于接口而言,并不要求父接口都完成了初始化,只有被访问时才初始化父接口。

1.加载:加载阶段还没结束,验证阶段可能已经开始了

a.通过一个类的全限定名来获取定义此类的二进制字节流(其实从zip包、网络、文件获取都是可以的,很灵活)

b.将字节流代表的静态存储结构转化为方法区运行时数据结构

c.在内存中生成一个代表这个类的java.lang.Class对象(对于HotSpot来说这个对象存在方法区),作为方法区这个类的各种数据的访问接口

2.验证:因为加载类时的源头可能有很多,很难保证安全

a.文件格式验证,这个是基于二进制流进行的,例如文件头魔数等等,经过这个阶段验证才会进入方法区存储

b.元数据验证,进行的是语义的检验,比如是否继承了final类,类中字段方法是否和父类冲突等

c.字节码验证,校验语义合法,符合逻辑,例如把一个对象赋值给一个毫无相关的数据类型

d.符号引用校验,发生在虚拟机将符号引用转化为直接引用的时候

3.准备:

为类变量分配内存并设置类变量初始值(通常情况下是零值,如果是final的则在准备阶段直接就赋值为指定值),这些变量将在方法区分配,而实例变量是在对象初始化时分配在java堆中。

4.解析:

符号引用替换为直接引用。

符号引用:以一组符号可以无歧义的描述定位到目标即可  

直接引用:指向目标的指针,或者可以间接访问到目标的句柄。

5.初始化:执行类构造器<clinit>方法的过程

在准备阶段已经为变量赋值过一次初始值,初始化阶段则按照程序员的意愿类初始化变量和资源

<clinit>方法:

a.由编译器自动收集类的所有类变量和静态语句块中语句合并产生

b.与类的构造函数<init>不同,不需要显式调用父类构造器,虚拟机会保证父类的<clinit>方法先调用,所以object的是最早的

c.接口中不能使用静态语句块,但仍有初始化赋值操作,所以一样有<clinit>方法。接口中不需要先初始化父接口的<clinit>方法,只有在子接口使用到父接口的变量时才会初始化。另外接口的实现类初始化时也不会立即初始化接口。

d.虚拟机会保证多个线程执行一个类的<clinit>方法时是线程安全的

类加载器:

比较两个类的是否相等,只有在是同一个类加载器加载的情况下才有意义

类何时才会被卸载?

代表这个类的Class对象(在方法区的对象)不可用时这个对象会被回收,这是类被卸载的直接条件。在这个类的实例对象都被回收,并且加载这个类的类加载器也被回收的情况下才满足这个条件,所以被jvm自带类加载器加载的类始终不会被卸载的(因为jvm会始终引用这些类加载器),而自定义类加载器加载的类是可以被卸载的。

双亲委派模型:

启动类加载器:<JAVA_HOME>\lib下例如 rt.jar

扩展类加载器:<JAVA_HOME>\lib\ext

应用程序类加载器:程序默认类加载器

自定义类加载器:

工作过程:一个类加载器收到了加载类的请求,首先自己不会去尝试加载,而是委派给父类去加载,最终会被传到启动类加载器,父加载器反馈无法完成加载请求时,自己才去加载,如果都没加载到则抛出ClassNotFoundException异常。

第8章 虚拟机字节码执行引擎

栈帧:用于支持虚拟机进行方法调用和方法执行的数据结构。

存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。在编译时期栈帧需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入方法表的code属性中。

局部变量表:存放方法参数和方法内部定义的局部变量,容量以变量槽Slot为最小单位,一个slot可以放下一个32位以内的数据类型。

操作数栈:也称操作栈

动态连接:方法调用,静态(例如方法重载,发生在编译阶段,一般是找出一个“更合适”的版本执行)/动态分派(继承复写)

方法返回地址:a.遇到方法返回的字节码指令b.抛出了异常,两种方式都会返回到方法调用的位置。

动态类型语言:他的类型检查的主体过程是在运行期而不是编译器。java是在编译器,java1.7加入了MethodHandle来实现了动态,完成了新加的invokedynamic指令。

基于栈 vs 寄存器:

栈可移植,寄存器由硬件直接提供,收到硬件的约束。栈的指令数量一般比较多,要出栈入栈,而且读取速度慢一些

第10章 早期(编译期)优化

编译器分类:

前端编译器:javac编译器,*.java -> *.class文件

JIT编译器:Hotspot的C1,C2编译器, *.class文件 -> 机器码

AOT编译器:直接 *.java -> 本地机器代码

javac编译器做的事情:

对代码的运行效率几乎没有任何优化措施,把对性能的优化集中到了后端的及时编译器,这样可以让那些不是由java产生的Class文件(如JRuby、Groovy等产生的Class文件)也能享受到优化的效果。javac做了很多针对java语言编码过程中的优化措施改善程序员的编码风格和提高编码效率。

。。。解语法糖、字节码生成(实例构造器<init>,类构造器<clinit>方法)

java语法糖的味道:编译器实现的一些小把戏

泛型与泛型擦除:这是一种伪泛型,只在源码中存在,编译后的字节码中就替换为原生类型了(例如,List<String> ->List),并且在相应的地方加上了强转。这种泛型遇到方法重载就出了问题

自动装箱拆箱和遍历循环:包装类的”==“运算在遇到算术运算的时候才自动拆箱,equals不处理数据转型的关系,应该尽量避免拆装箱。遍历就拆成迭代器(List)的方式或者普通的(int[])遍历方式。

变长参数、内部类、字符串的swith支持(jvm帮我们转换为了对字符串hashCode的swith)

枚举类:

a.生成了一个和枚举同名的final类继承java.lang.Enum类

b.类内部生成了几个和枚举同名的自身类的几个常量,例如 public static final Day MONDAY;,并在static块中初始化这些常量

c.生成valueOf(String s);和values()方法

条件编译:

例如:if(false){a++},a++就会被直接忽略。

第11章 晚期(运行期)优化

在常用的商用虚拟机(Sun Hotspot、IBM J9)中,java程序最初都是通过解释器解释执行的,当某个方法或代码块运行特别频繁时就会认定为”热点代码“,为了提高运行时效率,jvm会将这些代码编译成本地平台相关的机器码,进行各层次的优化,完成这个任务的就是JIT编译器

解释器和编译器:JIT生成的代码比javac的更优秀,准确说是jit优化后的本地代码比解释器解释字节码后实际执行的本地代码更优秀。

并不是所有的虚拟机都采用解释器和编译器并存的架构(比图JRockit),但主流的如Hotspot、J9都同时包含,它们各有优势,解释器可以让程序迅速启动和执行,节约资源;编译器在程序执行一段时候后更能发挥作用,把更多的代码编译为本地代码,对代码优化获取更好的执行效率。

Hotspot混合编译模式:

hotspot有两个及时编译器,Client Compiler和Server Compiler,或者简称为C1、C2编译器。C1可以获取更高的编译速度,进行简单可靠的优化,C2则可以获取更好的编译质量,甚至会进行一些不可靠的激进优化,任何一个和解释器混合执行都叫混合编译模式。

编译对象和触发条件:

a.被多次调用的方法

b.被多次执行的循环体(仍然会以整个方法作为编译对象)

热点探测方式:

a.基于采样的热点探测:周期性检查各个线程的栈顶,发现某个方法经常出现在栈顶则标记为热点方法

b.基于计数器的热点探测:为每个方法建立计数器,统计执行次数,有方法调用计数器和回边计数器,两计数器之和超过阀值就向编译器提交编译请求。并且方法调用计数器有个半衰周期,到了就计数减半。hotspot就是用的这种方式。

优化的方式:方法内联(这个很重要,避免了方法栈帧入栈指针寻址的操作)、常量传播、无用代码消除(例如类似y = y)、消除公共字表达式(例如,y = b.value,z = b.value -> z = y,减少了一次访问b对象)等,还有例如范围检查消除(例如循环体内访问数组,只要保证循环条件小于数组长度,访问数组时就可以去掉长度的验证代码)、空值检查消除。

方法内联:

内联条件:编译期解析的方法,例如私有方法、实例构造器(构造方法)、父类方法、静态方法(能被继承,但是不能被复写)、final方法。因为对于虚方法无法确定实际的是哪个方法版本。

类型继承关系分析:引入帮助内联p354

逃逸分析:

分析对象动态作用域,例如一个对象被定义后作为调用参数传递给其它方法就是方法逃逸,若把对象赋值给类变量或者可以在其它线程访问到的实例变量称为线程逃逸。

如果一个对象不会被逃逸到方法或线程之外则可以做一些优化:

a.栈上分配:java堆是线程共享的,而且需要gc回收它,把对象存在栈上是个不错的选择,随着栈帧出栈就销毁,gc压力变小

b.同步消除:线程同步是个耗时的过程,需要用户态切换到核心态

c.标量替换:变量是说一个数据无法再分解成更小的数据来表示,例如int,long;而对象称为聚合量。若不会逃逸,则可以把一个java对象拆散,执行的时候不创建对象,直接把变量分配到栈上。

第12章 Java内存模型和线程

由于计算机的存储设备和处理器的运算速度有几个数量级的差距,所以会加入一层读写速度尽可能接近处理器速度的高速缓存作为内存和处理器之间的缓冲:将运算需要的数据复制到缓存中,进行运算,运算结束后同步到主存,关系图如下:

Java内存模型:

java虚拟机试图定义一种java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问区别,达到各种平台下一致的内存访问效果。

主内存和工作内存:

所有变量存在主内存中,每个线程有自己的工作内存,保存了所需要的主内存的拷贝,所有操作都在自己的工作内存完成,线程间的值传递都是通过主内存来完成。

这个地方的主内存对应于java堆对象实例部分(应该还有方法区吧),工作内存对应于虚拟机栈的部分区域。

主存和工作内存之间的八大交互操作:

lock锁定:作用于主内存变量,把一个变量标示为一条线程独占的状态

unlock解锁:主内存,把一个处于锁定状态的变量释放出来,释放后的变量才能被其它线程锁定

read读取:作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后load动作使用

load载入:作用于工作内存,将read操作得到的变量值放入工作内存的变量副本中

use使用:作用于工作内存变量,把工作内存中的一个变量的值传递给执行引擎

assign赋值:作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量

store存储:作用于工作内存变量,把工作内存的一个变量的值传送给主内存中,以便随后的write操作

write写入:作用于主内存变量,把store操作的值放入主内存变量中

由于这八种内存操作加上一些规则限定(见p365),再叫上volatile的一些特殊规定,保证并发下的线程安全。

volatile特殊规则:

java里面最轻量级的同步机制。

1.保证对所有线程的可见性,每次使用前先从主内存刷新最新的值,每次修改值都必须立刻同步回主内存

2.防止指令重排序,保证代码的执行顺序和程序的顺序相同

java线程的实现:

内核线程(KLT)实现,直接由操作系统内核支持的线程,这种线程由内核完成线程的切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。一般不会直接使用内核线程,而是使用内核线程的一种高级接口--轻量级进程(LWP),每个LWP都由一个内核线程支持。对于Sun JDK,Windows和Linux中一条java线程就映射到一条轻量级进程中。

LWP.JPG

java线程调度:

协同式调度:线程把自己的工作执行完了之后,主动通知系统切换到另一个线程。缺点执行时间不可控,如果一个线程编写有问题,就一直阻塞在那里。

抢占式调度:由系统分配执行时间,线程的切换不由线程本身来决定。java就是这种方式,这种情况下可以通过优先级的方式建议系统给某些线程多些执行时间,java中一共设置了10哥优先级。不过这种方式不那么靠谱,因为java的线程都是映射到系统原生线程的,所以线程调度还是取决于系统线程。例如Windows中优先级只有7种,不能一一对应,加入多了10中还好,不过只有7种就比较尴尬了,不够分的。

java线程状态:

新建New:创建尚未启动

运行Running:包含了操作系统线程的Running和Ready,正在执行或者等待分配cpu时间

无限期等待Waiting:不会被分配cpu执行时间,等待被其它线程唤醒,没有设置时间的Object.wait()方法,Thread.join()方法

限期等待Timed Waiting:无需被其它线程唤醒,一定时间后由系统自行唤醒。Thread.sleep(1000)方法,Object.wait(1000)方法,Thread.join(1000)方法.

阻塞Blocked:等待获取一个排它锁,将在另外一个线程放弃这个锁时发生,而等待状态是需要被唤醒的。程序进入同步区域时进入阻塞。

结束Terminated:结束执行。

第13章 线程安全与锁优化

什么是线程安全?

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

不可变的肯定是线程安全的:

String不可变:对字符数组的封装
jdk <= 1.6时,private final char value[];  offset;  count; hash;
jdk >= 1.7时,private final char value[];  hash;
String为什么设计为不可变的?
1.缓存的需要,String会被String pool缓存,缓存会被多个线程之间共享,一个客户端的修改影响其他的就会产生风险。
2.HashMap的需要,一般key为String,如果String是mutable,那么修改属性后,其hashcode也将改变。这样导致在HashMap中找不到原来的value。
3.保证hashcode可以缓存。
4.classloader中需要,String会在加载class的时候需要,如果String可变,那么可能会修改加载中的类。

总之,安全性和String字符串常量池缓存是String被设计成不可变的主要原因。

其中不可变的还有Integer、Long、Double等,但是AtomicInteger和AtomicLong并非不可变的,因为要多个线程使用来计数之类的。

线程安全的实现方式:乐观锁&悲观锁

悲观锁,阻塞同步:

认为别人会修改,为了保险,操作时先上锁。加锁、用户态核心态转换、维护锁计数器和检查是否有需要唤醒的线程,花销比较大。

1.synchronized,编译之后会在对应代码块前后分别添加monitorrenter和monitorexit指令,同步块对同一线程是可重入的。

java的线程是映射到操作系统的原生线程之上的,如果要阻塞或者唤醒一个线程都需要从用户态切换到核心态,状态转换需要耗费很多的处理器时间,所以这个关键词是一个重量级的操作,尽量避免使用

2.ReentrantLock,一样具备线程重入性,写法不同。lock(),unlock()方法经常配合try/finally语句块来完成,不过它增加了一些高级功能,等待可中断、可实现公平锁以及锁可以绑定多个条件Condition。

对比:在1.5之前,在多线程下前者的吞吐量明显低于后者,但是在1.6发布之后,他们性能基本持平了,所以还是建议使用前者。

生产者-消费者模型可通过1.使用Synchronized结合对象的wait()和notifyAll()方法实现,2.使用重入锁Condition和Condition实现

非悲观锁,非阻塞同步:

先进性操作,如果没有其他线程争抢共享数据,那就操作成功了,若果有争抢产生了冲突,那就采取其他的补偿措施,常见的就是不断的重试,直到成功为止,这种乐观锁都不需要把线程挂起。

例如Integer就是采用cas(Compare-and-Swap)的技术完成安全,cas是一条硬件的指令,native方法调用的。辅助类rt.jar中的Unsafe类,

cas需要三个操作数,分别为内存地址V,旧值A和新值B,执行时当且仅当V符合旧值A时,采用新值B更新V的值,否则一直循环直到成功为止。

CAS与Synchronized的使用情景:

1、对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

2、对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

补充: synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

自旋锁和自适应自旋:

共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得,线程状态切换要进入核心态很消耗资源,若有两个以上的线程同时执行,则可以让后面请求锁的线程稍等一下,让它执行一个忙循环,就是自旋,这个就是自旋锁。自旋不是阻塞,还是要消耗资源的,若锁被占用的时间很长,那只会白白浪费时间。自旋次数默认为10,jdk1.6默认开启,自适应自旋就是根据前面的经验决定自旋的次数。

锁消除:

虚拟机即时编译器运行时,在逃逸分析数据支持下,堆上的数据不会逃逸出去从而被其他线程访问到,就可当成栈上的数据来对待,这个时候就不需要同步。比如我们写了一个方法中:return s1+s2+s3;jdk1.5之前会替换为StringBuffer的append操作,而这个sb是个局部变量不会逃逸,这个时候就可以进行锁消除。

锁粗化:

一系列的连续操作都对同一个对象反复的加锁解锁,甚至加锁操作出现在循环体内,这个时候即使没有线程竞争也会造成不必要的性能损耗,比如StringBuffer的连续的append 的操作,就可以将加锁操作提到第一个append方法之前。(那这个例子中到底是消除还是粗化呢?我想的是先粗化,然后又发现可以消除,不矛盾,哈哈)

轻量级锁:无竞争情况下使用cas操作去消除同步使用的互斥量

Synchronized是基于互斥锁的,是一种重量级锁,轻量级锁就是相对于其而言的,但是它不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提(这个是轻量级锁的依据)下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。若果两条线程以上竞争这个锁就会膨胀为重量级锁。但是如果存在锁竞争,除了互斥量的开销还发生了CAS操作,这种情况下比传统的重量级锁还要慢。

偏向锁:无竞争情况下把整个同步都消除掉,cas也不做了偏向锁失败后会变为无锁状态或者轻量级锁状态。

1.6引入的一项锁优化,默认开启的,偏心的锁,会偏向于第一个获取它的线程,如果接下来的执行过程中该锁没被其他线程获取,则持有该偏向锁的线程将永远不需要同步(这个或许就是和轻量级的差别)。偏向锁可以提高带有同步但无竞争的程序性能,但是大部分既然做了同步肯定有竞争的,所以就是多余的,关闭它倒是可以提升性能,这个就具体看情况了。

无锁<偏向锁<轻量级锁<重量级锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值