JVM day01 JVMJVM内存结构直接内存

Posted halulu.me

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM day01 JVMJVM内存结构直接内存相关的知识,希望对你有一定的参考价值。

JVM

定义:

JVM(Java Virtural Machine),即 java虚拟机。
是java的class文件的运行环境(java二进制字节码的运行环境)

好处:

1、一次编写,到处运行
2、自动内存管理,垃圾回收机制(自动释放因编码不当的内存,减少负担)
3、数组下标越界的越界检查(防止数组越界覆盖其他代码的内存)
4、多态(面向对象编程)

比较:

常见的JVM:

OpenJDK、HotSpot

JVM构成:
在这里插入图片描述

JVM指令:

JVM指令经过解释器解释成为机器码,然后机器码就可以交给CPU执行。

JVM内存结构

1、程序计数器(线程私有)

Program Counter Register(程序计数器,寄存器)
程序计数器是java对物理硬件的抽象,它在物理上是通过寄存器实现的,寄存器是CPU组件中读取速度最快的单元。
读取JVM指令是很普遍的,所以jvm就把CPU中的寄存器当作程序计数器,让它存储指令地址。

作用:

在指令的执行过程中,记住下一条指令执行的地址。

流程:

1、JVM指令经过解释器形成机器码
2、机器码交给CPU执行的同时,程序计数器会记录下一条执行执行的地址
3、第一条指令执行完后,解释器会从程序计数器里面获取下一条指令的执行地址。

在这里插入图片描述

特点:

(1)线程私有

时间片:
假设有2个线程,线程1和线程2.
多个线程运行的时候,CPU会有一个调度器组件给每个线程安排一个时间片,当线程1的时间片执行完的时候会去执行线程2,线程2的时间片用完了就再执行线程1。

线程在切换的过程中,程序计数器是各自线程私有的(每个线程都有各自的程序计数器)。

(2)不会存在内存溢出(JVM规范中唯一一个)

2、虚拟机栈(线程私有)

栈的数据结构:先进后出。
每个线程拥有各自的虚拟机栈。(线程运行时需要的内存空间)
栈内是由多个栈帧组成(每个方法运行时需要的内存就是一个栈帧)

设定栈内存
在这里插入图片描述

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

垃圾回收不会也不需要涉及栈内存。栈内存是每个方法调用的时候产生的栈帧内存的组合。当方法调用完毕的时候,属于该方法的栈帧就会被弹出,不需要垃圾回收机制来释放内存。(栈内存会自动弹出)

(2)栈内存的分配越大越好吗?

物理内存是固定的,当栈内存越大的时候,虚拟机栈可存在数量就变少,也就是说线程的数量会相对减少(50M分给1M的栈,有50个,但是分给2M的栈,就只有25个)。栈内存分配大了,只是能够进行更多次的方法递归调用,并不会增强运行的效率,反而会影响线程的数量,所以一般采用默认的栈内存大小就行。

(3)方法内(栈帧)内部的局部变量是线程安全的吗?

1、如果方法内的局部变量没有逃离方法的作用范围,那么它就是线程安全的。
2、如果局部变量引用了对象,或者的局部变量得数据来源于参数(外部成员变量),或者Return结果被外部变量引用的时候就得考虑线程安全得问题。

(4)栈内存溢出

java.lang.StackOverFlowError

a、递归导致的栈内存溢出(栈帧多)

jackson-databin的包中,jackson数据的转换有可能递归造成栈内存溢出

public class App {
    private static int count ;

    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }
    private static void method1() {
        count++;
        method1();
    }
}

b、栈帧大导致的内存溢出(栈帧大)

栈帧大的情况并不常见,一个int类型的数据才4B,要达到默认的1024KB,需要有很多的int类型的数据或者其他的基本数据类型。

3、线程运行诊断

(1)CPU占用高

运行java代码:

1、javac xxx.java
nohup java xxx & (nohup让class文件在后台运行)

定位线程

2、top(定位哪个进程对cpu占用高)
3、ps H -eo pid,tid,%cpu | grep 进程id (定位那个线程对cpu占用高)
4、jstack 进程id (据线程id找到有问题的线程,进而找出具体原因。)
jstack输出的线程id是十六进制的,所以原线程id需要转换。

(2)迟迟返回不到结果

原因:可能发生死锁。

步骤与上面的一致。

4、本地方法栈(线程私有)

本地方法栈(Native Method Stacks) :java虚拟机调用本地方法时提供的内存空间。

java有一定限制的,有时候不能够直接与操作系统的底层打交道,所以需要用c或者c++语言编写的本地方法与操作系统底层的API打交道。java代码可以间接地通过本地方法调用底层的功能。

例如:

Object类中wait()、clone()、hashcode()、notify()都是native的。

5、堆(线程共享)

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

特点:

1、线程共享,所以需要考虑线程安全的问题
2、有垃圾回收机制

堆内存溢出

java.lang.OutOfMemoryError: Java heap space

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

修改对内存空间:
-Xmx8m
排查错误的时候建议设置内存空间小一点,尽早发现错误。

堆内存诊断

1、jps工具

查看当前系统中有那些java进程

2、jmap

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

3、jconsole

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

垃圾回收以后,内存占用依旧很高

由于编程失误导致一些对象始终被引用,使得该类对象无法被垃圾回收。
使用jvisualvm工具

6、方法区(线程共享)

1、方法区是线程共享的内存区域。
2、方法区存储了类(成员变量、成员方法、构造器、方法)、运行时常量池、类加载器的信息。
3、方法区在虚拟机启动的时候被创建。
4、方法区逻辑上时堆的组成部分,但是不同的jvm厂商不一定将方法区定义在堆上,它有一个别名叫做Non-Heap(非堆)。
5、HostPot虚拟机,在jdk8之前方法区又称为永久代,永久代使用了结构内存的一部分作为方法区。jdk8之后成为元空间,元空间并不是使用结构内存,而是使用了本地内存。

在这里插入图片描述

方法区内存溢出

1、1.8以前导致的永久代内存溢出

-XX:MaxPermSize=10m
java.lang.OutOfMemoryError: PermGen space

2、1.8以后导致的元空间内存溢出

元空间使用的是本地内存,默认是没有上限的。
-XX:MaxMetaspaceSize=10m
java.lang.OutOfMemoryError: Compressed class space

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

3、场景

spring、mybatis等用到cglib动态生成代理类都有可能造成方法区的内存溢出。
cglib的底层就是使用ClassWriter类来动态生成类的二进制字节码的。

方法区运行时常量池

1.8之前运行时常量池包含StringTable,1.8之后StringTable移到堆内存。

常量池

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

二进制字节码(类基本信息,常量池,类方法定义(包含虚拟机指令))
类方法定义中虚拟机指令的地址指向常量池(查表)
astore往LocalVariableTable存数据,aload往LocalVariableTable取数据
javap工具反编译字节码(javap -v xxx.class)

运行时常量池

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

7、stringtable(垃圾回收)

public class Demo1_22 {
    public static void main(String[] args) {
        String s1 = "a"; 
        String s2 = "b";
        
        String s3 = s1 + s2;
        //底层是:new StringBuilder().append("a").append("b").toString()  
        //相当于new String("ab")
        String s4 = "ab";
       	boolean b1 = s4 == s3; // false

		String s5 = "a" + "b"
		//ldc String ab 直接直接找到ab,而不是分开找
		//javac在编译期间进行优化,结果已经在编译期间确定为ab
		boolean b2 = s5 == s4; // true
        
    }
}

1、常量池中的信息,都会被加载到运行时常量池中,这时a只是常量池中的符号。
2、只有a被执行的时候,才会经过虚拟机指令ldc ,把a符号变为“a”字符串对象(惰性)
3、转化为字符串对象之后,会准备好一块空间StringTable,在串池里面找字符串对象a,如果a没有,就把a存进串池,反之亦然。
4、StringTable俗称串池,数据结构是hash表,长度是固定的,不可扩容。

注意:

1、String s5 = “a” + “b” 会便成“ab”。 (字符串常量的拼接原理是编译期的优化)
2、String s3 = s1 + s2; (字符串变量的拼接原理是StringBuilder(1.8))
底层是:new StringBuilder().append(“a”).append(“b”).toString()
相当于:new String(“ab”)
3、StirngTable字符串是延迟加载的,即字符串是一个一个放进串池的。(第一次调用时,才会被放到串池,遇到相同的,就不放入串池)

intern方法

1、可以使用intern方法,主动将串池中还没有的字符串对象放入串池。
2、1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回。
3、1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会把此对象复制一份放入串池, 会把串池中的对象返回。

案例1

//  [ "a", "b","ab"]
    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();
        //ab已经存在
        // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

        System.out.println( s2 == "ab"); // true
        System.out.println( s == "ab" );  //true ,“ab”是s放进去的,所以相同。
    }

案例2

    //  ["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();
        //将new String ("ab")中的ab放进串池
        // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

        System.out.println( s2 == "ab"); // true
        System.out.println( s == "ab" );  //false ,“ab”不是s放进去的,所以不相同。
    }

区别在于串池中的字符串对象是由谁放进去的

StringTable的位置

1、1.6的时候StringTable是在永久代中
2、1.6之后在堆内存中。
在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit

StringTable垃圾回收

-Xmx10m
-XX:+PrintStringTableStatistics
-XX:+PrintGCDetails-verbose:gc

/**
 * 演示 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 < 10000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

StirngTable性能调优

StirngTable的性能是跟大小相关的,如果hash表的桶buckets的个数比较多,那么元素就比较分散,hash碰撞的几率就比较少,查找的速度就增快,反之亦然。

1、桶的数量越多,速度就越快

-Xms500m
-Xmx500m
-XX:+PrintStringTableStatistics
-XX:StringTableSize=1009(设置桶的数量)

2、考虑字符串对象放入串池中

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());
                    //address.add(line)
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();


    }
}

address.add(line.intern())(放入串池中)
在这里插入图片描述

address.add(line)(不放入串池)

在这里插入图片描述

直接内存

Direct Memory 直接内存,操作系统的内存。
常见于NIO操作时,用于数据缓冲区
分配和回收成本较高,但读写性能高(直接使用系统内存)
不受JVM内存回收管理
ByteBuffer类就是使用直接内存

1、读写性能高

IO读写
在这里插入图片描述

数据存放在2个缓冲区,造成不必要的数据的复制

直接内存读写

在这里插入图片描述

Direcat Memory直接内存,系统和java共享,数据只需要复制1份数据。

2、直接内存不受垃圾回收管理(内存溢出)

直接内存溢出
java.lang.OutOfMemoryError: Direct buffer memory

3、直接内存底层的释放原理

直接内存的分配和释放是通过unsafe对象来进行管理,需要主动调用unsafe的freeMemory方法才能释放直接内存。
垃圾回收只能释放java内存,不需要手动释放内存

ByteBuffer垃圾回收底层就是使用cleaner(虚引用类型PhantomReference,当关联的对象被垃圾回收时,cleaner会调用虚引用对象的clean方法)来监测ByteBuffer对象,
当ByteBuffer被垃圾回收掉时(ByteBuffer是java对象,可被垃圾回收),由RefenceHandler线程(不是主线程)通过cleaner的clean方法调用unsafe对象的freeMemory方法释放内存

-XX:+DisableExplicitGC禁用显示的垃圾回收,让System.gc()无效
System.gc()显示垃圾回收,会回收新生代和老年代(full gc),会造成程序暂停时间比较长

-XX:+DisableExplicitGC会对直接内存的释放造成较大的影响。这是因为如果不显示回收ByteBuffer,那么在ByteBuffer被真正的垃圾回收掉的这段期间,直接内存会占用会大的空间,并不会被主动释放。
解决 :不使用System.gc(),直接使用unsafe对象的FreeMemory()方法主动释放内存。

以上是关于JVM day01 JVMJVM内存结构直接内存的主要内容,如果未能解决你的问题,请参考以下文章

转JVM内存模型

JVMJVM内存结构

JVM 内存结构

JVM运行时内存结构

性能测试三十四:jvm内存结构(栈堆永久代)

不懂这些JVM知识,真不敢说是大数据开发!