1.JVM内存模型
2.Stringtable面试题
3.垃圾回收(判断GCroot的算法)
4.垃圾回收算法
5.垃圾收集器
6.sychronized优化
1.JVM内存模型
JVM内存模型包括:类加载子系统,运行时数据,执行引擎。JVM主要分析运行时数据,它包括方法区,堆,虚拟机栈,程序计数器,本地方法栈。
程序计数器(线程私有):
作用:记录下一条jvm指令的执行地址。程序计数器简单来说就是来给我们记录我们程序执行到第几行。
特点:它是线程私有的;不会出现内存溢出
虚拟机栈(线程私有):
每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析
1. 垃圾回收是否涉及栈内存?不涉及,栈帧内存就是每次调用方法产生的,调用完之后,栈帧将会弹出栈,不需要额外的垃圾回收
2. 栈内存分配越大越好吗?不是,栈内存可以通过-Xms size来设置,比如-Xms size 1m就是指给栈分配1M,而线程占用的是栈内存,如果一个线程1M的话,总物理内存500M,可以有500个线程,如果-Xms size 2m,那么就只有250个线程。也就是说栈内存越大的话,线程数越少。
3. 方法内的局部变量是否线程安全?
package cn.itcast.jvm.t1.stack;
/**
* 局部变量的线程安全问题
*/
public class Demo1_17 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}
public static void m1() {
StringBuilder sb = new StringBuilder(); //线程安全
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m2(StringBuilder sb) { //线程不安全,sb作为一个参数传进来的时候有可能有其他线程并发修改,比如main函数在修改时,还有一个线程调用这个函数,造成同时修改
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() { //线程不安全,因为它有返回值,返回值可能并发被修改
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出(java.lang.StackOverFlowError:java stack space):
栈帧过多导致栈内存溢出
栈帧过大导致栈内存溢出
问题诊断:
cpu过高时
1.用top定位哪个进程对cpu的占用过高
2.ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高) 3.jstack 进程id 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
本地方法栈(线程私有):和虚拟机栈类似,但是它的对象是本地方法,也是线程私有的
堆(线程共享):
通过 new 关键字,创建对象都会使用堆内存
特点:它是线程共享的,堆中对象都需要考虑线程安全的问题 有垃圾回收机制
堆内存溢出诊断:
1. jps 工具 查看当前系统中有哪些 java 进程
2. jmap 工具 查看堆内存占用情况 jmap - heap 进程id
3. jconsole 工具 图形界面的,多功能的监测工具,可以连续监测
方法区:jdk 1.6方法区通过永久代实现,由常量池,class,classloader组成
jdk 1.8方法区由元空间实现,存放在本地内存,由常量池,class,classloader组成
内存溢出(java.lang.OutOfMemoryError:java heap space):
1.8 以前会导致永久代内存溢出
java.lang.OutOfMemoryError: PermGen space -XX:MaxPermSize=8m
1.8 之后会导致元空间内存溢出
java.lang.OutOfMemoryError: Metaspace -XX:MaxMetaspaceSize=8m
解释器:解释器是用来执行代码的,但是它不能直接执行,首先它要将二进制字节码指令给解释器,然后解释器转化成字节码文件,最后交给cpu执行。(二进制字节码指令--->解释器--->机器码--->cpu)
即时编译器:用来执行一些热点代码
GC回收:垃圾回收,后面会讲
2.Stringtable面试题
package cn.itcast.jvm.t1.stringtable;
/**
* 演示字符串相关面试题
*/
public class Demo1_21 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab
String s4 = s1 + s2; // new String("ab")
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern();
String x1 = "cd";
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
}
}
首先stringtable放的就是字符串常量,创建字符串时,若里面没有,则会把创建的字符串加入,比如String s=“aa”;此时查看stringtable里面是否有“aa”字符串,发现没有,则加入字符串常量池中,下次不用创建新的对象“aa”,而是直接取出字符串常量池中的“aa”。
字符串变量拼接的原理是 StringBuilder (1.8)(最后会new 一个新对象返回)
intern 方法:主动将串池中还没有的字符串对象放入串池
jdk1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
jdk1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回
通过里两个例子:
package cn.itcast.jvm.t1.stringtable;
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class Demo1_22 {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
System.out.println(s3 == s5);
}
}
首先看String s1 = "a";和String s1 = "b";还有String s1 = "ab";执行这个之后会查看stringtable里有没有字符串“a”“b”“ab”,发现没有,那么就会把“a”“b”“ab”分别加入stringtable中此时有[“a”“b”“ab”],因此s1,s2,s3属于串池中的内存。String s4 = s1 + s2;这行代码是字符串拼接相当于new StringBuilder().append("a").append("b").toString。也就是说s4最终是new出来的,所以它在堆内存中。 String s5 = "a" + "b";也是属于串池中的。所以s3==s5为true
package cn.itcast.jvm.t1.stringtable;
public class Demo1_23 {
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == x);
System.out.println( s == x );
}
}
通过上面分析,x把“ab”放入串池,s先把“a”,“b”放入串池,在进行字符串拼接返回的对象在堆内存中;也就是说此时stringtable中有["ab","a","b"]。再来分析String s2 = s.intern();这个代码在jdk1.8中intern会把这个字符串对象尝试放入串池中,如果有则不放入,如果没有放入串池,会把串池对象返回;jdk1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回。也就是说s2无论1.8还是1.6返回的都是串池的对象,而s则在1.8返回的是串池的对象,1.6返回的是堆中的对象(new出来的)。所以1.8中s2==x为true,s==x为true。1.6中s2==x为true,s==x为false。通过这两个例子就很好的理解第一套题目的答案。
3.垃圾回收(判断GCroot的算法)
1)引用计数法:python中使用,有循环引用的问题
2)可达性分析算法;以GCroot对象为起点向下搜索,如果在这条链上则不能被回收,否则要被回收
哪些可以作为GCroot?
1,虚拟机栈中引用的对象(栈帧中的本地方法表)。
2,方法区中(1.8称为元空间)的类静态属性引用的对象,一般指被static修饰的对象,加载类的时候就加载到内存中。
3,方法区中的常量引用的对象。
4,本地方法栈中的JNI(native方法)引用的对象
4.垃圾回收算法
1)标记清除:速度快,但是会造成内存碎片
图中看出被GC Root引用的对象不能被回收(紫色部分),需要回收的是没有被GC Root引用的对象(灰色部分),这些垃圾会被标记,然后被清理,但是内存空间仍然在,只是把地址回收到一张表中下一次使用可查。
2)标记整理:速度慢,但是没有内存碎片
这种情况是存活较多的情况,把垃圾标记,然后在清理过程中会对存活的对象进行整理成连续的内存。
3)复制:需要占用双倍的内存空间,没有内存碎片
这种情况为存活较少的情况,清理时会把存活的对象从FROM复制到TO,然后进行垃圾回收,最后FROM和TO再次交换,完成复制。
分代回收(新生代采用复制算法,老年代采用标记整理算法):
1)对象首先分配在伊甸园区域
2)新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
3)minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
4)当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
5)当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时 间更长
垃圾从新生代到老年代有哪些情况?
1、躲过15次GC之后进入老年代,可通过JVM参数“-XX:MaxTenuringThreshold”来设
置年龄,默认为15岁;
2、动态对象年龄判断;
动态年龄判断: Survivor区的对象年龄从小到大进行累加,当累加到X年龄时的总和大于50%(可以使用-XX:TargetSurvivorRatio=?来设置保留多少空闲空间,默认值是50),那么比X大的都会晋升到老年代;
3、老年代空间担保机制;
新生代Minor GC后剩余存活对象太多,无法放入Survivor区中,此时就必须将这些存活对
象直接转移到老年代去,如果此时老年代空间也不够怎么办?
1)执行任何一次Minor GC之前,JVM会先检查一下老年代可用内存空间,是否大于新生代
所有对象的总大小,因为在极端情况下,可能新生代Minor GC之后,新生代所有对象都需要
存活,那就会造成新生代所有对象全部要进入老年代;
2)如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起一次Minor GC,因为 Minor GC之后即使所有对象都存活,Survivor区放不下了,也可以转移到老年代去;
3)如果执行Minor GC之前,检测发现老年代的可用空间已经小于新生代的全部对象总大小,
那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次Minor GC后进
入老年代的对象的平均大小,如果判断发现老年代的内存大小,大于之前每一次Minor GC
后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下Minor GC,但是此时真的可
能有风险,那就是Minor GC过后,剩余的存活对象的大小,大于Survivor空间的大小,也
大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次“Full GC";
公
所以老年代空间分配担保机制的目的?也是为了避免频繁进行Full GC;
4、大对象直接进入老年代;
5.垃圾收集器
垃圾收集器简单来说就是垃圾回收的落地实现!
1)串行
-XX:+UseSerialGC = Serial + SerialOld
2)吞吐量优先
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
3)响应时间优先
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
6.sychronized优化
讲到优化,首先要知道每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存 储这个对象的 哈希码 、 分代年龄 ,当加锁时,这些信息就根据情况被替换为标记位 、 线程锁记录指针 、 重量级锁指针 、 线程ID 等内容
1)轻量级锁
如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻 量级锁来优化。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
2)锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻 量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
3)重量锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退 出了同步块,释放了锁),这时当前线程就可以避免阻塞。
4)偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁 来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS.
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
5)其他优化
1. 减少上锁时间 同步代码块中尽量短
2. 减少锁的粒度 将一个锁拆分为多个锁提高并发度,例如:
1)ConcurrentHashMap(分段锁)
2)LongAdder 分为 base 和 cells 两部分。没有并发争用的时候或者是 cells 数组正在初始化的时 候,会使用 CAS 来累加值到 base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允 许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值
3)LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要 高
3. 锁粗化 多次循环进入同步块不如同步块内多次循环 另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁, 没必要重入多次)
new StringBuffer().append("a").append("b").append("c");
4. 锁消除 JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候 就会被即时编译器忽略掉所有同步操作。
5. 读写分离 CopyOnWriteArrayList ConyOnWriteSet