JVM理解

本文详细解析了JVM内存模型,包括虚拟机栈、本地方法栈、堆和方法区,以及它们的特点和可能导致的内存溢出问题。讨论了StringTable的面试题,强调了字符串常量池和intern()方法的使用。介绍了垃圾回收的基本概念,如GC root的判断算法,以及常见的垃圾回收算法和收集器。此外,还探讨了synchronized的优化,包括轻量级锁、锁膨胀、偏向锁等概念。
摘要由CSDN通过智能技术生成

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

#HashMap#

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值