【Java虚拟机】万字长文,搞定Java虚拟机方方面面!1

简介: 【Java虚拟机】万字长文,搞定Java虚拟机方方面面!

1.JVM内存结构

1.1.JVM内存结构图

3bc1381e88164175950fec09bc529c3a.jpg77b0eedbaad14a4e931b74751b1c7a56.jpg


1.2.程序计数器

Program Counter Register程序计数器(寄存器)


9e90a759d96f4fbfa62d391ed5e553fa.jpg

  • **作用:**记住下一条JVM指令的执行地址。
  • 特点:
  • 线程私有化,每个线程独有一个程序计数器。
  • 不会存在内存溢出。


43b1f479c8e1475ea990813f729e7b6a.jpg


1.3.虚拟机栈

Java Virtual Machine Stacks(java虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈。
  • 每个栈由多个栈帧(Frame)组成,对应这每次方法调用时所占的内存。
  • 每个线程只能有一个活动栈帧,对应这当前正在执行的那个方法。


4ab5318146f24339a6a0c279b41c2a27.jpg


baa5a5897573472fb566defd0fe8f046.jpg

问题分析

  • 垃圾回收是否涉及栈内存?
  • 栈内存主要是方法执行的内存,每个方法执行完成后,会自动的弹出栈,所以无需垃圾回收机制,垃圾回收只是在堆内存中无用的对象中使用。
  • 栈内存分配的越大越好吗
  • 不是栈内存根据操作系统来分配就好,如果栈内存分配的过大的话,会影响同时调用的线程数,比如500M的内存,栈内存分配1M,这样就可以有500个线程执行,如果分配2M就只能有250个线程执行。
  • 方法内的局部变量是否为线程安全的?
  • 如果方法内局部变量没有逃离方法的作用访问,他是线程安全的,因为是每个线程独立的变量。
  • 如果是局部变量引用了对象,并且逃离了方法的作用范围,其他线程可以得到这个值,这会就要考虑线程安全性问题。



6919ef19e815416daa5773555641a235.jpg

2、栈内存溢出

  • 栈帧过多导致栈内存溢出
  • 栈帧过大导致栈内存溢出

(1)演示栈内存溢出场景,方法递归调用

/**
 * 演示栈内存溢出
 */
public class Demo1 {
    private static int count = 0;
    public static void main(String[] args) {
        try{
            //方法入口调用
            addCount();
        }catch (Throwable e){
            e.printStackTrace();
            System.out.println(count);
        }
    }
    /**
     * 方法递归调用,让其栈内存溢出
     */
    private static void addCount(){
        count++;
        addCount();
    }
}


80b1c36fbc6146b4ac21e48a49a60ab1.jpg

(2)如何设置栈内存的大小


6f67b2d334e142deb29d4c3f486bc2c0.jpg

84cf79b0badc452cbd274940faeb2bbd.jpg

(2)演示json格式转换栈内存溢出

  • 准备员工和部门的实体
/**
 * 部门对象
 */
public class Dept {
    //部门名称
    private String name;
    //员工集合
    private List<Emp> empList;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<Emp> getEmpList() {
        return empList;
    }
    public void setEmpList(List<Emp> empList) {
        this.empList = empList;
    }
    @Override
    public String toString() {
        return "Dept{" +
                "name='" + name + '\'' +
                ", empList=" + empList +
                '}';
    }
}
/**
 * 员工对象
 */
public class Emp {
    //员工姓名
    private String name;
    //部门对象
    private Dept dept;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Dept getDept() {
        return dept;
    }
    public void setDept(Dept dept) {
        this.dept = dept;
    }
    @Override
    public String toString() {
        return "Emp{" +
                "name='" + name + '\'' +
                ", dept=" + dept +
                '}';
    }
}
  • 主方法测试
public class Main {
    public static void main(String[] args) throws JsonProcessingException {
        Dept dept = new Dept();
        dept.setName("Market");
        Emp emp1 = new Emp();
        emp1.setName("张山");
        emp1.setDept(dept);
        Emp emp2 = new Emp();
        emp2.setName("李四");
        emp2.setDept(dept);
        dept.setEmpList(Arrays.asList(emp1,emp2));
        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writeValueAsString(dept));
    }
}

98d6a0b2501a422386968512971b22a5.jpg

  • 问题分析:
问题定位:
{name :'Marker' , empList:[ {name:'张三'},dept:{name: 'Market',empList:[{name:'张三'},dept:{name:'张三',empList:[...]}]}]}
Dept对象中有Emp的List集合,每个Emp中又持有Dept对象,Dept对象中又持有Emp的List集合,无限循环导致方法调用栈内存溢出。
  • 解决办法:
//在员工实体中把Dept对象加入@JsonIgnore,JsonIgnore是在json序列化时将pojo类中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。
@JsonIgnore
private Dept dept;

3、线程运行诊断

案例:cpu占用过多

定位:

  • 用top定位哪个进程对cpu的占用使用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号

1.4.本地方法栈

本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。

Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。


08ee0df169a448f68e52b66d833b0519.jpg

  • 本地方法栈是一个后入先出(Last In First Out)栈。
  • 由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
  • 本地方法栈会抛出 StackOverflowErrorOutOfMemoryError 异常。

1.5.Java堆

1、堆简介

  • Heap(堆):通过new关键字,创建对象都会使用堆内存。
  • 特点:
  • 它是线程共享的,堆中对象都需要考虑线程安全的问题。
  • 有垃圾回收机制
  • 对于多数应用来说,Java堆(Java Heap)是Java虚拟机管理的最大一块内存。Java堆被所有线程共享,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例。

2、堆内存溢出

(1)示例代码

/**
 * 演示堆内存溢出
 */
public class Demo1 {
    public static void main(String[] args) {
        int i = 0;
        try{
            List<String> list = new ArrayList<>();
            String a = "hello";
            while(true){
                list.add(a);
                a = a + a;
                i++;
            }
        }catch (Throwable e){
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

f60eebc4ee704b5db3ea0d30a85ee901.jpg

(2)分析原因


26c6fe68d6d24a8ea21f04e36e8a5cc7.jpg

3、堆内存诊断

(1)jps工具

  • 查看当前系统种有哪些java进程

(2)jmap工具

  • 查看堆内存占用情况

(3)jconsole工具

  • 图形化界面,多功能检测
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);
        byte[] array = new byte[1024 * 1024 * 10];
        System.out.println("2...");
        Thread.sleep(30000);
        array = null;
        System.gc();
        System.out.println("3...");
        Thread.sleep(10000000L);
    }
}

78c273ca200646ab92256e78a305c347.jpg

查看但钱进程使用的堆内存情况:jmap -heap 进程号

29170db1a94148189c96554749e8264f.jpg


使用jconsole图形化界面分析


acdb65f507c14974a23161e54e665375.jpg


3596753b194a44b99770696cee25e4db.jpg

1.6.方法区

1、方法区简介

方法区(Method Area)也是所有线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。也称作Non-Heap非堆内存。

Java虚拟机规范堆方法区的限制非常宽松,可以选择不实现垃圾收集,但是这部分区域的回收确实是有必要的。

平时,说到永久带(PermGen space)的时候往往将其和方法区不加区别。这么理解在一定角度也说的过去。因为,《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。

同时,大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。

在JDK1.8及以后版本,永久带被移除,新出现的元空间(Metaspace)替代了它。元空间属于Native Memory Space

  • JVM1.6的方法区与JVM1.8的方法区对比

07f56e9536c14074a6fb5b842be38337.jpg

2、方法区内存溢出

  • 示例代码:
public class Demo4 extends ClassLoader{ //继承ClassLoader可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        Demo4 demo4 = new Demo4();
        try{
            for (int i = 0; i < 100000; i++,j++) {
                //ClassWriter作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                //版本号,public,类名,父类,接口
                cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
                //返回byte[]
                byte[] code = cw.toByteArray();
                //执行了类的加载
                demo4.defineClass("Class"+i,code,0,code.length); //Class对象
            }
        }finally {
            System.out.println(j);
        }
    }
}

(1)1.8以前会导致永久代内存溢出

演示永久代内存溢出:java.lang.OutOfMemoryError: PermGen space
设置堆内存: -XX:MaxPermSize=8m

a475ed6f70dd4b3b897be0e4d8157030.jpg

(2)1.8以后会导致元空间内存溢出

演示元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
设置堆内存: -XX:MaxMetaspaceSize=8m
注意:元空间依赖于系统内存的大小,所以一般很难演示出元空间溢出


f64e0a2c535e420fa650dc911ccbe57a.jpg

3、常量池

  • 以一段代码进行分析:
//二进制字节码存放的有 :类的基本信息、常量池、类方法定义、也包含了虚拟机指令
public class Demo5 {
    public static void main(String[] args) {
        System.out.println("I am LiXiang");
    }
}
  • javap -v Demo5.class 查看Demo5编译后的指令,java文件编译后为二进制字节码文件

(1)类的基本信息

e7f7f900956742228457dbb2f47c8c6d.jpg

(2)常量池

ae0e85951e294b9698010861480b660c.jpg

(3)类方法定义、虚拟机指令

c8989be31657470e814639fba561f354.jpg

(4)java程序编译成字节码文件的执行过程

adf0bacfc5214137be4d15a492609796.jpg

0: getstatic    #2  // Field java/lang/System.out:Ljava/io/PrintStream;
获取一个静态变量,对应常量池的 #2步骤

2f47b2dc9e584c669bb9ff297bbc5125.jpg

再继续寻找常量池的#21、#22步骤

364a134e69cb42ed8880dc480481f210.jpg

再继续寻找常量池的#28、#29、30步骤

08268af896ae4c46ba76b63e1d7193e1.jpg

3: ldc    #3   // String I am LiXiang
读取字符串,对应常量池#3,#23

09666adb93634a04877e52423f5d54a9.jpg

7ad3416fcd984007bd084dc83c511b9d.jpg

5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
方法执行,调用#4、#24、#25、#31、#32、#33

f2924f1310d3472ea156b12f805c76e1.jpg

4b0e2612476d48e3bb4a18483b8bb6b8.jpg

4、运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(

Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。

1.7.StringTable

1、常量池与串池的关系

字符串池在JDK1.7之后存在于堆中的一块区域,String s1 = "abc"这样声明的字符串会放入字符串池中,String s1 = new String("abcd")会在字符串池有一个"abcd"的字符串对象,堆中也有1个,2个不同。

  • 字符串池可以避免重复创建字符串对象
  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 它的结构为hash表结构,相同的字符串只存在一份

示例代码:

public class Demo6 {
    //串池是HashTable结构,不能扩容,串池中相同元素只会被添加一次
    //常量池中的信息,都会被加载到运行时常量池中,这时a b ab 都是常量池的符号,还没有变为java字符串对象
    //ldc #2 会把a符号变为"a"字符串对象,并且放入StringTable串池 StringTable["a"]
    //ldc #3 会把b符号变为"b"字符串对象,并且放入StringTable串池 StringTable["a","b"]
    //ldc #4 会把ab符号变为"ab"字符串对象,并且放入StringTable串池 StringTable["a","b","ab"]
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1+s2;
    }
}

7df835dc9fb94cff8876f01e67e24202.jpg

2、字符串变量拼接

    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1+s2;
        System.out.println(s3 == s4);
    }

7cc2a61d3cf2449290dcd530fb40da48.jpg

f4e712d79b1d43218eb78543680edb88.jpg

3、编译器优化

    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = "a"+"b";
        System.out.println(s3 == s4);
    }


eb6ceebc90fd4e8e9bd38cf6b329c123.jpg

58a262fe6c544f8f87495c8abfdbd837.jpg

4、StringTable特性

  • 常量池中的字符仅是符号,第一次用到的时候才变为对象。
  • 利用串池的机制,来避免重复创建字符串对象。
  • 字符串变量拼接的原理是StringBuilder(1.8)。
  • 字符串常量拼接的原理是编译期优化。
  • 可以用intern方法,主动的将串池中还没有的字符串放入串池。

5、intern()方法将对象放入串池

public class Demo8 {
    //["a","b"]
    public static void main(String[] args) { //1983
        String s = new String("a") + new String("b"); //new String("ab")
        //堆 new String("a") new String("b") new String("ab")
        String s2 = s.intern();//将这个字符串尝试放入串池,如果有则并不会被放入,如果没有则放入串池。会把串池中的对象返回
        //注意:intern()方法在1.6版本中是将堆中的数据拷贝一份,所以再用s取比较的时候就会出现false
        //s2与串池中的ab是相等的
        System.out.println("s2 == ab"+s2 == "ab");
        //同样s被放入串池中,所以与串池中的ab也是相等的
        System.out.println("s == ab"+s == "ab");
    }
}

c8d44e7eef0c4d2fa5260ced08a5ebdc.jpg

public class Demo9 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a"+"b";
        String s4 = s1+s2;
        String s5 = "ab";
        String s6 = s4.intern();
        System.out.println(s3 == s4); //false,s4 = new String("ab")存放在堆中,s3存放在串池中
        System.out.println(s3 == s5); //true,s3存放在串池中,s5直接拿串池中的数据
        System.out.println(s3 == s6); //1.8中为true,1.6为false,因为1.6复制一份副本存放在串池中
    }
}

12889ec9975a4548a18e7cd3687a1737.jpg

public class Demo9 {
    public static void main(String[] args) { 
    String x2 = new String("c") + new String("d");
        x2.intern();
        String x1 = "cd";
        System.out.println(x1 == x2); //true,因为x2堆中的对象已经放入串池中,s1为串池中对象
    }
}
public class Demo9 {
    public static void main(String[] args) { 
        String x1 = "cd";
    String x2 = new String("c") + new String("d");
        x2.intern();
        System.out.println(x1 == x2); //false
    }
}

6、StringTable的位置

StringTable在1.6时存放在永久代中,在1.8中存放在堆中

  • 下面这段代码分别在1.6与1.8中执行


af334613d66443b8a396e44db8726e07.jpg

cc5ff52110d141d1a046ffc6430bfd55.jpg

4296f8c9031c4eb8bfb4820c69f9c4cf.jpg

37c4bbfb185b4e3cb3dab1c8d009e6fc.jpg

7、StringTable垃圾回收

字符串常量也会触发垃圾回收机制

  • 初始化为1771个字符串常量
  • 07c5929b82994bd8a2399b34062af1de.jpg


dc5f74aecb28490b9b868deadad27f70.jpg


b6012b6237f840149b26acef2097ce58.jpg


861d6d65c0c44f428ee60218bcf76855.jpg

8、StringTable调优

通过调整StringTable中桶的个数来提高读取效率。默认是60013个。

调整:-XX:StringTableSize=桶个数

桶的个数调大会明显提升读取速度。

1.8.直接内存

1、直接内存简介

Direct Memory

  • 常见于NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理


f20fc081044641acbac58338cfdc57cf.jpg

ec8ae7cb27b44e1cbab7b6615417adcd.jpg


6ee0d096ba2d43abb1df1aac4070320b.jpg

2、直接内存,内存溢出问题

  • 一直向List中添加100兆的数据



a9b21159101e49deadf4862c26b67314.jpg


b16550ef6a6e4fbab402ead8bebdf9fe.jpg

3、直接内存释放原理

(1)演示直接内存释放过程



81ea2e887a75473bb39c97df0ec82546.jpg

799ef415abe04747b235ca0708cb122a.jpg




dadb5542bb4d48d3981a8b8e0f12d32c.jpg

(2)直接内存释放原理

Java提供了Unsafe类用来进行直接内存的分配与释放

Unsafe无法直接使用,需要通过反射来获取


09167b791e0a44a3af423135741f4bfb.jpg(3)分析ByteBuffer.allocateDirect()怎末进行直接内存的创建与释放的


b34b07362f8743e5b0e267bd703e9bbd.jpg



36d1fc252c8146c78f11308209bdb25e.jpg


c167e9d0215749c1a0c9619d0986e5eb.jpg

2603977897ca41f78eefbde63a0873f9.jpg

使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法

ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的 clean方法调用freeMemory来释放直接内存。

4、禁用显示回收堆直接内存的影响

  • 关闭GC显示调用:-XX:+DisableExplicitGC

关闭显示调用GC,会导致直接内存无法释放的问题,我们可以通过Unsafe来释放内存。

相关文章
|
10天前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
11 0
|
6天前
|
缓存 监控 Java
Java虚拟机(JVM)性能调优实战指南
在追求软件开发卓越的征途中,Java虚拟机(JVM)性能调优是一个不可或缺的环节。本文将通过具体的数据和案例,深入探讨JVM性能调优的理论基础与实践技巧,旨在为广大Java开发者提供一套系统化的性能优化方案。文章首先剖析了JVM内存管理机制的工作原理,然后通过对比分析不同垃圾收集器的适用场景及性能表现,为读者揭示了选择合适垃圾回收策略的数据支持。接下来,结合线程管理和JIT编译优化等高级话题,文章详细阐述了如何利用现代JVM提供的丰富工具进行问题诊断和性能监控。最后,通过实际案例分析,展示了性能调优过程中可能遇到的挑战及应对策略,确保读者能够将理论运用于实践,有效提升Java应用的性能。 【
37 10
|
4天前
|
监控 算法 Java
深入理解Java虚拟机:JVM调优的实用策略
在Java应用开发中,性能优化常常成为提升系统响应速度和处理能力的关键。本文将探讨Java虚拟机(JVM)调优的核心概念,包括垃圾回收、内存管理和编译器优化等方面,并提供一系列经过验证的调优技巧。通过这些实践指导,开发人员可以有效减少延迟,提高吞吐量,确保应用稳定运行。 【7月更文挑战第16天】
|
20小时前
|
JSON Java BI
一次Java性能调优实践【代码+JVM 性能提升70%】
这是我第一次对系统进行调优,涉及代码和JVM层面的调优。如果你能看到最后的话,或许会对你日常的开发有帮助,可以避免像我一样,犯一些低级别的错误。本次调优的代码是埋点系统中的报表分析功能,小公司,开发结束后,没有Code Review环节,所以下面某些问题,也许在Code Review环节就可以避免。
14 0
一次Java性能调优实践【代码+JVM 性能提升70%】
|
10天前
|
存储 Java 程序员
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
35 10
|
10天前
|
存储 运维 Java
Java面试题:JVM的内存结构有哪些主要部分?请简述每个部分的作用
Java面试题:JVM的内存结构有哪些主要部分?请简述每个部分的作用
29 9
|
8天前
|
存储 监控 Java
揭秘Java虚拟机:探索JVM的工作原理与性能优化
本文深入探讨了Java虚拟机(JVM)的核心机制,从类加载到垃圾回收,再到即时编译技术,揭示了这些复杂过程如何共同作用于Java程序的性能表现。通过分析现代JVM的内存管理策略和性能监控工具,文章提供了实用的调优建议,帮助开发者有效提升Java应用的性能。
26 3
|
10天前
|
存储 安全 Java
Java面试题:在JVM中,堆和栈有什么区别?请详细解释说明,要深入到底层知识
Java面试题:在JVM中,堆和栈有什么区别?请详细解释说明,要深入到底层知识
20 3
|
10天前
|
存储 Java 编译器
Java面试题:描述方法区(Method Area)的作用以及它在JVM中的演变(从永久代到元空间)
Java面试题:描述方法区(Method Area)的作用以及它在JVM中的演变(从永久代到元空间)
17 3
|
10天前
|
算法 Java
Java面试题:列举并解释JVM中常见的垃圾收集器,并比较它们的优缺点
Java面试题:列举并解释JVM中常见的垃圾收集器,并比较它们的优缺点
22 3