学习笔记:JVM(一)内存结构

1. 程序计数器

  1. 作用:记住下一条JVM指令的执行地址。
  2. 特点: 线程私有;不会存在内存溢出
  3. 存在于CPU寄存器中Program Counter Register

2. Java虚拟机栈

局部变量表操作数栈方法返回地址动态连接(从运行时常量池中连接)

2.1 Java虚拟机定义

  1. 每个线程运行时所需要的内存,称为虚拟机栈;
  2. 每个栈由多个栈帧,对应着每次方法调用时候占用的内存;
  3. 每个线程只能有一个活动栈帧,对应着当前正在执行的方法。

问题辨析:

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

    不涉及,方法调用完会自动弹出,回收内存。存储的局部变量、方法参数会自动释放。

  • 栈内存分配越大越好吗?
    不是,栈内存越大,线程数会受到限制,因为内存有限,栈内存是线程独享的。

  • 方法的局部变量是否线程安全?
    变量是否是线程安全的,取决于这个变量被多线程共享时,每次运行结果和单线程运行的结果是否是一样的。
    a. 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
    b. 如果局部变量引用了对象,且逃离了方法的作用范围,那么就需要考虑线程安全(基本数据类型不会有这个问题)

2.2 栈内存溢出

  • 栈帧过多会导致栈内存溢出;(递归时候)
  • 栈帧过大会导致栈内存溢出;

2.3 线程运行诊断

案例:cpu占用过多定位
Linux下用top定位哪个进程对cpu的占用过高
ps H -eo pid,tid,%cpu | grep 进程id
(用ps命令进一步定位是哪个线程引起的cpu占用过高)
jstack 进程id
可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号。
案例:程序执行很长时间没有结果

  • 下面程序发生了死锁。
    两个或多个并发进程中,每个进程占有某种资源,同时又在等待其他进程释放它或它持有的资源,才能向前推进状态,这一组进程产生了死锁。
public class Demo {
	    static A a = new A();
	    static B b = new B();
	    public static void main(String[] args) throws InterruptedException {
	        new Thread(()->{
	            synchronized (a) {
	                try {
	                    Thread.sleep(2000);
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }
	                synchronized (b) {
	                    System.out.println("我获得了 a 和 b");
	                }
	            }
	        }).start();
	        Thread.sleep(1000);
	        new Thread(()->{
	            synchronized (b) {
	                synchronized (a) {
	                    System.out.println("我获得了 a 和 b");
	                }
	            }
	        }).start();
	    }
	}

3. 本地方法栈

本地方法栈发挥的作用和虚拟机栈类似,用于native修饰的方法。(一般由C语言编写native方法)

4. 堆

4.1堆的定义

通过 new 关键字,创建对象都会使用堆内存。
特点:
• 它是线程共享的,堆中对象都需要考虑线程安全问题
• 有垃圾回收机制

4.2 堆内存溢出

List集合中一直添加数据,会出现堆内存溢出

4.3 堆内存诊断#

  1. jps工具
    查看当前系统中有哪些java进程
  2. jmap工具
    查看堆内存占用情况 jmap
  3. jconsole工具
    图形界面,多功能的监测工具,可以连续监测
  4. jvisualvm(推荐使用)

案例 垃圾回收后,内存占用仍然很高
使用 jvisualvm 命令进行诊断
使用 堆Dunp 进行对当前线程内存进行转储快照,进而分析为什么垃圾回收后内存还很高

5. 方法区

方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

5.1 定义

5.2 组成

jdk 1.6 对方法区的实现称为永久代
jdk 1.8 对方法区的实现称为元空间

5.3 运行时常量池

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

5.4 方法区溢出

public class Demo1_8 extends ClassLoader { //可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 20000; i++, j++) {
                //ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                //版本号,pubblic,类名,包名,父类
                cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                //生成类,并且返回byte[]
                byte[] code = cw.toByteArray();
                //只会触发类的加载,不会触发链接。。等
                test.defineClass("Class" + i, code, 0, code.length);//class对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m
演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m

场景:框架会产生很多运行时的类,容易导致内存溢出
• spring
• mybatis
都用到cglib

5.5 StringTable(串池)

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);
    }
}

//结果
s4 不等于 s3 //s3是串池中的,s4是通过new对象生成的,其值存在堆中
s3 等于 s5 //
对于 单独的赋值,是对数据的直接到StringTable中取找,如果没有,则创建,有则直接取
对于 s4 = s1+s2 则是使用StringBuilder(),方法进行拼接,结果是一个新的对象,该对象存在堆上面

5.6 StringTable特性

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

  • 1.8 将这个字符对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
  • 1.6 将这个字符对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

5.7 StringTable位置

1.7 的时候StringTable 是在堆空间
1.6 时StringTable是在永久代中 ,永久代是Full GC (老年代空间不足才会触发)才会触发,导致StringTable的回收效率不高,所以在1.7 以后把StringTable 转移到堆中(只需要miner GC 就能触发垃圾回收)

5.8 StringTable 垃圾回收

/**
 * 演示 StringTable 垃圾回收
 * -XX:MaxPermSize=10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo1_7 {
    // 1754
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 500000; j++) { // j=10, j=1000000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

5.8 StringTable 性能调优

• 调整 -XX:StringTableSize=桶个数
• 考虑将字符串对象是否入池
​ 如果字符常量比较多时,可以把桶的个数(StringTableSize)调大,让有更多的hash,减少hash冲突

6. 直接内存

6.1 定义

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

/**
 * 演示 ByteBuffer 作用
 */
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
public class ByteBufferTest {
    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);
    }
}

6.2 分配和回收原理

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值