JVM 堆和方法区、运行时常量池的定义与详解

Heap 堆的定义

定义 :通过 new 关键字,创建对象都会使用堆内存

特点

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

堆内存溢出

使用下面代码进行演示

import java.util.ArrayList;
import java.util.List;

public class heap_overflow {

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

在这里对代码进行解释,在new一个List对象之后,在while true死循环当中,每一次都会有一个值加入到list集合当中,在这里list的作用区间在try这个里面,也就是说当这个try代码块还没有跑完,这个list是不会被垃圾回收机制进行回收的,并且字符串对象同理,是不会被回收的。所以代码只循环了25次就会抛出异常 java.lang.OutOfMemoryError: Java heap space 这个时候因为内存已经占满。

在这里插入图片描述
在这里我们也可以设置堆内存的大小参数,使用 -Xmx 进行设置,我们给Xmx设置为16m,再进行测试。在这篇文章有怎么 设置参数 (eclipes)

在这里插入图片描述
堆内存诊断

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

在这里使用一段代码进行测试:

public class heap_memory {
	public static void main(String[] args) throws InterruptedException {
		System.out.println("1...");
		Thread.sleep(30000);
		byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
		System.out.println("2...");
		Thread.sleep(20000);
		array = null;
		System.gc();
		System.out.println("3...");
		Thread.sleep(1000000L);
	}
}

在idea当中进行查看:先演示这个jmap

在这里插入图片描述
而jconsole是一个图形界面工具,同上所示,打开后输入命令jconsole,选择当前运行的这个java程序,建立连接,

在这里插入图片描述
建立安全连接失败,建立不安全连接。可以仔细的看到当前堆内存的使用。

在这里插入图片描述
还有一个工具 jvisualvm 和 jconsole 差不多

在这里插入图片描述
Method Area 方法范围

Java虚拟机具有一个在所有Java虚拟机线程之间共享的方法区域。该方法区域类似于常规语言的编译代码的存储区域,或者类似于操作系统过程中的“文本”段。它存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法。

方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但是简单的实现可以选择不进行垃圾回收或压缩。该规范没有规定方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小的,或者可以根据计算的需要进行扩展,如果不需要更大的方法区域,则可以缩小。方法区域的内存不必是连续的。

Java虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,并且在方法区域大小可变的情况下,可以控制最大和最小方法区域大小。

Run-Time Constant Pool 运行时常量池的定义

运行时间常数池是的每个类或每个接口的运行时表示constant_pool在表class文件。它包含多种常量,从编译时已知的数字文字到必须在运行时解析的方法和字段引用。运行时常量池的功能类似于常规编程语言的符号表,尽管它包含的数据范围比典型的符号表还大。

每个运行时常量池都是从Java虚拟机的方法区域分配的。当Java虚拟机创建类或接口时,将为该类或接口构造运行时常量池。

以下异常条件与类或接口的运行时常量池的构造有关:

创建类或接口时,如果运行时常量池的构造所需的内存超过Java虚拟机的方法区域中可用的内存,则Java虚拟机将抛出OutOfMemoryError。

组成

在这里插入图片描述
方法区内存溢出(与jdk版本有关)

  • 1.8 以前会导致永久代内存溢出
  • 1.8 之后会导致元空间内存溢出

使用以下代码段进行测试:一致生成对应类的二进制编码,并且进行处理。

package function;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
public class function_overflow extends ClassLoader{
    public static void main(String[] args) {
        int j = 0;
        try {
            function_overflow test = new function_overflow();
            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);
        }
    }
}

在这里由于是jdk1.8的版本内存使用的是元空间内存,而元空间内存的大小和本机的物理内存有关,在这里进行设置元空间内存的大小,使用-XX:MaxMetaspaceSize=10m 设置为10m,其他参数也可。再次运行代码会发现跑出来异常。java.lang.OutOfMemoryError: Compressed class space 也就是导致了内存空间不足。

在这里插入图片描述
反编译class文件

反编译的二进制字节码包含了(类基本信息,常量池,类方法定义,包含了虚拟机指令)

使用一个简单的helloworld的java代码,运行之后会生成.class文件,在这个文件的路径下,使用java提供的一个工具javap -v进行反编译查看详细信息。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

StringTable 字符串常量池

先定义几个简单的String类型的变量:常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象,当执行的时候就会被加载进去。

public class test {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b";
    }
}

执行代码,使用Javap -v对class文件进行反编译:可以看到参数会进行一起存储,存储在StringTable当中。

在这里插入图片描述
问1:在上述代码当中添加一个s4变量,判断 s3 和 s4 是否相等

 String s4 = s1 + s2;
System.out.println(s3 == s4);

答案是false,在这里 s3 是已经存在的一个对象,而 s4 是 s1 和 s2 相加得到的,这里的 s4 会存放在堆中。虽然值是相等的,但是所对应的地址是不一样的。

问2:添加一个字符串对象s5,进行判断。

String s5 = "ab";
System.out.println(s3 == s5);

在这里,s3 变量已经存在,值为 ab,这个时候代码进行编译的时候会去找 ab 这个值,可以找到,s5 的地址会指过去,所以 s3 和 s5 的地址是一样的,故 s3 == s5

StringTable 特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    1. 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    2. 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回

StringTable 存在的位置

StringTable在jdk版本不同而存在位置不同。在1.8之后存在heap堆当中,在1.8之前存在于常量池当中。

在这里插入图片描述
使用以下代码进行测试

package StringTable;
import java.util.*;
public class position {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

在jdk6下设置 -XX:MaxPermSize=10m 设置永久代的大小。
在jdk8下设置 -Xmx10m -XX,在这里演示一个jdk1.8版本,在这里本地虚拟机的堆内存是够大的,我们使用-Xmx设置堆内存大小。再运行代码,出现报错,java.lang.OutOfMemoryError: GC overhead limit exceeded,在这里超过了GC垃圾回收机制的设置,

在这里插入图片描述
我们需要添加参数-XX:-UseGCOverheadLimit进行关闭该设置,之后再运行代码,发现提示报错信息是:java.lang.OutOfMemoryError: Java heap space 是由于堆内存不足导致的报错。

在这里插入图片描述

StringTable 垃圾回收

-Xmx10m 设置堆的大小
-XX:+PrintStringTableStatistics 打印StringTable内存的值
-XX:+PrintGCDetails -verbose:gc 打印垃圾回收的详细信息等

使用下面的代码进行测试:并且将上述的参数添加给jvm。

public class GC {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

在这里循环了十万次,也就说会存十万个值到堆当中,在前面还设置了堆的大小,在这里很显然是存不下的,所以在这里就会触发垃圾回收机制,将没有用的对象进行了垃圾回收,所以在下图当中只有13317个对象存在。

在这里插入图片描述
StringTable 性能调优

使用一段测试代码,读取一个文件的内容,在这个文件当中有479830条数据,再进行读取,查看用时。测试代码如下:

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class performance_tuning {
    public static void main(String[] args) throws IOException {
        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;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }
    }
}

在这里插入图片描述
添加一个虚拟机参数 -XX:StringTableSize=1009 这个参数的最小值就是1009,设置为最小值,再一次运行代码查看所需时间。大概就需要9秒

在这里插入图片描述
所以说:对StringTable性能调优可以 调整 -XX:StringTableSize=桶个数

StringTable 性能调优(案例)
使用一段代码进行测试,与上述代码基本一致,在每一次读取文件当中的单词之后,将内容进行添加到一个list当中,

package StringTable;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class performance_tuning_two {
    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);
                    //address.add(line.intern());
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

之后我们进行jconsole当中查看内存等占用,可以看到内存占用了300多m

在这里插入图片描述
我们修改前面的代码,将添加之前从运行时常量池当中进行检查是否存在,加上一个intern方法,address.add(line.intern()); 再次打开jconsole进行查看内存占用,在这里大概只会占用大概200m的内存,在intern方法进行检测,很有效的避免了内存占用的问题。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Modify_QmQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值