定义:
Java Virtual Machine - java 程序的运行环境(Java二进制字节码的运行环境)
好处:3
比较:
jvm jre jdk
jvm屏蔽Java代码与底层操作系统之间的差异
jdk+集成开发工具->JavaSE
jdk+应用服务器(如:tomcat)+集成开发工具->JavaEE
方法执行时的每行代码是由执行引擎中的解释器逐行进行执行
方法里面的热点代码(频繁执行的代码)由执行引擎的即时编译器编译
GC:会对堆里面不再被引用的对象进行回收
Program Counter Register程序计数器(寄存器)
二进制字节码 jvm指令 java源代码
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
二进栈字节码 经过解释器 变成机器码 交给cpu执行
程序计数器负责将下一条jvm指令的执行地址告诉解释器
程序计数器在物理上是通过寄存器实现的
栈:先进后出
Java Virtual Machine Stacks (Java 虚拟机栈)
问题辨析
垃圾回收是否涉及栈内存?
栈内存分配越大越好吗?
不是
运行java代码时可以通过-Xss size给栈内存指定大小
方法内的局部变量是否线程安全?
java.lang.StackOverflowError
案例1: cpu 占用过多
定位
案例2:程序运行很长时间没有结果
java虚拟机调用本地方法(不是由Java编写的代码)时,需要给本地方法提供的内层空间
Heap 堆
特点
-Xmx8m:可以改变堆内存空间大小8m :代表8兆默认4g
java.lang.OutOfMemoryError: java heap space
堆内存中的对象只有不再有人使用了才会被当做垃圾回收,如果不断产生对象且一直有人使用它们,就意味着这些对象不能成为垃圾,当这些对象达到一定的数量,堆内存就会被耗尽
int i=0;
try{
Lsit<String> list = new ArrayList<>();
String a="hello";
while(true){
list.add(a);//hello
a = a+a; //hellohello
i++;
}
}catch(Throwable e){
e.printStackTrace();
System.out.println(i);
}
1.jps工具
2.jmap工具
3.jconsole工具
案例
JVM规范-方法区定义
Chapter 2. The Structure of the Java Virtual Machine (oracle.com)
永久代是jdk1.8以前方法区的一个实现
1.8已经不再由jvm管理其内存结构了,已经被移出到本地内存当中 (操作系统内存)
JDK1.8版本之前方法区用的堆的内存,叫永久代,JDK1.8之后用的操作系统的内存,叫元空间
这里说的不对,StringTable一直都在本地内存,StringTable中存储的是对字符串对象的指针,对应的String对象在堆中
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
场景
spring
mybatis
先看几道面试题:
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";//编译期优化为ab,常量池没有所以放入常量池
String s4 = s1 + s2;//new String("ab")
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
String x2 = new String("c") + new String("d");//new String("cd")
String x1 = "cd";
x2.intern();//没入池成功
// 问,如果调换了【最后两行代码】的位置呢?:true,如果是jdk1.6呢false
System.out.println(x1 == x2);//false
package cn.itcast.jvm.t1.stringtable;
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class Demo1_22 {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
System.out.println(s3 == s5);
}
}
/**
* 演示字符串字面量也是【延迟】成为对象的
*/
public class TestString {
public static void main(String[] args) {
int x = args.length;
System.out.println(); // 字符串个数 2275
System.out.print("1");
System.out.print("2");
System.out.print("3");
System.out.print("4");
System.out.print("5");
System.out.print("6");
System.out.print("7");
System.out.print("8");
System.out.print("9");
System.out.print("0");
System.out.print("1"); // 字符串个数 2285
System.out.print("2");
System.out.print("3");
System.out.print("4");
System.out.print("5");
System.out.print("6");
System.out.print("7");
System.out.print("8");
System.out.print("9");
System.out.print("0");
System.out.print(x); // 字符串个数
}
}
JDK1.8
情况一:
public class Demo1_23 {
// ["ab", "a", "b"]
public static void main(String[] args) {
String s = new String("a") + new String("b");//在堆中
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == "ab");//true
System.out.println( s == "ab" );//true
}
}
情况二:
public class Demo1_23 {
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");//在堆中
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == "ab");//true
System.out.println( s == "ab" );//false
}
}
JDK1.6
public class Demo1_23 {
// ["a", "b", "ab"]
public static void main(String[] args) {
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
// s 拷贝一份,放入串池
String x = "ab";
System.out.println( s2 == x);//true
System.out.println( s == x );//false
}
}
//["ab","a","b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // s2,是串池中原有的ab,s是堆中的
System.out.println( s2 == x);//true
System.out.println( s == x );//false
}
}
改变的原因:永久代的回收效率很低,永久代只有fullGC 的时候才会垃圾回收,而FUll GC 的触发条件是老年代空间不足,发生的时机晚。而StringTable的使用又比较频繁所以就会占用大量的内存,进而导致永久代内存不足
**证明方式:**不断的往StringTable里存放大量的字符串对象,并且用一个长时间存活的对象引用它,这样肯定会造成空间不足,如果在jdk1.6就会报永久代的内存空间不足(PermGen space),如果在jdk1.7及以上就会报堆空间不足(Java heap space)
/**
* 演示 StringTable 位置
* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit一个开关
* 在jdk6下设置 -XX:MaxPermSize=10m
*/
public class Demo1_6 {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
int i = 0;
try {
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());//intern()就是让其存入StringTable
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
从以下案例中可以看出StringTable是存在垃圾回收的
/**
* 演示 StringTable 垃圾回收
* 虚拟机堆内存的最大值 打印字符串表的统计信息 打印垃圾回收的详细信息
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo1_7 {
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);
}
}
}
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->756K(9728K), 0.0027377 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K->424K(2560K)] 2804K->700K(9728K), 0.0132936 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 2472K->424K(2560K)] 2748K->700K(9728K), 0.0026577 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
100000
Heap
PSYoungGen total 2560K, used 1432K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 49% used [0x00000000ffd00000,0x00000000ffdfc240,0x00000000fff00000)
from space 512K, 82% used [0x00000000fff00000,0x00000000fff6a020,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 276K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 3% used [0x00000000ff600000,0x00000000ff645010,0x00000000ffd00000)
Metaspace used 3222K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13234 = 317616 bytes, avg 24.000
Number of literals : 13234 = 566288 bytes, avg 42.790
Total footprint : = 1043992 bytes
Average bucket size : 0.661
Variance of bucket size : 0.662
Std. dev. of bucket size: 0.813
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 19582 = 469968 bytes, avg 24.000
Number of literals : 19582 = 1155792 bytes, avg 59.023
Total footprint : = 2105864 bytes
Average bucket size : 0.326
Variance of bucket size : 0.341
Std. dev. of bucket size: 0.584
Maximum bucket size : 4
Process finished with exit code 0
如果有大量的重复字符串可以让字符串入池来减少字符串的个数减少堆内存的使用
/**
* 演示 intern 减少内存占用
* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
* -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
*/
public class Demo1_25 {
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.intern());
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}
不属于Java虚拟机的内存管理而是属于系统内存(操作系统内存)
垃圾回收不会管理直接内存
Direct Memory
/**
* 演示 ByteBuffer 作用
*/
public class Demo1_9 {
static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
static final String TO = "E:\\a.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用时:1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}
java本身不具有磁盘读写的能力,需要调用操作系统的函数(本地方法)
所以这里会涉及到CPU的状态从用户态切换为内核态,然后就由cpu的函数去读取磁盘文件的内容,读取之后在内核态的时候会在操作系统内存中划出一块系统缓存区(系统缓存区Java代码不能运行),所以Java会在堆内存中分配一块Java缓冲区,Java代码要想读取的流中的数据必须从系统缓存区把数据间接的读入到Java缓冲区,cpu会再进入用户态,之后再去调输出流的写入操作
出现的问题:
调用ByteBuffer的allocateDirect(): 分配一块直接内存
这个方法调用之后意味着会在操作系统里面划出一块缓冲区(direct memory),这块操作系统划出的区域Java代码可以直接访问——换句话说这块内存系统可以用,Java代码也可以用
好处:
**存在内存溢出:**java.lang.OutOfMemoryError: Direct buffer memory
/**
* 演示直接内存溢出
*/
public class Demo1_10 {
static int _100Mb = 1024 * 1024 * 100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
// 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
// jdk8 对方法区的实现称为元空间
}
}