Java Virtual Machine:java程序的运行时(java二进制字节码的运行环境)
优点
jvm, jre, jdk之间的区别:
JVM整体结构:
JVM 内存结构主要包括:
含义:Program Counter Register 程序计数器(寄存器)
作用:记住下一条指令的执行地址。现实中程序往往是多线程协作完成任务的。JVM的多线程是通过CPU时间片轮转来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。在JVM中,通过程序计数器来记录程序的字节码执行位置。程序计数器具有线程隔离性,每个线程拥有自己的程序计数器。
特点:
含义:Java Virtual Machine Stacks(Java虚拟机栈)
问题:
1、垃圾回收是否涉及虚拟机栈?
答:不涉及,虚拟机栈是每个线程自己使用的内存空间,不需要GC管理。
2、栈内存是否越大越好?
答:不是。虽然栈内存越大调用深度就可以越深,但是由于每个线程对应一个栈内存,则栈内存越大可分配的线程就越少。
3、方法内的局部变量是否是线程安全的?
答:如果方法内的局部变量没有逃脱他的作用范围,则它是线程安全的。如果局部变量引用了对象,并且逃离了方法的作用范围,则需要考虑线程安全问题。
异常:
出现的原因:
调用深度过多导致的栈桢过多超出栈内存溢出
方法中过多的本地变量导致的栈桢多大而引发的栈内存溢出
案例一:CPU占用过多
定位方法
ps H -eo pid,tid,%cpu | grep 进程id
案例二:程序运行很长时间没有结果
Java堆是被所有线程共享的内存区域,所有对象实例以及数组(通过new关键字创建的)都应该在堆上分配。
如果在Java堆中没有内存完成实力分配,并且堆无法再扩展时,Java虚拟机会抛出OutOfMemory异常
其中可以使用-Xmx和-Xms设置堆的最大内存和最小内存。
内存诊断工具:
jps工具:查看当前系统中有哪些java进程
jmap工具:查看堆内存的占用情况
jvirsualvm:可以查看堆中各个对象占用内存的大小
方法区是各个线程共享的内存区域,它用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区是线程共享的
如果方法区无法满足新的内存分配需求时,将抛出OutOfMemory异常
JDK8之前,hotspot使用永久代来实现方法区,使得垃圾收集器可以像管理堆一样管理方法区,省区了专门编写内存管理代码的工作
JDK8及之后在本地内存中实现的元空间(Metaspace)来代替了永久代
方法区内存溢出的主要原因是加载的类过多导致超出内存空间
// 报错:java.lang.OutOfMemory: PremGen space
// 参数:-XX:MaxPremSize=8M
// 报错:java.lang.OutOfMemory: Metaspace space
// 参数:-XX:MaxMetaspaceSize=8M
问题产生的场景:当前许多主流的框架,如Spring、MyBatis对类进行增强时,都会使用CGLib这类字节码技术,当增强的类越多,就需要越大的方法区空间来保证动态生成的新类型可以载入内存。
常量池:就是一张表,虚拟机执行根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里边的符号地址变为真实地址。
StringTable是运行时一个用于存储String常量的HashTable结构区域,不能够扩容。
试题:
1.6版本下:位于永久代
1.8版本下:位于堆中
调整的原因:因为永久代触发GC的条件比较严格,而由于一般程序中都存在大量的过期无用字符串常量导致StringTable十分臃肿并且不能及时进行垃圾回收,所以1.8版本之后将其放在堆中
验证其存放位置从永久代(PermGen)到堆(Heap)的转变方法:不断创建具有引用的字符串常量,看哪个区域报OutOfMemory错误。代码如下:
/**
* 测试StringTable所在的位置
* //当使用UseGCOverheadLimit时,如果GC占用了98%的时间,但是只释放了2%的空间,则报此错误。
* //这里没有使用(-号表示不使用,+号表示使用)
* 1.8:-Xmx10m -XX:-UseGCOverheadLimit
* 1.6: -XX:MaxPermSize=10m
*/
public class StringTableLocationTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int j = 0;
try {
for (int i = 0; i < 300000; i++) {
//将j转化为字符串并放入StringTable中,再将返回的对象引用放入list
list.add(String.valueOf(j).intern());
j++;
}
}catch (Exception e){
e.printStackTrace();
} finally {
System.out.println(j);
}
}
}
1.8版本的结果:
1.6版本结果
测试代码
/**
* 串池GC测试
* 最大堆内存为10m 打印串池统计信息 打印垃圾回收的详细信息:次数、时间等
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class StringTableGCTest {
public static void main(String[] args) {
int j = 0;
try {
for (int i = 0; i < 10000; i++) {
String.valueOf(j).intern();
j++;
}
}catch (Exception e){
e.printStackTrace();
} finally {
System.out.println(j);
}
}
}
当没有在串池中加入字符串常量时,串池中总的字符串个数为1754。并且没有发生GC。
当试图在串池中加入10000字符串常量时,串池中总的字符串个数为7226,只增加了5000多个,因为在这个过程中发生了GC。
如果应用中存在大量的字符串,而且这些字符串中存在大量的重复,那么就可以使用String::intern方法将字符串放入串池,减小字符串对象的个数,进而减少字符串对内存的消耗。
调整 -XX:StringTableSize=桶的个数。StringTable内部采用HashTable的形式进行存储,当HashTable桶的个数非常小时,容易产生哈希冲突,减慢StringTable的存取速度。通过StringTableSize参数可以调整HashTable桶的个数,默认为60013个。
本机系统中的内存
java要读取大文件,首先CPU要切换到内核态,然后调用Native方法将磁盘文件先读入到系统缓存区,然后从系统缓冲区复制到java缓冲区,过程中需要两次复制。
而使用直接内存之后,可以在系统内存中分配一块区域用于存储磁盘文件,并且Java中也可使用DirectByteBuffer作为这块内存的引用进行操作,这样就能在一些场景中显出提升性能。
如果直接内存中存储的数据过大也会导致OutOfMemory异常:
当直接内存使用较多,而且使用-XX:DisableExplicitGC禁用掉代码中System.gc()时,建议直接使用Unsafe对象对直接内存进行管理,示例代码如下: