目录
---------------------------------内存结构-----------------------------------------
----------------------------------垃圾回收----------------------------------------
-----------------------类加载与字节码技术------------------------------------
-----------------------------------内存模型---------------------------------------
4.3 原子操作类
jvm组成部分
---------------------------------内存结构-----------------------------------------
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
1. 程序计数器
1.1 定义
Program Counter Register 程序计数器(寄存器)
1.2 特点
是线程私有的,每个线程都会有自己的程序计数器。
不会存在内存溢出,唯一一个不会存在内存溢出的区,堆,栈,方法区等均会出现内存溢出。
1.3 作用
作用是记住下一条jvm指令的执行地址。
1.4 java代码执行流程
java源代码会编译成二进制字节码,二进制字节码会被解释器变成机器码,其中程序计数器的作用是记住下一条指令的执行地址,且程序计时器采用处理速度最快的寄存器,机器码交给CPU执行。
2. 虚拟机栈
2.1 定义
Java Virtual Machine Stacks (Java 虚拟机栈)
-
每个线程运行时所需要的内存,称为虚拟机栈。
-
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
问题辨析:
1 垃圾回收是否涉及栈内存?
不会,栈内存由每个栈帧内存组成,栈帧内存是在每个方法调用时入栈被使用,方法调用结束后弹出栈,所以不会被垃圾回收。
2 栈内存分配越大越好吗?
不是,我们物理内存的大小是一定的,所以当栈内存划分过大时,会时线程数减少,从而降低程序性能。
3 方法内的局部变量是否线程安全?
如果局部变量没有逃离该方法的作用范围则是线程安全的。
如果局部变量引用了对象,并且逃离了该方法的作用范围(局部变量的引用是传入的参数或者是返回值),则需要考虑线程安全问题。
package cn.itcast.jvm.t1.stack;
/**
* 局部变量的线程安全问题
*/
public class Demo1_17 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}
//线程安全,sb属于方法局部变量,其他线程不能访问到该变量,所以线程是安全的
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//线程不安全,可能被其他线程调用修改传入的参数sb的值
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//线程不安全,可能被其他线程调用返回的结果sb
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
2.2 栈内存溢出
-
栈帧过多导致栈内存溢出。
- 栈帧过大导致栈内存溢出。(默认1m,不太容易出现)
应用场景:
-
递归调用没有写结束条件。
package cn.itcast.jvm.t1.stack;
/**
* 演示栈内存溢出 java.lang.StackOverflowError
* -Xss256k
*/
public class Demo1_2 {
private static int count;
public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}
private static void method1() {
count++;
method1();
}
}
-
进行json格式转换的时候对象的循环调用。
package cn.itcast.jvm.t1.stack;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.List;
/**
* 演示栈内存溢出 java.lang.StackOverflowError
* json 数据转换
*/
public class Demo1_19 {
public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");
Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);
Emp e2 = new Emp();
e2.setName("li");
e2.setDept(d);
d.setEmps(Arrays.asList(e1, e2));
// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}
class Emp {
private String name;
//@JsonIgnore:该注解表示在进行json格式转换的时候不会存在循环调用
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept {
private String name;
private List<Emp> emps;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmps() {
return emps;
}
public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}
2.3 线程运行诊断
案例1: cpu 占用过多(while-true)
定位(linux下)
首先运行死循环java代码 nohup表示代码在后台运行
用top定位哪个进程对cpu的占用过高
ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
jstack 进程id
可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
案例2:程序运行很长时间没有结果(死锁)
package cn.itcast.jvm.t1.stack;
/**
* 演示线程死锁
*/
class A {
};
class B {
};
public class Demo1_3 {
static A a = new A();
static B b = new B();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (a) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (b) {
synchronized (a) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
}
}
3. 本地方法栈
java虚拟机不能直接与操作系统做交互,所以如果要与操作系统做交互则需要用到由c或者c++编写的本地方法,而本地方法栈中就存放的本地方法,如wait(),notify(),notifyAll(),equals(),clone()等。且本地方法由native修饰。
4.堆
4.1 定义
Heap 堆:通过 new 关键字,创建对象都会使用堆内存。
特点
-
它是线程共享的,堆中对象都需要考虑线程安全的问题。
-
有垃圾回收机制。
4.2 堆内存溢出
注意:调整堆内存大小参数为 -Xmx 8m。
-Xmx 8m
堆内存溢出代码演示:
package cn.itcast.jvm.t1.heap;
import java.util.ArrayList;
import java.util.List;
/**
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m
*/
public class Demo1_5 {
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);
}
}
}
4.3 堆内存诊断
代码演示:
package cn.itcast.jvm.t1.heap;
/**
* 演示堆内存
*/
public class Demo1_4 {
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);
}
}
-
jps :工具查看当前系统中有哪些 java 进程
jmap 工具:查看堆内存占用情况 jmap - heap 进程id
jconsole 工具:图形界面的,多功能的监测工具,可以连续监测
jvisualvm工具:具备jconsole不具备的更多功能,
5. 方法区
5.1 定义
JVM规范-方法区定义
5.2 组成
类的相关信息,如成员变量,成员方法,构造方法,运行时常量池等。
5.3 方法区内存溢出
1.8 以前会导致永久代内存溢出.
演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space。
方法区内存大小(1.8之前永久代)参数设置:-XX:MaxPermSize=8m。
package cn.itcast.jvm;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import com.sun.xml.internal.ws.org.objectweb.asm.Opcodes;
/**
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m
*/
public class Demo1_8 extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 20000; i++, j++) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] code = cw.toByteArray();
test.defineClass("Class" + i, code, 0, code.length);
}
} finally {
System.out.println(j);
}
}
}
1.8 之后会导致元空间内存溢出。
演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace。
方法区内存大小(1.8之后元空间)参数设置:-XX:MaxMetaspaceSize=8m。
package cn.itcast.jvm.t1.metaspace;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 26000; 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);
}
}
}
场景:
spring、mybatis使用动态代理技术时会产生大量的动态代理类可能会造成内存溢出。
5.4 运行时常量池
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。我们可以通过javap -v helloword.class指令来进行反编译来获取二进制字节码的相关信息。
package cn.itcast.jvm.t5;
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
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";
// new StringBuilder().append("a").append("b").toString() new String("ab")
String s4 = s1 + s2;
//返回false
System.out.println(s3 == s4);
// javac 在编译期间的优化,结果已经在编译期确定为ab,所以如果常量池中有则不会加入
String s5 = "a" + "b";
//返回ture
System.out.println(s3 == s5);
}
}
5.5 StringTable 特性
-
常量池中的字符串仅是符号,第一次用到时才变为对象。
-
利用串池的机制,来避免重复创建字符串对象。
-
字符串变量拼接的原理是 StringBuilder (1.8)。
-
字符串常量拼接的原理是编译期优化。
-
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池。
-
运行时常量池的 一部分,储存字符串常量
-
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回。
package cn.itcast.jvm.t1.stringtable;
public class Demo1_23 {
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
// 堆 new String("a") new String("b") new String("ab")
String s = new String("a") + new String("b");
// 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,不管有没有都会把串池中的对象返回
String s2 = s.intern();
System.out.println( s2 == x);//返回值为true
System.out.println( s == x );//返回值为false
}
}
-
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回。
package cn.itcast.jvm;
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
}
}
面试题:
package cn.itcast.jvm.t1.stringtable;
/**
* 演示字符串相关面试题
*/
public class Demo1_21 {
public static void main(String[] args) {
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")
x2.intern();
String x1 = "cd";
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
}
}
5.6 StringTable 位置
演示StringTable的位置jdk1.6下:
package cn.itcast.jvm;
import java.util.ArrayList;
import java.util.List;
/**
* 演示 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());
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
演示StringTable的位置jdk1.8下:
package cn.itcast.jvm.t1.stringtable;
import java.util.ArrayList;
import java.util.List;
/**
* 演示 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;