(一)JVM的内存结构
定义:
Java Virtual Machine - java程序的运行环境(Java二进制字节码的运行环境)
好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态:可扩展性极大的提升
JDK与JVM的关系:
学习路线
内存结构
程序计数器(Program Counter Register)_
作用
记住下一条JVM指令的执行地址
时间片
每个线程都将有一个时间片的概念,如果线程一的时间片时间用完了程序计数器将记录下一条指令的地址,CPU将切换至线程二执行代码,待线程二的时间片用完或代码执行完毕则切换回线程一继续执行程序计数器内的下一条指令。
特点
- 线程私有的(每个线程都拥有自己的程序计数器)
- 不会存在内存溢出
Java虚拟机栈
定义:
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
问题辨析
-
垃圾回收是否涉及栈内存?
答:不涉及,栈内存无非就是栈帧内存一次次方法调用产生的,再每一次方法调用结束后都会被弹出栈,自动回收掉。
-
栈内存分类的越大越好么?
答:不是,分配的越大则可以同时运行的线程数越少,栈内存越大可以使方法递归调用的次数变多。
-
方法内局部变量是否线程安全?
答:如果方法内的局部变量没有逃离方法的作用访问,那么局部变量在各自的线程内私有,不会受到其他线程干扰。 如果这个局部变量引用了对象,并逃离了方法的作用域,则需要考虑线程安全问题。
线程运行诊断
CPU占用过多
定位问题:
- 用top命令查看哪个进程对CPU的占用过高:top
- 用ps命令进一步定位是那个线程引起的cpu占用过高:ps H -eo pid,tid,%cpu | grep 进程id
- jstack进程id:jstack 32655
- 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号
本地方法栈
定义:
本地方法栈:在本地由c或c++编写的对操作系统进行操作的方法。Java很多时候无法对操作系统进行操作,所以调用本地方法栈。
堆
Heap堆:使用的是系统内存
定义:
通过new关键字,创建对象都会使用堆内存
特点:
- 它是线程共享的,堆内对象都需要考虑线程安全的问题
- 有垃圾回收机制
堆内存溢出:堆内存耗尽,溢出的含义都是耗尽
方法区(Method Area)
JVM1.6及之后
1.6后方法区是存在于堆内的,实现为永久代(PermGen)包含Class(类信息),ClassLoader(类加载器),运行时常量池三部分, 垃圾回收效率较低,易于内存溢出。
JVM1.8及之后
1.8及之后方法区存在于本地内存,实现变为元空间(MetaSpace)包含Class(类信息),ClassLoader(类加载器),运行时常量池三部分
二进制字节码
包含
- 类基本信息
- 常量池
- 类方法定义
- 虚拟机指令
类基本信息
常量池
类定义方法:默认会有构造方法
虚拟机指令
- ldc指令用来将常量池中指定的常量放入操作数栈中,这里指定的常量是 #2
- invokestatic指令用来调用类中静态方法。如果调用的静态方法描述中需要传入参数,则会将当前操作数栈顶元素作为方法的参数,并从操作数中出栈,静态方法执行完毕会将返回值推到操作数栈中。静态方法用常量池中常量来表示,该常量类型是一个方法的符号引用。
StringTable 特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象。
- 利用串池的机制,来避免重复创建字符串对象。
- 字符串变量拼接的原理是 StringBuilder (JDK1.8)。
- 字符串常量拼接的原理是编译期优化(String s3 = s1 + s2; 则在编译期不会直接拼接,因为s1,s2都是变量,String s4 = “a” + “b”; 则在编译期直接拼接, 即s4 = “ab”; )。
- 可以用 intern() 方法,主动的将串池中还没有的字符串对象放入串池。
- JDK1.8 将这个字符串对象尝试放入运行时常量池,如果有则不会放入,没有则会放入,会把串池内的对象返回
- JDK1.6 将这个字符串对象尝试放入运行时常量池,如果有则不会放入,没有则会将对象复制一份放入,即调用 intern() 方法的对象和真正放入串池内的对象是两个对象,最后把串池内的对象返回
先后进入运行时常量池的两种情况
intern() 方法: 将对象放入运行时常量池。
- 如果运行时常量池中原来存在,则返回该对象,此时运行时常量池与堆内为不同对象。
- 反之运行时常量池内不存在该字符串常量,则将方法的调用者在堆内放入运行时常量池,并返回该对象,此时,运行时常量池与堆内为同一对象。
1、"ab"字符串在 intern() 方法后进入运行时常量池,仅存在于堆内**(运行时常量池内不存在该字符串常量)**
// 运行时常量池存在于方法区内 -> 元空间(MateSpace)位于内存空间
// 创建出的对象则是存在于堆之中,非运行时常量池就在其中,也包括字符串常量池
public static void main(String[] args) {
// new String("ab");
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab");
// 运行时常量池 ["a", "b"],拼接的字符串ab开始仅存在于堆内
// 将这个字符串对象尝试放入运行时常量池,如果有则不会放入,没有则会放入,会把串池内的对象返回
String s2 = s.intern();
// true 字符串之间比较的是地址,即是否为对象本身,由此说明s2对象与运行时常量池中的"ab"为同一对象
System.out.println(s2 == "ab");
// true "ab"不存在与运行时常量池,则都是堆内地址,故相等
System.out.println(s == "ab");
// true 本次intern()方法是将堆内对象直接放入运行时常量池,则运行时常量池与堆内是同一对象
System.out.println(s == s2);
}
2、"ab"字符串在 intern() 方法前进入运行时常量池,堆内对象与运行时常量池内不是同一对象
public static void main(String[] args) {
String x = "ab"; // ab直接进入运行时常量池
// a,b进入运行时常量池,new String("ab")进入堆
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab");
// 运行时常量池 ["ab", "a", "b"]
// 将这个字符串对象尝试放入运行时常量池,如果有则不会放入,没有则会放入,会把串池内的对象返回
// 本次返回的是运行时常量池内"ab"的对象,而非堆内的s地址。
String s2 = s.intern();
// true s2和x都是运行时常量池内的地址,故相等
System.out.println(s2 == x);
// false s是堆内地址,x是运行时常量池内地址,故不相等
System.out.println(s == x);
// false 本次intern()方法是直接将运行时常量池内对象返回,与堆内对象不是同一对象
System.out.println(s == s2);
}