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)
堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况 jmap - heap 进程id - 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 方法,主动将串池中还没有的字符串对象放入串池
- 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
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方法进行检测,很有效的避免了内存占用的问题。