JVM内存结构总结

Java Virtual Machine:

    Java二进制字节码的运行环境 -> 也可以说是Java的运行环境

    好处:

        一次编写,到处运行的基石,屏蔽各个平台的字节码等差异,对外提供了一个一致的环境

        JVM中有一个内存管理机制,提供了垃圾回收功能

        提供了数组越界异常的判断

        多态的支持

        

    比较:

        jvm:java虚拟机,作为java最底层的支持,屏蔽了字节码等运行平台相关的东西

        jre:jvm+基础类库,集成了java的虚拟机和基础的类库,类似集合类、线程类等

        jdk:jre+编译工具,集成了编译工具之后才能对高级语言编译成机器语言

        

    学习jvm可以理解底层的实现原理

    

    常见的jvm:

        Oracle:Hotspot

        Eclipse:OpenJ9

        下面知识基于HotSpot

    

    学习路线:

        ClassLoader

        JVM内存结构:

            方法区

            堆

            虚拟机栈

            程序计数器

            本地方法栈

        执行引擎:

            解释器

            JIT及时编译器

            GC垃圾回收

        

    

    1.程序计数器(Program Counter Register)

        作用:

            java源代码 -> 二进制字节码(jvm指令) -> 通过解释器解释成机器码 -> 交给CPU处理

            程序计数器就是在指令运行的时候记住下一条jvm指令的地址

            根据地址信息就可以寻址并执行

            当执行完此地址的指令的时候,就会把下一条指令地址存入程序计数器

            

            元件为寄存器

            

        特点:

            线程私有:

                java是支持多线程的,CPU的调度器会分配时间片给不同线程,没执行完就暂存

                线程切换的过程中,会用到程序计数器来记录最后一条指令的地址,并且标识自己属于某线程,以做到线程私有

                每一个线程都有属于自己的程序计数器

            

            唯一一个不会存在内存溢出的区域

            

    2.虚拟机栈:

        即每一个线程运行时所需要的一个内存空间,给每一个线程都划分

        如果有多个线程的话,就有多个虚拟机栈

        

        虚拟机栈由栈帧组成,一次方法调用对应一个栈帧,内部包含了参数、局部变量和返回地址等数据,一个虚拟机栈可以有多个栈帧,像递归就是有多个栈帧

        在每个虚拟机栈只能由一个活动栈帧,即一个线程内只有一个方法正在执行

        

        问题辨析:

            1.垃圾回收是否涉及虚拟机栈?

                不涉及,每次栈帧运行完都会自动回收

            2.栈内存越大越好吗?

                -Xss可以指定栈内存

                Linux、mac、Oracle默认都是1M,Windows取决于它的虚拟内存

                栈内存和线程数成反比:因为物理内存都是一定的,栈内存*线程数=物理内存

                并不是越大越好

            3.方法内的局部变量是否线程安全?

                局部变量不会,就算多个线程调用同一个方法,由于每个线程都会生成一个栈,调用时都会生成一个栈帧,而在栈帧里局部变量是私有的,所以不会影响

                但共享的数据可能会造成线程不安全,例如static变量,可以使用ThreadLocal来控制

            4.线程公有的需要考虑线程安全,线程私有的不需要考虑线程安全

                传入和传出的参数需要考虑线程安全问题,因为可能有别的方法调用它

                即如果逃离了方法的作用范围,就要考虑线程安全问题

 

        栈内存溢出:

            栈帧过多,导致栈内存溢出:

                栈的大小是固定了,但是和递归一样不断调用的话,就会产生栈内存溢出问题,抛出StackOverFlowError的异常

            

            栈帧过大,导致栈内存溢出:

                如果一个栈帧的占用内存过大,也会导致栈内存溢出,就几个栈帧九八内存塞满了

 

            循环引用导致栈内存溢出:

                如果有两个类相互注入,比如学生类有学院类对象,学院类有学生类对象,就会产生循环引用错误,抛出异常

                如果是使用JSON转换产生的错误,可以使用@JsonIgnore注解,忽略属性对象类中的转换

 

        线程运行诊断:

            案例一:CPU占用高

                定位: top命令可以查看哪个进程对cpu占用过高, ps H -eo pid,tid,%cpu | grep 线程id

                Jdk定位:jstack 进程id,可以列出线程中的状态,可以通过nid判断是哪个线程占用高

                

            案例二:长时间没结果

                有可能是多线程造成的线程死锁,导致无法出现结果

                可以使用以上的Jdk定位,查看问题

                程序死锁实例:两个线程都使用了synchronized锁住了资源,但是由于互相争抢导致两边都没有释放

 

            实际开发中,都是先定位,然后再解决问题

 

3.本地方法栈:

    有时候Java不能和底层的打交道,但是可以通过调用例如C++等本地方法来进行交互

    例如:Object有一个native方法clone(),这个方法并没有实现,就是使用了本地C/C++来进行了实现

 

4.堆

    我们使用new来创建的对象都会放在堆里面,创建对象都会使用堆内存

    特点:

  1. 他是线程共享的,堆中的对象都需要考虑线程安全的问题
  2. 有垃圾回收机制

堆内存溢出:

        就算有垃圾回收机制,但是如果现在还是有人在使用,那么堆中对象也不能作为垃圾进行回收

        如果不断产生新对象,达到一定数量,就会导致堆内存耗尽,抛出OutOfMemoryError错误

        可以使用-Xmx指定堆空间大小

    堆内存诊断:

        Jps:查看当前系统有哪些java进程

 

        Jmap:查看某个时刻堆内存占用情况 idea控制台输入: jamp-heap id

            程序新创建的对象会放在一个叫Eden的空间

 

        Jconsole:GUI界面,功能很多,还可以持续查看堆内存占用情况 idea控制台输入: jconsole

 

        Jvisualvm:GUI界面,可以检测和查看具体信息,使用界面内使用dump可以堆转储,定位当前时刻的所有信息

            

    案例:垃圾回收之后,线程占用还是很高

        先定位,后处理

        使用jvisualvm工具,使用GUI界面进程查看具体的信息

        

5.方法区

    JVM规范对于方法区的定义:方法区是所有jvm线程的共享的区域,存储了和类结构相关的信息:成员变量、成员方法、构造方法等。他在虚拟机创建的时候被创建,它逻辑上是堆的组成部分,但是各大厂商实现不同。方法区也会有内存溢出的情况。

    

    在1.6里,使用了永久代(perGem)的方式实现。而在1.8里面,使用了元空间本地内存(Metaspace)实现。

 

    方法区内存溢出:

        拓展:类加载器ClassLoader可以用于加载类的二进制字节码。可以使用defineClass方法触发加载字节码成一个类(加载)

            Classwriter作用就是生成类的二进制字节码。可以使用vist方法生成类的字节码(生成)

        

        可以使用-XX:MaxMetaspaceSize指定元空间内存大小

        可以使用类生成器+类加载器的方法新建一个类,如果类新建过多,就会产生方法区内存溢出的问题,导致OutOfMemoryError

        

    内存溢出场景:

        Spring:

Cglib动态生成代理类,详见之前文档中的aop

 

        Mybatis:

            Cglib动态生成mapper接口(实现类)

 

    运行时常量池:

        在运行程序之前,java会把当前的类编译成二进制字节码,包含了类的基本信息、常量池、类方法定义,字面量信息,还包含了虚拟机指令等数据,然后放入常量池

 

反编译:可以在console使用javap -c 类名.class来反编译一个class文件,用于把二进制字节码转我们能看的代码

 

反编译之后可以看到,有一个常量池的表,它对应了在编译后我们生成的class文件中的指令的操作,指令需要做的操作可以去常量池表中寻找操作方法,都有一一对应的操作

 

在程序运行的时候,常量池中的数值会放入运行中常量池中,并把里面的虚拟地址变为真实地址,以此让机器可以寻址

 

    

 

        查看字节码:

            Constant pool:常量池

            Code:代码块,里面的 #* 标识常量池中的虚拟地址,运行的时候会变成真实地址

            LocalVariableTable:局部变量表,查看当前存储东西的位置

 

    StringTable:字符串池

        字符串池是什么:

        底层是以HashMap来实现的。

        当我们创建字符串符号的时候,就会把字符串符号放入常量池,当发生常量拼接,直接赋值、new字符串对象操作的时候,就会把这个字符串符号放入字符串池,放入对象到堆中,并返回引用给某个对象。如果我们需要去新建一个字符串符号,就会先去字符串池寻找有没有这个字符串,如果有直接返回引用,没有就会放一个新的进入字符串池,并返回引用。

        字符串池和常量池的关系:

        当运行的时候,符号只是存放在常量池中,并不会成为一个String对象。当它开始执行到新建对象赋值这一块的时候,就会把常量池中的符号拿出来,并在StringTable中查找是否有这个对象,如果没有就新建出一个String对象,放入StringTable中,如果有就直接把这个引用拿出来使用

懒加载:使用的时候再创建,然后才放进去串池中

 

        字符串拼接(变量拼接):

        在字节码中可以查看当两个字符串拼接的时候,是先进行了新建StringBuilder对象,拼接完成后进行toString输出。总的来说,当进行字符串拼接的时候,如果拼接是变量的话,就要新建了一个对象出来放入堆中,最后调用了toString方法进行输出,但没有把最后的结果放入字符串池中

 

        字符串池的编译时优化(常量拼接):

        当我们使用常量池中的符号进行拼接的时候(而不是使用对象来拼接),常量池中符号会进行编译时的优化,自动拼接起来,然后会先在字符串池中寻找拼接好的字符串,如果有就直接返回引用并存入堆和字符串池中,如果没有再新建一个对象出来, 返回引用存入堆和字符串池

 

        字符串池中字符串的延迟加载:

        即一开始字符串池并不会有全部的常量,当发现一个没见过的常量的时候,例如new String("a"),就把a加入字符串池,并在其他地方拿出来使用。如果是见过的常量,那么就直接拿引用出来使用。

 

        特性:

        常量池中的字符串仅仅是符号,第一次用的时候才会变为一个对象

        JVM利用串池的机制,来避免重复创建字符串对象

        字符串拼接变量用的是StringBuilder(1.8)

        字符串拼接常量是编译器优化

        可以使用intern方法,主动尝试将串池中还没有的字符串对象放入串池中,如果没有就放入并返回引用,有就不会放入并返回引用

 

        (示例)当现在使用有String s = new String("a") + new String("b");

        先在运行时常量池中加入a符号、b符号,然后要创建String对象,就要在堆中创建一个String对象ab,并同时把a、b放入字符串池。但现在s是一个字符串对象ab,而且由于是变量拼接,没有在里面存放ab这个字符串,所以调用intern方法就可以把这个引用放入字符串池和堆中,这时s的引用等于"ab"的引用。

        如果在里面已经有ab这个字符串了,那么s的引用不会等于"ab"的引用

            

(JDK1.6)在JDK1.6中,会复制一个新的对象放入字符串池。

 

        面试题:


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

//


						// s3是字符串池中的地址,s4是堆中的地址 -> false
System.out.println(s3 == s4);


						// s5是字符串池中的地址 -> true
System.out.println(s3 == s5);


						// s6s4在字符串池中找到了ab,返回字符串池中的引用 -> true
System.out.println(s3 == s6);

String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern();
String x1 = "cd";

//
问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6
System.out.println(x1 == x2);

        // 如果是jdk1.8,返回true,x1是字符串池中的引用,x2是堆中地址放入了字符串池中,那么就是说,两边的地址是相等的

        // 如果是jdk1.6,返回false,x2放入的时候放了一个新的地址给他,而x1获取的时候就获取了新的地址,所以并不相等

 

        位置:

        Jvm1.6之前,字符串池是放在永久代中,即方法区的常量池里面,而这会导致垃圾回收效率低下。而在Jvm1.6之后,字符串池的位置就从永久代变成了堆中。

 

        StringTable垃圾回收:

        当字符串池的内存不够的时候,就会触发垃圾回收机制,然后GC会把字符串池中已经没用的字符串地址清除,以腾出空间给下面的地址使用。

    

        StringTable调优:

        场景:使用一个List存放一个480000个词的字典,而这480000个单词都要放入字符串池中

        速度调优方案:调整-XX:StringTableSize=桶个数

        由于StringTable底层是由HashMap实现的,而HashMap的性能和他的大小相关。如果HashMap桶多了,那么就表示元素就比较分散,hash碰撞的几率就小,查找速度变快。反之就会变慢。可以通过调整桶的个数,来调整hash碰撞的几率,从而加快/减慢储存的速度。

        看回场景:如果我们桶的数目改变了,那么读取速度就会收到相应影响。

            

        内存调优方案:考虑当前处理的字符串可以从池中获取

        当使用采用intern方法,让同一字符串仅入池一次,其余重复的就拿池内已经存放了的地址。

        看回场景:如果我们使用了intern方法,那么在字符串池中,对于每一个相同的字符串都只会存在一个地址,而对比原来没有使用intern的时候,字符串池内存占用会更小。

 

 

        扩展:HashMap性能

        HashMap存入数据的时候,会计算key的hashcode,然后放入一个数组里面。如果此时,计算出来的hashcode已经在里面了,那么同一hashcode的元素就会组成一个链表。这个数组每一个位置叫一个"桶"(bucket),如果存放的数据多了,hash碰撞几率增加,那么在同一hashcode里面就需要链表顺序查找元素,这样就很慢了。反之就会变快。 可以通过负载因子,平衡桶和链表的长度,默认为0.75

    

 

6.直接内存Direct Memory

    常见于NIO(块I/O)操作中的ByteBuffer,ByteBuffer里面就是使用了数据缓冲区,也就是直接内存。ByteBuffer不归JVM管理,而是属于操作系统内存

    由于是操作系统内存,所以它的分配和回收成本较高,但同时读写能力强

 

    内存溢出:

        由于它不受控与JVM的GC,所以当调用它的时候,将会产生内存溢出的问题

    

    内存释放原理:

        由于它占用的是操作系统的内存,所以当需要释放的时候,JVM会调用本地Unsafe对象的freeMemory方法进行内存释放。JVM的垃圾回收只能对JVM内部例如堆内存、元空间等位置进行释放回收。详情可以看DirectByteBuffer类源码,它调用Cleaner(虚引用)来监测并进行内存释放。

 

    

    拓展:IO和NIO

    IO属于流操作,一次只能与外界交互一个字节,当未完成的时候会阻塞IO。

    NIO属于块操作,以块的方式与外界交互,可以规模化的传输数据,但缺少了IO的简单性。NIO流里面需要在操作系统内存开辟直接内存ByteBuffer作为缓冲区,这个缓冲区连接着系统缓冲区和Java缓冲区,这样Java缓冲区就能直接访问到系统缓冲区的资源。

posted @ 2022-03-28 16:59  Quent1nCn  阅读(26)  评论(0编辑  收藏  举报