JVM解析
三种jvm
-
Sun公司
-
BEA公司
-
IBM公司
-
请你谈谈你对JVM的理解? java8虚拟机和之前的变化更新?
-
什么是O0M,什么是栈溢出StackOverFlowError? 怎么分析?
-
JVM的常用调优参数有哪些?
-
内存快照如何抓取,怎么分析Dump文件?知道吗?
-
谈谈JVM中,类加载器你的认识?
- JVM的位置
- JVM的体系结构
- 类加载器
- 双亲委派机制
- 沙箱安全机制
- Native
- PC寄存器
- 方法区
- 栈
- 三种JVM
- 堆
- 新生区、老年区
- 永久区
- 堆内存调优
- GC
- 常用算法
- JMM
jvm体系结构
线程安全区
线程隔离的数据区,有方法区,堆
线程不安全区
由所有线程共享的数据区,有虚拟机线,程序计数器,本地方法栈
1、程序计数器
- 它是一块较小的内存空间
- 每一条JVM线程都有自己的PC寄存器,各条线程之间互不影响,独立存储,这类内存区域被称为“线程私有”内存
- 在任意时刻,一条JVM线程只会执行一个方法的代码。该方法称为该线程的当前方法
- 这个区是唯一不会报堆栈溢出的
- 把程序打成很小的文件,进行标注,比如1、2、3、4、5,按顺序执行
- 又叫PC寄存区
2、java虚拟机栈(JAVA栈)
我理解的栈是这样的:在代码执行时,首先main方法入栈,然后调用A方法,A方法就入栈,在A方法途中可以调用别的方法,比如A方法途中调用了B方法,那么B方法就入栈,B方法如果途中有别的方法执行,就再进行入栈,如果没有,则B方法执行完后出栈,继续执行A方法,A方法执行完后A出栈,然后main方法出栈,然后程序结束。这就是为什么栈里放的都是引用,也满足了栈的基本规则:先入后出
- 栈:存放八大基本类型+对象引用+实例的方法
- main()先进栈压栈,main出栈时程序结束,main方法是栈底
- Java虚拟机栈也是线程私有的。这个栈与线程同时创建,它的生命周期与线程相同。
- 可以通过设置JVM stack来设置栈的大小
- 如果线程请求的栈深度大于虚拟机所允许的深度将抛出StackOverflowError(堆栈溢出)
- 如果在设置stack大小时无法申请那么大的内存,则抛出OutOfMemoryError (内存溢出)
- 程序正在执行的方法,一定在栈的顶部
栈帧
3、本地方法栈
本地方法栈与虚拟机栈作用相似,后者为虚拟机执行Java方法服务,而前者为虚拟机用到的Native方法服务
意思就是说,虚拟机栈执行java有关的方法,而本地方法栈执行一些非java方法,比如JDBC、Tomcat
异常跟虚拟机栈一样。
和别的语言交互时,在本地方法栈进行对接,例如JDBC
public class Demo {
public static void main(String[] args) {
new Thread(()->{
},"my thread name").start();
}
//native:凡是带了native关键字的方法,说明java的作用范围达不到了,回去调用底层C语言的库
//会进入本地方法栈
public native void start0();
}
4、java堆
一个JVM只有一个堆内存
堆里有什么:类、方法、常量、变量,栈里放它们的实例
虚拟机管理的内存中最大的一块,同时也是被所有线程所共享的,它在虚拟机启动时创建,存在的意义就是存放对象实例。
- java堆是CG的主要部分,因为它存放了实例,清垃圾肯定从根源下手,采用分代回收
- 堆大小也能设置
- Java堆所使用的内存不需要保证是物理连续的,只要逻辑连续即可。
- java堆分为新生代、老年代
- 永生代不属于堆!!!!!!(jdk8后永生代被改名为元空间)
物理连续
一个挨一个顺序往下走,就像军训时第一排报数,一个接一个
逻辑连续
顺序是乱的,但是还按顺序走,像是按学号报数,不一定站的顺序是挨着的,但是还要按1、2、3的顺序报
新生代
- 类:诞生和成长的地方,甚至死亡
- 伊甸园,所有的对象都是在伊甸园区new出来的
- 幸存者区(0,1)
- 轻GC
老年代
- 当重GC
永生代(元空间)
这个区域常驻内存。用来存放jdk自身携带的Class对象。Interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收,只有关闭虚拟机才会释放这个区域的内存
一个启动类,加载了大量的第三方jar包或者tomcat部署了太多应用,大量动态生成的反射类。不断地被加载,知道内存满,就会出现OOM(内存溢出)
- jdk1.6之前:永久代,常量池是在方法区中
- jdk1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中
- jdk1.8之后:无永久代,常量池在元空间
- 非堆
永久代更难回收,基本不会甚至
GC垃圾回收主要是在伊甸园区和养老区
在jdk8之后,永久代(永久储存区)改名为元空间
5、方法区
跟堆一样是被各个线程共享的内存区域,用于存储以被虚拟机加载的类信息、常量、静态变量、但是它的别名叫非堆,用来与堆做一下区别。
- 此区域属于共享数据资源区
- 方法区在虚拟机启动的时候创建。
- 方法区的容量会自动扩展,也会自动收缩
就像常量池一样,它是优先加载的,并且在任何时候任何地方都可以用,它和永生代的关系就像接口和类的关系
堆,栈,方法区之间的关系
public class Test2 {
public static void main(String[] args) {
public Test2 t2 = new Test2();
//JVM将Test2类信息加载到方法区,new Test2()实例保存在堆区,Test2引用保存在栈区
}
}
运行时常量池
它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。
6、直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存的大小的限制。
基本上可以理解就是本机内存呗
栈+堆+方法区:交互作用
类加载器
作用:加载Class文件 类似newStudent()
类是模板,对象是具体的
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
public class Car {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
}
}
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader);
//AppClassLoader 应用程序加载器
System.out.println(classLoader.getParent());
//ExtClassLoader 扩展类加载器
System.out.println(classLoader.getParent().getParent());
//null 1.不存在 2.Java程序获取不到1.类加载器收到类加载的请求
双亲委派机制
当某个类加载器需要加载某个.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
用代码来理解一下双亲委派机制
这段代码展示了双亲委派机制
咋们在自己创建的lang包下运行,他会向上一级去请求类加载,然后被咋们假冒的lang包给顶回来了,告诉你找不到string中的lang方法
/*
1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3.启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子类加载器进行加载
4.重复步骤3
*/
/*
若都找不到就会报 Class Not Found
null:Java调用不到,可能编程语言是C写的,所以调不到
Java =C++-- 去掉C里面比较繁琐的东西 指针,内存管理(JVM帮你做了)
*/
/*
类加载器不等于双亲委派机制
例如tomcat会先自己加载这个类,如果自己加载不了,才会让父类加载,这不属于双亲委派机制
*/
沙箱安全机制
Java安全模型的核心就是Java沙箱,沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在JVM特定的运行范围中,沙箱主要限制系统资源的访问包括CPU、内存、文件系统、网络。
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱机制。
也就是说沙箱安全机制是java的一个安全模型,它可以限制java代码在JVM上运行的范围,保护代码,也保护本地系统
OM(内存溢出)异常实战
1、堆溢出
测试
import java.util.ArrayList;
import java.util.List;
public class HotspotOOMtest {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}
报错信息
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3296.hprof ...
Heap dump file created [27859308 bytes in 0.208 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.util.Arrays.copyOf(Unknown Source)
at java.util.ArrayList.grow(Unknown Source)
at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
at java.util.ArrayList.add(Unknown Source)
at com.hy.HotspotOOMtest.main(HotspotOOMtest.java:15)
2、虚拟机栈和本地方法栈溢出
-
单线程将虚拟机栈和本地方法栈内存溢出
测试
public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak(){ stackLength++; stackLeak(); } public static void main(String[] args) { JavaVMStackSOF oom = new JavaVMStackSOF(); try{ oom.stackLeak(); } catch (Throwable e){ System.out.println("stack length: "+ oom.stackLength); throw e; } } }
异常
stack length: 990Exception in thread "main" java.lang.StackOverflowError at com.hy.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12) at com.hy.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
-
通过创建多线程导致虚拟机栈和本地方法栈内存溢出异常
测试
/** * VM Args: -Xss2M(这时候不防设置大些) * @author mch * */ public class JavaVMStackOOM { private void dontStop(){ while(true){ } } public void stackLeakByThread(){ while(true){ Thread thread = new Thread(new Runnable(){ @Override public void run(){ dontStop(); } }); thread.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }
异常
3、方法区和运行时常量池溢出
测试
import java.util.ArrayList;
import java.util.List;
/**
* VM Arges: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author mch
*
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//使用List保持这常量池引用,避免Fuli GC回收常量池行为
List<String> list = new ArrayList<String>();
//10MB的PermSize在integer范围内足够产生OOM了
int i =0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
}
异常
4、本机直接溢出
测试
import java.lang.reflect.Field;
/**
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
* @author mch
*
*/
public class DirectMenoryOOM {
private static int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception{
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe)unsafeField.get(null);
while(true){
unsafe.allocateMemory(_1MB);
}
}
}
异常
GC垃圾收集器与内存分配策略
怎么判断是需要回收的
死掉的对象需要回收
1.1、如何判断对象是否已经死掉(引用计数法)
给他一个计数标识,起始为0,用它一次就+1,要是有的一直为0,就干掉它
但是,Java语言中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环引用的问题。
1.2、如何判断对象是否已经死掉(可达性分析算法或者根搜索算法)
就是有一个根节点GC roots,和他有联系的就不清除,没联系的就清除
GC作用区域:堆,方法区
程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭;
栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。
强引用不容易被回收
强引用
eg:new对象
软引用
eg:局部变量,new allrylist=null
弱引用
eg:传参,for循环的i
虚引用
eg:反射区的东西,没有对象实例
引用计数法,标记清除法
标记需要清理的垃圾、触发一次GC后清楚这些被标记的垃圾
但是一般不用这种方法,因为它有几个缺点
- 效率不高
- 会产生很多空间碎片,因为清楚之后就都不连续了
复制算法
就是把分给GC的内存分为两块儿,左边先标记后,当触发一次GC时,按顺序复制到右边,然后清除掉左边所有的内容,下次从右边往左边复制,循环往复
这个方法通常用在新生区,因为新生区的更迭比较频繁,需要效率高的方法来支持
但是它也有个显而易见的缺点,它会将内存缩小为原来的一半
标记整理清楚算法
标记算法是最传统最稳定的算法,通常用在老年代
标记整理算法是先标记需要回收的垃圾,在一次GC触发后删除这些标记的垃圾,然后进行排序整理
标记算法的效率慢但是保住了1/2的内存空间,适合用在内存大并且对象存活率高的老年代
并行:多条线程同时执行
并发:多个线程执行同一个东西
吞吐量:cpu运行时间/程序响应时间
虚拟机执行引擎
Java虚拟机字节码执行引擎是jvm最核心的组成部分之一
所有的 Java 虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果,下面将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。
早期优化语法糖
语法糖指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说,使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
Java 语法糖在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。
java语法糖的味道
1.泛型与类型擦除
它的本质是参数化类型,也就是说操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("hello", "你好");
map.put("how are you?", "吃了没?");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you?"));
}
public class GenericTypes {
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}
}
这段代码是不能被编译的,因为参数 List 和 List 编译之后都被擦除了,变成了一样的原生类型 List,擦除动作导致这两种方法的特征签名变得一模一样。
解语法糖之后变成了两个一样的方法
public class GenericTypes {
public static String method(List<String> list) {
System.out.println("invoke method(List<String> list)");
return "";
}
public static int method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
return 1;
}
public static void main(String[] args) {
method(new ArrayList<String>());
method(new ArrayList<Integer>());
}
}
差别是两个 method 方法添加了不同的返回值,由于这两个返回值的加入,方法重载居然成功了,即这段代码可以被编译和执行
也就是说,两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个 Class 文件中的。
添加返回值后发现可以运行,是因为解语法糖后方法也不一致,因为返回值
2.自动装箱、拆箱与遍历循环
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
// 如果在 JDK 1.8 中,还有另外一颗语法糖
// 能让上面这句代码进一步简写成 List<Integer> list = [1, 2, 3, 4];
int sum = 0;
for (int i : list) {
sum += i;
}
System.out.println(sum);
}
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d); //true
System.out.println(e == f); //false
System.out.println(c == (a + b)); //true
System.out.println(c.equals(a + b)); //true
System.out.println(g == (a + b)); //true
System.out.println(g.equals(a + b)); //false
}
int和封装类赋值时就是自动装箱和自动拆箱
3.条件编译
public static void main(String[] args) {
if (true) {
System.out.println("block 1");
} else {
System.out.println("block 2");
}
}
解完语法糖之后
public static void main(String[] args) {
// 编译器将会提示 "Unreachable code"
while (false) {
System.out.println("");
}
}
- 给if的判断条件里放入true,我们可以知道肯定进if,在编译器编译时就会进行优化,会直接把这个if-else语句杀掉,也就是解语法糖,解完之后就只有一个输出语句了
- while里放false,意思永远页进不去这个while循环,编译器解语法糖后就会干掉这个循环
运行期优化
解释器与编译器
优势一:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。
优势二:当程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率。
经典优化技术
- 语言无关的经典优化技术之一:公共子表达式消除。
- 语言相关的经典优化技术之一:数组范围检查消除。
- 最重要的优化技术之一:方法内联。
- 最前沿的优化技术之一:逃逸分析。
公共子表达式消除
就是如果一个表达式已经被计算过,第二次又碰到这个表达式,发现任何条件都没有发生变化,那么它就是个公共子表达式,有点像缓存
int d= (c * b)*12+a+ (a + b * c)
//编译器检测到“c * b”与“b* c”是一样的表达式,而且在计算期间b与c的值是不变的。
int d=E*12+a+(a+E);
数组边界检查消除
就是编译器判定你变量的取值范围始终在数组范围内就会消除数组的大小界限,不再去判断,省下检测数组越界的工作
if (foo != null) {
return foo.value;
} else {
throw new NullPointerException();
}
# 虚拟机隐式优化;
try {
return foo.value;
} catch (Segment_Fault e) {
uncommon_trap(e);
}
方法内联
就是直接把调用的方法放到调用方的块儿里,尽量减少真实方法的调用
public static void foo(Object object) {
if (object != null) {
System.out.println("ok.");
}
}
public static void testInline(String[] args) {
Object object = null;
foo(object);
}
线程安全与锁优化
线程安全:
不可变:不可变的数据一定是安全的,无论是对象的方法实现还是方法的调用者,都不需要采取任何的线程安全保障措施
会出现线程安全的最低条件:多个线程之间存在共享数据访问
相对线程安全:
对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
线程兼容:
一个ArrayList的线程是不安全的,但是如果有一个A类锁住了,那么这个集合就是线程安全的,这就是线程兼容
就像是一个木头笼子(线程不安全),它有一定的安全性,但是不太安全,如果外面套了个铁笼子(线程安全的),那么里面的木头笼子也跟着变安全了(线程兼容)
线程对立:
- 一个要开、一个要关,怼死了,就会有线程对立
- java已经将大部分线程对立给慢慢取消优化掉了
同步:
互斥同步:
互斥同步是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。而互斥是实现同步的一种手段。
非阻塞同步:
线程A和B、B进A线程后,发现A不能执行,又返回自己的线程做之后做的事情,然后过一段时间再来询问A能不能执行,并不会造成阻塞现象
无同步方案:
-
可重入代码:
- 我们可以通过一个简单的原则来判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。
- 以在代码执行的任何时刻中断它,转而去执行另外一段代码
- 它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
-
线程本地存储
- 如果一段代码中的数据必须与别的线程共享,那么久看能不能并为一个线程执行,如果可以,就不会出现争抢的现象了
- Mq消息队列可以解决
锁优化
-
自旋锁与自适应自旋
- 如果物理机器能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个自旋,这项技术就是所谓的自旋锁。
- 怎么优化自旋锁?
- 如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时候很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是 10 次,用户可以使用参数 -XX:PreBlockSpin 来更改。
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
-
锁消除
- 就是在编译时,要求线程同步,但是检测发现不会和别的线程出现争抢,就会把锁给清除掉
- 锁消除的主要判定依据来源于逃逸分析的数据支持
一段看起来没有同步的代码
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
Javac 转化后的字符串连接操作
```java
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
会转化为三个小线程,但是这三个线程的作用域被限制在了concatString这个方法中,不会逃逸,别的线程也访问不到它,所以这里虽然有锁,但是会被安全的消除掉
-
锁粗化
如果虚拟机探测到由一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
例如上面的代码,连续对一个对象执行三次添加操作。会将三次线程操作粗化为一次,也就是只执行一次加锁操作,就我没必要锁一次执行一次开一次下次再锁一次,因为我操作的是同一个对象,就直接操作完再开锁
-
轻量级锁
- 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
- 没咋看懂这个锁
-
偏向锁
-
这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
-
偏向锁可以提高带有同步但无竞争的程序性能。
-
它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。(效益权衡)
这锁的意思就是有一个线程获得了这个锁,然后后来这个锁再也没有被别的线程获取,那么这个线程就不需要再同步了
-
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
会转化为三个小线程,但是这三个线程的作用域被限制在了concatString这个方法中,不会逃逸,别的线程也访问不到它,所以这里虽然有锁,但是会被安全的消除掉
-
锁粗化
如果虚拟机探测到由一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
例如上面的代码,连续对一个对象执行三次添加操作。会将三次线程操作粗化为一次,也就是只执行一次加锁操作,就我没必要锁一次执行一次开一次下次再锁一次,因为我操作的是同一个对象,就直接操作完再开锁
-
轻量级锁
- 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
- 没咋看懂这个锁
-
偏向锁
-
这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
-
偏向锁可以提高带有同步但无竞争的程序性能。
-
它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。(效益权衡)
这锁的意思就是有一个线程获得了这个锁,然后后来这个锁再也没有被别的线程获取,那么这个线程就不需要再同步了
-