前言
一、JVM是什么?
Java Virtual Machine -java程序运行环境(Java二进制字节码运行环境)
好处: 1.一次编写,到处运行 2.自动内存管理,垃圾回收机制 3.数组下标越界检查(抛出异常,否则覆盖其他内存资源) 4.多态
概念比较:JVM JRE JDK
二、常见JVM
三、学习路线
JVM内存结构→GC垃圾回收→类的字节码结构→类加载器→运行时优化(解释器)
四、内存结构
1. 程序计数器
蓝色路径是Java源代码的实现过程,源代码→jvm指令→解释器→机器码→CPU。 程序计数器的作用体现在解释器解释jvm指令时,是当前线程所执行的字节码的行号指示器,通俗点说就是记住下一条jvm指令的执行地址。由于读写地址非常频繁,因此物理硬件上通过寄存器实现。
特点: 1. 线程私有 每个线程都有自己的程序计数器,线程切换时记录指令执行地址。 2. 没有内存溢出
2. Java虚拟机栈(JVM Stacks)
2.1定义
虚拟机栈——线程运行所需要的内存空间,由多个栈帧组成 栈帧——每个方法调用时所需要的内存空间 活动栈帧——当前正在执行的(虚拟机栈顶的)那个方法
特点: 线程私有 问题辨析: 1.垃圾回收是否设计栈内存? 不涉及,栈内存的占用在每次调用方法时产生,随着方法调用的结束栈帧内存被自动回收,不需要垃圾回收来管理。垃圾回收管理的是堆内存。
2.栈内存分配是否越大越好? 栈内存的大小在运行时指定虚拟机参数 -Xss,如不指定,不同的操作系统对应的栈内存大小分别为:Linux/x64:1024KB; macOS:1024KB; Oracle Solaris/x64:1024KB; Windows:取决于虚拟内存。 物理内存的大小一定,栈内存划的越大,则线程数越少。栈内存大能进行更多次的方法递归调用,并不能影响运行速度。
3.方法内的局部变量是否线程安全? 取决于局部变量对于每一个线程是共享的还是私有的。若为私有则每个线程对应一个栈,局部变量是线程安全的。若为共享(static类型),线程不安全。 举个例子: m1方法中的局部变量sb是线程私有的,线程安全; m2方法中的局部变量sb作为方法参数,是多个线程共享的,线程不安全,可以改为StringBuffer型(考虑了同步机制); m2方法中的局部变量sb作为方法返回值,是多个线程共享的,线程不安全。 总结:对于引用类型变量来说,如果方法的局部变量没有逃离方法的作用范围,是线程安全的。对于逃离了方法的作用范围的,若只有一个线程操作这个变量,一定是线程安全的,如果有多个线程操作,且变量考虑了同步机制,是线程安全的,反之,是不安全的。
2.2内存溢出
Exception:StackOverflowError 产生原因:
2.3线程运行诊断
案例1:CPU占用过多,定位(Linux系统)
- top定位占用cpu较多的进程
- ps 命令定位线程
- jstack根据线程id找到有问题的线程(16进制),定位到源码行
案例2:程序运行很久都没有结果 top定位占用cpu较多的进程 4. ps 命令定位线程 5. jstack根据线程id找到有问题的线程(16进制),定位到源码行
3. 本地方法栈(Native Method Stacks)
本地方法:与操作系统底层交互的方法,jvm通过本地方法接口调用底层功能。 作用:给本地方法运行提供内存空间。 特点:线程私有
4.堆(Heap)
4.1定义
通过new关键字创建的对象都会使用堆内存。
特点: 1. 线程共享,堆中的对象要考虑线程安全的问题。 2.有垃圾回收机制(堆中不再使用的对象会当成垃圾回收以释放内存资源)
4.2堆内存溢出
Exception: OutOfMemoryError 参数:-Xmx 修改堆空间大小
4.3堆内存诊断
- jps工具
查看当前系统中有哪些Java进程 - jmap工具
监测某一时刻堆内存的占用情况 jmap -heap 进程id - jconsole工具
连续监测堆内存占用情况 - jvisualvm工具
堆转储 dump 查看对象信息
案例: 垃圾回收后,内存占用依然很高
5.方法区(Method Area)
5.1 定义
方法区是所有线程共享的区域,存储了类结构的相关信息,包括运行时常量池、成员变量、方法数据、成员方法和构造器方法的代码等。 方法区在虚拟机启动时被创建,逻辑上属于堆(并不强制使用堆内存)。
5.2 组成
方法区在jdk1.6和1.8版本的组成和逻辑位置如下图所示。 作为一个进程部署在系统内存中。
5.3方法区内存溢出
Exception: OutOfMemoryError 参数:-XX:MaxMetaspaceSize 修改内存大小
1.8以前会导致永久代内存溢出 OutOfMemoryError:PermGen space 1.8以后导致元空间内存溢出 OutOfMemoryError:Metaspace
运行时动态产生并加载Class 实际场景: - Spring - mybatis
5.4运行时常量池
二进制字节码文件(.class)组成:类基本信息、常量池、类方法定义(包含了虚拟机指令)。javap -v *.class反编译后可以查看。根据编译原理,执行虚拟机指令。 常量池是一张表,存于.class文件中,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量(如基本数据类型、字符串等)等信息。 运行时常量池:类被加载时,常量池信息会放入运行时常量池并把其中的符号地址(#1等)变为真实地址。
5.5StringTable
特性:
- 运行时常量池中的字符串仅是符号,第一次使用时才变为对象,并加载到串池中
- 采用串池的机制,避免创建重复的字符串对象
- 字符串变量拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的对象放入串池。
将字符串对象尝试放入串池,如果有则并不会放入,如果没有则将同一个对象放入串池,(即串池中的对象和堆中的对象是同一个) 会把串池中的对象返回
面试题:
以常考的面试题为例, String s1 = “a”; / String s2 = “b”; String s3 = “ab”; String s4 = s1 + s2; String s5 = “a” + “b”; String s6 = s4.intern(); String x2 = new String(“c”) + new String(“d”); String x1 = “cd”; x2.intern();
问:
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println(x1 == x2);
如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
我们先来看一段代码的底层原理,首先捋清楚代码在底层运行的逻辑,之后我们再聚焦到字符串在哪里创建的:
public class Demo1 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
反编译Demo1.class文件
运行时常量池: 注意逻辑地址,即 #及后面的数字 Main方法: 在底层理解一下代码 String s1 = “a”; 的意思:在运行时常量池#7位置加载字符串变量,而#7位置指向#8位置存储的字符串"a",加载后把它存入到局部变量表2中。 局部变量表 理解了代码运行的逻辑,知道了符号加载的底层逻辑,我们区分一下常量池和串池即StringTable的关系。常量池开始存在于字节码文件中,当程序运行时,常量池被加载到运行时常量池中,但此时运行时常量池中的符号还没有成为Java中的字符串对象。只有执行到引用符号的代码时,才会把符号变为字符串对象。 串池StringTable数据结构上是一个哈希表,长度固定,不能扩容。运行到引用符号的代码,生成一个字符串对象,将生成的字符串对象放入串池中。 所以说,常量池是存在于.class文件中的,而串池是运行时形成的。
那么两个引用的字符串相加的原理是什么呢?按照上面的分析思路,底层原理为 new StringBuilder().append(“a”).append(“b”).toString() new String(“ab”) 可以看出来,最后是new 了一个String。另外一种思路是,s1 s2是引用型变量,运行时引用可以被修改,编译期无法判断相加后是否不变,因此会new新的对象。 因此System.out.println(s3 == s4);的结果也显而易见了,s3位于串池中,而s4是new出来的对象,位于堆中,"==" 做出的是引用地址是否相同,结果为false。
同样的分析原理, String s5 = “a” + “b”;底层实现逻辑为在常量池中找到"ab"(Javac在编译期的优化,因为"a" “b"是常量,在编译期就能确定合并后仍然是常量),并把它放在串池中,而此时串池中已经有了"ab"对象,因此s5指向已经存在的"ab”。 因此System.out.println(s3 == s5);结果为true。
public class Demo2 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
String s5 = "a" + "b";
System.out.println(s3 == s5);
}
}
补充小细节:如何理解只有执行到引用符号的代码时,才会把符号变为字符串对象。 我们用IDEA调试证明一下:
public class TestString {
public static void main(String[] args) {
int x = args.length;
System.out.println();
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");
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);
}
}
给程序添加断点,依次执行,内存标签中可以观察String的变化,每执行一次,生成一个对象并放入串池。当执行到重复的字符时,String不会变化,即不会放入串池中。
|