JVM学习--内存结构

引言

 

程序计数器

程序计数器(Program Counter Register,寄存器),Java源代码运行的第一步就是将源码编译成JVM指令,每一条指令都有对应的执行地址,JVM通过程序计数器来记住每条指令的下一个执行地址,这样就实现的程序代码的执行。

 

 

作用:是记住下一条jvm指令的执行地址

特点:

  • 线程私有,每一个线程都有自己的一个程序计数器
  • 不会存在线程溢出

 Java虚拟机栈

定义

Java虚拟机栈(Java Virtual Machine Stacks)

  • 每个虚拟机运行时需要的内存,成为虚拟机栈。
  • 每个站有多个栈帧(Frame)组成,对应着诶此方法调用时所占用的内存。
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
  • 指令-xss是可以调整栈的大小的

判断方法内的局部变量是否是线程安全的?

  • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。
  • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。

栈内存溢出

  1. 栈帧过多引起栈内存溢出,比如递归过程中没有比较好的跳出递归条件,导致无限递归,此时必然会导致栈内存溢出。
  2. 栈帧过大导致栈内存溢出

线程运行诊断

1.CPU占用过高时

  • 可以用top命令可以找到那个进程对cpu的占用过高,
  • ps H -eo pid,tid,%cpu | grep 进程id 可以定位到该进程,查询出该进程中哪一个线程CPU占用过高
  • jstack 进程号可以看到该进程的整个线程

2.程序运行很长时间没有结果

jstack命令可以看到死锁信息,代码中第一个线程给对象a加了锁,然后休眠,休眠之后在进行锁住b,但是第二个进程已经先把b锁住了,再打算锁住a,此时需要等待第一个线程解锁a。第一个线程现在又要来锁b,需要等待第二个进程解锁b,此时第一个第二个线程互相等待,进入死锁状态。

class A{};
class B{};
public class Demo1_3 {
    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();
    }

}

 本地方法栈

先解释一下本地方法,简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。本地方法栈就是起到为本地方法提供内存空间的作用。

定义

堆(Heap),通过new关键字来创建,创建对象时都会使用堆内存。

特点:

  1. 线程共享,堆中对象都需要考虑线程安全问题。
  2. 有垃圾回收机制
  3. -Xmx指令可以调整堆空间大小

堆内存溢出

public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

堆内存诊断

jps工具

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

jmap工具

  • 查看堆内存占用情况 jmap -heap 进程id

jconsole工具

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

方法区

所有JVM线程共享一个方法区

存储和类结构相关的信息,包括成员变量、方法数据、方法函数、构造器和一些特殊方法。

方法区内存溢出

JDK1.8之前回导致永久代内存溢出

JDK1.8之后会导致元空间内存溢出

-XX:MaxMetaspaceSize=8m指令可以设置方法区内存空间,下面代码使用类加载器循环创建一万个类,导致方法区内存溢出。
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 < 10000; 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();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

运行时常量池

二进制字节码包含:类基本信息、常量池、类方法定义,类方法定义中包括了JVM指令

javap工具可以对.class文件进行反编译,-c参数可以显示出类的详细信息

常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。

运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变成真实地址。

直接内存

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

分配和回收原理:

  1. 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法     
  2.  ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
posted @ 2020-09-22 14:20  s1awwhy  阅读(141)  评论(0编辑  收藏  举报