JVM内存结构

本文详细介绍了JVM的内存结构,包括程序计数器、虚拟机栈、本地方法栈、堆和方法区。栈内存溢出可能由于递归深度过大导致StackOverFlowError,而堆内存溢出则表现为OutOfMemoryError。方法区存储类信息、常量、静态变量等,1.8后由元空间替代永久代。字符串常量池在1.8后位于堆中, intern操作可优化内存使用。

1. JVM的内存结构

JVM的内存结构主要包括五个部分:程序计数器、虚拟机栈、本地方法栈、堆和方法区,其中程序计数器、虚拟机栈、本地方法栈是线程独有的,而堆和方法区是线程共享的


1.1 程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。是为了线程切换后能恢复到正确的执行位置。如果线程正在执行的是一个Java方法,这个计数器指示的是正在执行的虚拟机字节码指令的地址;如果正在执行一个native方法,计数器则为空(Undefined)。

1.2 虚拟机栈(JVM Stacks)

每个栈由多个栈帧组成,当执行一个方法时会创建一个栈帧,方法的调用到结束对应了栈帧的入栈到出栈每个栈中同一时间只有一个活动栈帧:即同一时间线程中只有一个方法在执行。

栈由多个栈帧组成,栈帧中存放了局部变量表、操作数栈、动态链接和方法出口等信息。

1.2.1 栈内存溢出

当出现递归程度很深的程序时,栈一直进行入栈操作,内存可能会耗尽,此时会出现StackOverFlowError的错误。

    public void stackError(int i){
        i++;
        stackError(i);
    }
    public static void main(String[] args) {
        new StackError().stackError(1);
    }

可以使用-Xss256K来设置栈帧的大小

1.3 本地方法栈(Native Method Stacks)

本地方法:不由java代码编写的方法,用native关键字修饰,不给出实际的方法实现体,大多由操作系统提供。

本地方法栈和虚拟机栈的功能类似,不同点是本地方法栈为本地方法服务。


2.1 堆(Heap)

主要存放对象实例。

2.1.1 特点

①:线程共享;

②:存在垃圾回收。

2.1.2 堆内存溢出

堆内存不够用时出现OutOfMemoryError:Java heap space错误:

    public static void main(String[] args) {
        List<String> list = new ArrayList();
        while(true){
            list.add("heapError");
        }
    }

2.1.3 堆内存的诊断

①:使用jps命令查看哪个java进程出现错误;

②:使用jmap -heap pid查看堆内存的占用情况,类似快照;

③:也可以使用jconsole,它是图形界面的,可以连续监测,类似视频;

2.2 方法区(Method Area)

存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。

1.7前使用永久代(PermGen)实现,在1.8后使用元空间(Metaspace)实现。

使用-XX:MaxMetaspaceSize=8m设置最大元空间大小。

2.2.1 元空间溢出

OutOfMemory:metaspace

2.2.2 常量池

存放字面量和符号引用。

2.2.3 运行时常量池

当类的字节码被加载时会将此类的常量池信息放入运行时常量池,出了保存符号地址,还保存了翻译出来的真正的内存地址。

2.2.4 串池String Teble

是一个HashTable,即桶的个数是它的大小,它主要存放字符串常量。

在java里字符串的构造主要有两种:

①:String str = new String("123");

②:String str = "123";

第一种方式创建的对象被存放在堆内存中,每次调用都会产生一个新的对象

第二种会先从串池中找是否有和"123"相同的字符串,如果有则将串池中的字符串引用赋给str,若没有则将"123"加入串池中,然后将加入到串池中的"123"的引用赋给str。

我们来验证一下:

        String str1 = "123";
        String str2 = new String("123");
        System.out.println(str1 == str2);

运行以上代码:

拼接字符串的本质

先来看下面这段代码:

        String str1 = "q";
        String str2 = "w";
        String str3 = "qw";
        String str4 = str1 + str2;
        System.out.println(str3 == str4);

.我先运行

为什么是flase?我们用javap -v StringTable.class命令看看二进制字节码中的JVM指令:

可见字符串拼接由StringConcatFactory中的makeConcatWithConstant实现,我找到了makeConcatWithConstant方法,里面有STRATEGY枚举,这是实现拼接的六种策略:

    private static enum Strategy {
        BC_SB,
        BC_SB_SIZED,
        BC_SB_SIZED_EXACT,
        MH_SB_SIZED,
        MH_SB_SIZED_EXACT,
        MH_INLINE_SIZED_EXACT;

        private Strategy() {
        }
    }
switch(STRATEGY) {
    case BC_SB:
        return StringConcatFactory.BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, StringConcatFactory.Mode.DEFAULT);
    case BC_SB_SIZED:
        return StringConcatFactory.BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, StringConcatFactory.Mode.SIZED);
    case BC_SB_SIZED_EXACT:                
        return StringConcatFactory.BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, StringConcatFactory.Mode.SIZED_EXACT);            
    case MH_SB_SIZED:
        return StringConcatFactory.MethodHandleStringBuilderStrategy.generate(mt, recipe, StringConcatFactory.Mode.SIZED);
    case MH_SB_SIZED_EXACT:    
        return StringConcatFactory.MethodHandleStringBuilderStrategy.generate(mt, recipe, StringConcatFactory.Mode.SIZED_EXACT);
    case MH_INLINE_SIZED_EXACT:
        return StringConcatFactory.MethodHandleInlineCopyStrategy.generate(mt, recipe);
    default:
        throw new StringConcatException("Concatenation strategy " + STRATEGY + " is not implemented");
}

可以发现前五种都是和StringBuilder有关,而我们采用的是默认方式,所以应该是第一种StringConcatFactory.Mode.DEFAULT。在jdk8中用"+"连接的字符串变量是纯用StringBuilder实现的,通过使用append方法追加字符串,toString方法创建新的字符串,所以用"+"连接的字符串本质上还是用new创建了新的String对象,所以它连接后的字符串属于堆内存

再看一个代码:

    String str1 = "q" + "w";
    String str2 = "qw";
    System.out.println(str1 == str2);

对于常量字符串的拼接,javac在编译期就进行了优化处理,拿到的字符串是"qw",所以没有makeConcatWithConstant方法的调用。

串池的位置?

jdk1.7是在永久代的常量池中,在jdk1.8以后是在堆中。

字符串的intern操作

调用intern的字符串,若串池中没有该字符串则将该字符串加入串池(调用者本身不加入串池),并将串池中的对象返回。环境是jdk11

String str1 = new String("qw");
String str2 = "qw";
System.out.println(str1 == str2);
str1.intern();
System.out.println(str1 == str2);

将intern后的变量赋给str1:

String str1 = new String("qw");
String str2 = "qw";
System.out.println(str1 == str2);
str1 = str1.intern();
System.out.println(str1 == str2);

串池的性能调优

①:-XX:StringTableSize=1009:改变串池中桶的个数(让每个桶中装的字符串个数适当);

②:重复的字符串进行入池可以节省大量内存。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值