黑马JVM学习笔记-内存结构

什么是JVM?

定义:

Java Virtual Machine - java 程序的运行环境(Java二进制字节码的运行环境)

好处:3

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查(下标越界抛出异常比数组新元素覆盖其他部分造成的危害小)
  • 多态(面向对象编程的基石,jvm内部采用虚方法表的方式实现多态)

比较:

jvm jre jdk

请添加图片描述

jvm屏蔽Java代码与底层操作系统之间的差异

jdk+集成开发工具->JavaSE

jdk+应用服务器(如:tomcat)+集成开发工具->JavaEE

学习JVM有什么用?

  • 面试!!!
  • 理解底层的实现原理
  • 中高级程序员的必备技能

常见的JVM有哪些?

请添加图片描述

学习路线

请添加图片描述

方法执行时的每行代码是由执行引擎中的解释器逐行进行执行

方法里面的热点代码(频繁执行的代码)由执行引擎的即时编译器编译

GC:会对堆里面不再被引用的对象进行回收

内存结构

1.程序计数器

请添加图片描述

1.1定义

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

  • 作用,是记住下一条jvm指令的执行地址
  • 特点
    • 是线程私有的
    • 唯一 一个不会存在内存溢出
1.2作用

二进制字节码 jvm指令 java源代码

0: getstatic #20            // PrintStream out = System.out;
3: astore_1                 // --
4: aload_1                  // out.println(1);
5: iconst_1                 // --
6: invokevirtual #26        // --
9: aload_1                  // out.println(2);
10: iconst_2                // --
11: invokevirtual #26       // --
14: aload_1                 // out.println(3);
15: iconst_3                // --
16: invokevirtual #26       // --
19: aload_1                 // out.println(4);
20: iconst_4                // --
21: invokevirtual #26       // --
24: aload_1                 // out.println(5);
25: iconst_5                // --
26: invokevirtual #26       // --
29: return

二进栈字节码 经过解释器 变成机器码 交给cpu执行

程序计数器负责将下一条jvm指令的执行地址告诉解释器

程序计数器在物理上是通过寄存器实现的

2.虚拟机栈

栈:先进后出

请添加图片描述

2.1定义:

请添加图片描述

Java Virtual Machine Stacks (Java 虚拟机栈)

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

问题辨析

  1. 垃圾回收是否涉及栈内存?

    • 栈帧内存每次方法调用结束后都会弹出栈;不涉及
  2. 栈内存分配越大越好吗?

    • 不是

    • 运行java代码时可以通过-Xss size给栈内存指定大小

    请添加图片描述

    • 栈内存分配越大(只是能更多次的进行方法调用),线程数越少
  3. 方法内的局部变量是否线程安全?

    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
    • 共享的需要考虑线程安全,私有的不需要考虑线程安全
2.2栈内存溢出

java.lang.StackOverflowError

  • 栈帧过多导致栈帧内层溢出
  • 栈帧过大导致栈内层溢出
2.3线程运行诊断

案例1: cpu 占用过多

定位

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高;H:打印进程数;-eo:规定哪些感兴趣的内容:pid:进程id、tid:线程id, %cpu:对cpu的占用;grep:进行筛选)
  • jstack 进程id
    • 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号

案例2:程序运行很长时间没有结果

  • 如:死锁

3.本地方法栈

java虚拟机调用本地方法(不是由Java编写的代码)时,需要给本地方法提供的内层空间

  • Object里面:clone()方法、hashCode()、notify()、wait()等都是native的

请添加图片描述

4.堆

  • 前面都是线程私有的,堆是线程共享的

请添加图片描述

4.1定义

Heap 堆

  • 通过new关键字,创建对象都会使用堆内层

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制
4.2 堆内存溢出
  • -Xmx8m:可以改变堆内存空间大小8m :代表8兆默认4g

  • java.lang.OutOfMemoryError: java heap space

堆内存中的对象只有不再有人使用了才会被当做垃圾回收,如果不断产生对象且一直有人使用它们,就意味着这些对象不能成为垃圾,当这些对象达到一定的数量,堆内存就会被耗尽

int i=0;
try{
    Lsit<String> list = new ArrayList<>();
    String a="hello";
    while(true){
        list.add(a);//hello
        a = a+a;    //hellohello
        i++;
    }
}catch(Throwable e){
    e.printStackTrace();
    System.out.println(i);
}
4.3 堆内存诊断

1.jps工具

  • 查看当前系统中有哪些Java程序

2.jmap工具

  • 查看堆内存占用情况(某个时刻的) jamp -heap进程id

3.jconsole工具

  • 图形界面的,多功能的监测工具,可以连续监测

案例

  • 垃圾回收后,内存占用仍然很高

5.方法区

请添加图片描述

5.1定义

JVM规范-方法区定义

Chapter 2. The Structure of the Java Virtual Machine (oracle.com)

  • 所有Java虚拟机线程共享的一块区域
  • 这块区域存储了跟类的结构相关的信息:成员变量、方法、成员方法以及构造器方法、运行时常量池
  • 在虚拟机启动时被创建,逻辑上是堆的一个组成部分

永久代是jdk1.8以前方法区的一个实现

5.2 组成

请添加图片描述

1.8已经不再由jvm管理其内存结构了,已经被移出到本地内存当中 (操作系统内存)

JDK1.8版本之前方法区用的堆的内存,叫永久代,JDK1.8之后用的操作系统的内存,叫元空间

这里说的不对,StringTable一直都在本地内存,StringTable中存储的是对字符串对象的指针,对应的String对象在堆中

请添加图片描述

5.3方法区内存溢出
  • 1.8 以前会导致永久代内存溢出
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m
  • 1.8 之后会导致元空间内存溢出
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m

场景

  • spring

  • mybatis

5.4 运行时常量池
  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量 等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量 池,并把里面的符号地址变为真实地址
5.5 StringTable(串池)

先看几道面试题:

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")
String x1 = "cd";
x2.intern();//没入池成功

// 问,如果调换了【最后两行代码】的位置呢?:true,如果是jdk1.6呢false
System.out.println(x1 == x2);//false
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);
        }
}

5.5 StringTable特性
  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串 池中的对象返回
    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回
/**
 * 演示字符串字面量也是【延迟】成为对象的
 */
public class TestString {
    public static void main(String[] args) {
        int x = args.length;
        System.out.println(); // 字符串个数 2275

        System.out.print("1");
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print("1"); // 字符串个数 2285
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print(x); // 字符串个数
    }
}

JDK1.8

情况一:
public class Demo1_23 {
    //  ["ab", "a", "b"]
    public static void main(String[] args) {
        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 == "ab");//true
        System.out.println( s == "ab" );//true
    }
}
情况二:
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 == "ab");//true
        System.out.println( s == "ab" );//false
    }
}

JDK1.6

public class Demo1_23 {

    // ["a", "b", "ab"]
    public static void main(String[] args) {


        String s = new String("a") + new String("b");

        // 堆  new String("a")   new String("b")  new String("ab")
        String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
        // s 拷贝一份,放入串池

        String x = "ab";
        System.out.println( s2 == x);//true
        System.out.println( s == x );//false
    }
}

    //["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(); // s2,是串池中原有的ab,s是堆中的
        System.out.println( s2 == x);//true
        System.out.println( s == x );//false
    }
}


5.6 StringTable 位置

请添加图片描述

改变的原因:永久代的回收效率很低,永久代只有fullGC 的时候才会垃圾回收,而FUll GC 的触发条件是老年代空间不足,发生的时机晚。而StringTable的使用又比较频繁所以就会占用大量的内存,进而导致永久代内存不足

**证明方式:**不断的往StringTable里存放大量的字符串对象,并且用一个长时间存活的对象引用它,这样肯定会造成空间不足,如果在jdk1.6就会报永久代的内存空间不足(PermGen space),如果在jdk1.7及以上就会报堆空间不足(Java heap space)

/**
 * 演示 StringTable 位置
 * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit一个开关
 * 在jdk6下设置 -XX:MaxPermSize=10m
 */
public class Demo1_6 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());//intern()就是让其存入StringTable
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
} 
5.7 StringTable 垃圾回收

从以下案例中可以看出StringTable是存在垃圾回收的

/**
 * 演示 StringTable 垃圾回收
 *  虚拟机堆内存的最大值 打印字符串表的统计信息           打印垃圾回收的详细信息
 * -Xmx10m            -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}


[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->756K(9728K), 0.0027377 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2536K->424K(2560K)] 2804K->700K(9728K), 0.0132936 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 2472K->424K(2560K)] 2748K->700K(9728K), 0.0026577 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
100000
Heap
 PSYoungGen      total 2560K, used 1432K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 49% used [0x00000000ffd00000,0x00000000ffdfc240,0x00000000fff00000)
  from space 512K, 82% used [0x00000000fff00000,0x00000000fff6a020,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 276K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 3% used [0x00000000ff600000,0x00000000ff645010,0x00000000ffd00000)
 Metaspace       used 3222K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13234 =    317616 bytes, avg  24.000
Number of literals      :     13234 =    566288 bytes, avg  42.790
Total footprint         :           =   1043992 bytes
Average bucket size     :     0.661
Variance of bucket size :     0.662
Std. dev. of bucket size:     0.813
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :     19582 =    469968 bytes, avg  24.000
Number of literals      :     19582 =   1155792 bytes, avg  59.023
Total footprint         :           =   2105864 bytes
Average bucket size     :     0.326
Variance of bucket size :     0.341
Std. dev. of bucket size:     0.584
Maximum bucket size     :         4

Process finished with exit code 0
5.8 StringTable 性能调优
  • 调整 -XX:StringTableSize=桶个数
  • 考虑将字符串对象是否入池

如果有大量的重复字符串可以让字符串入池来减少字符串的个数减少堆内存的使用

/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {
    public static void main(String[] args) throws IOException {
        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern());
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

6.直接内存

不属于Java虚拟机的内存管理而是属于系统内存(操作系统内存)

垃圾回收不会管理直接内存

6.1 定义

Direct Memory

  • 常见于NIO操作时,NIO读写时用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理
/**
 * 演示 ByteBuffer 作用
 */
public class Demo1_9 {
    static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
    }

    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    }

    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }
}

java本身不具有磁盘读写的能力,需要调用操作系统的函数(本地方法)

所以这里会涉及到CPU的状态从用户态切换为内核态,然后就由cpu的函数去读取磁盘文件的内容,读取之后在内核态的时候会在操作系统内存中划出一块系统缓存区(系统缓存区Java代码不能运行),所以Java会在堆内存中分配一块Java缓冲区,Java代码要想读取的流中的数据必须从系统缓存区把数据间接的读入到Java缓冲区,cpu会再进入用户态,之后再去调输出流的写入操作

出现的问题:

  • 读取的时候数据存了两份,造成了不必要的数据的复制

请添加图片描述

调用ByteBuffer的allocateDirect(): 分配一块直接内存

这个方法调用之后意味着会在操作系统里面划出一块缓冲区(direct memory),这块操作系统划出的区域Java代码可以直接访问——换句话说这块内存系统可以用,Java代码也可以用

好处:

  • 少了一次缓冲区的复制操作,速度就更快

请添加图片描述

**存在内存溢出:**java.lang.OutOfMemoryError: Direct buffer memory

/**
 * 演示直接内存溢出
 */
public class Demo1_10 {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        //                  jdk8 对方法区的实现称为元空间
    }
}
6.2 分配和回收原理
  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程(守护线程)通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值