JVM篇(三)之内存结构


JVM内存结构包括 程序计数器,本地方法栈,虚拟机栈,堆,方法区,

程序计数器

java源代码----->编译器----->二进制字节码 .class文件---->解释器----->机器码------>cpu执行

定义

是线程私有的,一块很小的内存区域,作为当前线程的行号指示器,用于记录当前执行的线程的下一条指令地址

作用

程序计数器在指令的执行过程中记住下一条指令执行的地址,解释器通过读取程序计数器中的地址来执行下一条指令,通过寄存器实现,寄存器是cpu读取最快的组件,读取操作比较频繁

特点

  • 线程私有,随着线程的创建而创建,线程销毁而销毁
  • 唯一一个不会存在内存溢出的区域

虚拟机栈

定义

java虚拟机栈,线程运行时需要的虚拟空间:每个栈内有每个栈帧组成,

  1. 每个线程运行时所需要的内存,称为虚拟机栈
  2. 每个栈由多个栈帧组成,每一个栈帧对应着一个方法的调用,也就是说每个方法运行时需要的内存,栈帧内容包括方法参数,局部变量,返回地址
  3. 每个线程只能有一个活动栈帧,对应着当前执行的方法

面试问题

  1. 垃圾回收是否设计栈内存?

不涉及,因为栈内存就是方法一次次调用产生的栈帧内存,而栈帧内存在方法每次调用后自动释放内存,垃圾回收是回收堆内存中的无用对象

​ 2.内存分配越大越好吗?

栈内存 -Xss ,栈内存越大,反而会使线程数变少,因为物理内存是一定的,比如说一个线程使用的是栈内存,一个线程使用了1m内存,物理内存有500m,理论上可以有500个线程同时运行,但是如果每个栈内存时2m内存,只有250个线程同时运行

调大了只是进行更多次的方法递归调用,而不会增强运行效率

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

看一个变量是否线程安全,其实就是看这个变量是线程共享的还是私有的,

  • 如果方法局部变量作用范围没有逃离方法作用范围,每个线程有自己私有的帧内存,局部变量在每个帧中的栈帧存储,所以是线程私有的,即线程安全就是线程安全
  • 但是局部变量引用了对象,并且为返回值返回,就需要考虑线程安全问题,就是线程不安全

栈内存溢出

  1. 栈帧过多,导致栈溢出,比如方法递归调用中,没有设置终止条件,就会导致栈溢出
  2. 栈帧过大,导致栈溢出,一个栈帧大小超过栈内存大小

线程运行诊断

Cpu占用过多

linux命令:

  • 定位:先用top 实时检测cpu运行情况定位哪个进程对cpu占用过高
  • 用ps H -eo pid,tid,%cpu | grep 进程号 定位哪个线程占用过高
  • jstack进程id 定位线程来查看具体出现问题,定位到源代码行数

程序运行时间很长没有结果(死锁现象)

tips:产生死锁的四个条件—>互斥,不可剥夺,请求和保持,循环等待

本地方法栈

定义

线程私有,java在调用本地方法时用到的内存区,当JVM创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是动态链接并直接调用该方法

本地方法:不是使用java语言写的方法

jps查看进程id

jmap命令可以查看堆内存情况

定义

java虚拟机中内存区域最大的一块,几乎所有的实例对象都在这里分配内存

程序计数器,虚拟机栈,本地方法栈都是线程私有区域,堆和方法区为线程共享区域

  • 通过new关键字,创建的对象都会使用堆
  • 有垃圾回收机制
  • 线程共享,考虑线程安全问题

堆内存溢出

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

产生异常

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
	at java.lang.StringBuilder.append(StringBuilder.java:136)
	at com.lsy.Jvm.HeadDemo.main(HeadDemo.java:14)

堆空间虚拟机参数**-Xmx**

堆内存诊断

  1. jps工具
    • 查看当前系统中有哪些java进程
  2. jmap工具
    • 查看对内存占用情况 jmap -heap 进程数
  3. jconsole工具
    • 图形界面的,多功能的检测工具,可以连续加测

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

方法区

定义

  • 存储了类的相关信息,比如类的成员变量,成员方法,以及构造器方法,在虚拟机启动时被创建

  • 在JDK1.6之前,使用永久代作为方法区的实现,永久代包含存储类的信息,类加载器.运行时常量池(包括StringTable)

  • 在JDK1.8之后,永久代废弃,使用元空间 元空间里存储类的信息,类加载器,常量池(不包含StringTable),不占用堆内存,jvm不管理它的内存结构,由本地内存管理,StringTable不再放到元空间部分,被移到heap堆里

在这里插入图片描述

在这里插入图片描述

方法区内存溢出

  • JDK1.8之前会产生永久代溢出

java.lang.OutOfMemoryError:PermGen space

-XX:MaxPermSize = 8m

  • JDK1.8之后会产生元空间内存溢出

java.lang.OutOfMemoryError:Metaspace

-XX:MaxMetaspaceSize = 8m

场景:

  • spring框架
  • mybatis

运行时常量池

程序的运行先编译成二进制字节码,有类的基本信息,类的常量池,类的方法定义

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

在这里插入图片描述

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

StringTable

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

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

  • 字符串变量拼接的原理是StringBuilder(1.8)

  • 字符串常量拼接的原理是编译期优化

    package com.lsy.Jvm;
    
    public class StringTable {
        //常量池中的信息,都会被加载到运行时常量池中,这是a ,b ,ab 都是常量池中的符号,还没有变为java字符串对象
        //ldc #2 会把a符号变为"a"的字符串对象,准备好一块空间StringTable[],变为a字符串对象后作为key去StringTable去找,如果没找到就把a字符串对象放入串池
        // ldc #3   会把"b"变为字符串对象,先去StringTable中去找,
        //ldc #4 会把"a,b"变为字符串对象
        public static void main(String[] args) {
            String s1 = "a";//用到了才会创建
            String s2 = "b";
            String s3 = "ab";
            /*变量字符串拼接,s1和s2为变量,在运行时有可能被修改,使用StringBuilder.append进行动态拼接
            * 先创建了一个StringBuilder,然后调用StringBuiler无参构造,先把s1调用进来,调用append方法,把s2调用进来,调用append方法,之后再toString
            * new StringBuilder.append().append().toString() new String()
            * */
            String s4 = s1+s2;
            //System.out.println(s3 == s4);//false s3是串池中的对象 ,s4是new出来的在堆里面的
            /*常量字符串拼接 在编译期
            * 先在串池中寻找,如果串池中没有就创建
            * s3 =s5 javac在编译期间的优化,结果已经在编译期确定为ab
            * */
            String s5 = "a" + "b";
    
        }
    }
    
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池

    • 1.8将这个字符串对象尝试放入串池中,如果有就不会放入,如果没有就会放入串池,就会把串池中的对象返回

      String s =   new String("a") + new String("b");
              String intern = s.intern();
              System.out.println(intern==x );//true
              System.out.println(s==x );//true
      
        String x = "ab";
              String s =   new String("a") + new String("b");
              String intern = s.intern();
              System.out.println(intern==x );//true
              System.out.println(s==x );//false
      
    • 1.6将这个字符串对象尝试放入串池中,如果有就不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

StringTable位置

JDK1.6之前在永久代,永久代回收效率很低,永久代在fullGC时才会垃圾回收,但是fullGC等到老年代不足时才会触发,触发的实际有点晚

StringTable使用的频率有点晚,如果回收效率不高,就会导致永久代内存不足.

所以JDK1.8之后转变到堆中,在堆中miniGC就会触发垃圾回收,把一些用不到的字符串常量回收

StringTable垃圾回收

当内存空间不足时,StringTable没有被引用的字符串常量也会被垃圾回收

在这里插入图片描述

StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      1737 =     41688 bytes, avg  24.000 //存储字符串对象
Number of literals      :      1737 =    156464 bytes, avg  90.077//字符串常量
Total footprint         :           =    678256 bytes
Average bucket size     :     0.029 
Variance of bucket size :     0.029
Std. dev. of bucket size:     0.171
Maximum bucket size     :         3

在这里插入图片描述

stringTable发生垃圾回收

[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->696K(9728K), 0.0036188 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2744K->704K(9728K), 0.0016845 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2752K->704K(9728K), 0.0041612 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

StringTable性能调优

  • 如果字符串常量个数比较大,可以调试StringTable桶的个数 -XX:StringTableSize 有更好的hash分布,减少hash冲突
  • 考虑将字符串对象入池

直接内存

定义

  • 主要用作NIO(ByteBuffer)操作,用于数据缓冲区
  • 分配回收成本较高,但是读写性能会非常高
  • 不属于jvm内存管理,属于操作系统内存,不受jvm内存回收管理
  • 也会产生内存溢出,oom:direct buffer memory

java本身不提供磁盘读写,要调用磁盘读写必须调用操作系统中的函数,调用本地方法native

从用户态转变为内核态,在内核态cpu读取磁盘内容,磁盘内容先分次读取到系统缓存区,系统缓冲中的数据java不能够运行,所以在堆内存中分一块java缓存区,从系统内存中把数据间接的读取到java缓冲区,反复进行读写操作

不使用直接内存所在问题:有两块缓存区,系统内存和java堆内存,读取的时候涉及到数据要存两份,从系统内存中把数据间接的读取到java缓冲区,反复进行读写操作,这样造成不必要的数据复制,效率不是很高
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值