1.JVM内存结构
1.1.JVM内存结构图
1.2.程序计数器
Program Counter Register程序计数器(寄存器)
- **作用:**记住下一条JVM指令的执行地址。
- 特点:
- 线程私有化,每个线程独有一个程序计数器。
- 不会存在内存溢出。
1.3.虚拟机栈
Java Virtual Machine Stacks(java虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈。
- 每个栈由多个栈帧(Frame)组成,对应这每次方法调用时所占的内存。
- 每个线程只能有一个活动栈帧,对应这当前正在执行的那个方法。
问题分析
- 垃圾回收是否涉及栈内存?
- 栈内存主要是方法执行的内存,每个方法执行完成后,会自动的弹出栈,所以无需垃圾回收机制,垃圾回收只是在堆内存中无用的对象中使用。
- 栈内存分配的越大越好吗
- 不是栈内存根据操作系统来分配就好,如果栈内存分配的过大的话,会影响同时调用的线程数,比如500M的内存,栈内存分配1M,这样就可以有500个线程执行,如果分配2M就只能有250个线程执行。
- 方法内的局部变量是否为线程安全的?
- 如果方法内局部变量没有逃离方法的作用访问,他是线程安全的,因为是每个线程独立的变量。
- 如果是局部变量引用了对象,并且逃离了方法的作用范围,其他线程可以得到这个值,这会就要考虑线程安全性问题。
2、栈内存溢出
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
(1)演示栈内存溢出场景,方法递归调用
/** * 演示栈内存溢出 */ public class Demo1 { private static int count = 0; public static void main(String[] args) { try{ //方法入口调用 addCount(); }catch (Throwable e){ e.printStackTrace(); System.out.println(count); } } /** * 方法递归调用,让其栈内存溢出 */ private static void addCount(){ count++; addCount(); } }
(2)如何设置栈内存的大小
(2)演示json格式转换栈内存溢出
- 准备员工和部门的实体
/** * 部门对象 */ public class Dept { //部门名称 private String name; //员工集合 private List<Emp> empList; public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Emp> getEmpList() { return empList; } public void setEmpList(List<Emp> empList) { this.empList = empList; } @Override public String toString() { return "Dept{" + "name='" + name + '\'' + ", empList=" + empList + '}'; } }
/** * 员工对象 */ public class Emp { //员工姓名 private String name; //部门对象 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; } @Override public String toString() { return "Emp{" + "name='" + name + '\'' + ", dept=" + dept + '}'; } }
- 主方法测试
public class Main { public static void main(String[] args) throws JsonProcessingException { Dept dept = new Dept(); dept.setName("Market"); Emp emp1 = new Emp(); emp1.setName("张山"); emp1.setDept(dept); Emp emp2 = new Emp(); emp2.setName("李四"); emp2.setDept(dept); dept.setEmpList(Arrays.asList(emp1,emp2)); ObjectMapper mapper = new ObjectMapper(); System.out.println(mapper.writeValueAsString(dept)); } }
- 问题分析:
问题定位: {name :'Marker' , empList:[ {name:'张三'},dept:{name: 'Market',empList:[{name:'张三'},dept:{name:'张三',empList:[...]}]}]} Dept对象中有Emp的List集合,每个Emp中又持有Dept对象,Dept对象中又持有Emp的List集合,无限循环导致方法调用栈内存溢出。
- 解决办法:
//在员工实体中把Dept对象加入@JsonIgnore,JsonIgnore是在json序列化时将pojo类中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。 @JsonIgnore private Dept dept;
3、线程运行诊断
案例:cpu占用过多
定位:
- 用top定位哪个进程对cpu的占用使用过高
- ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
- 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号
1.4.本地方法栈
本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
- 本地方法栈是一个后入先出(Last In First Out)栈。
- 由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
- 本地方法栈会抛出 StackOverflowError 和 OutOfMemoryError 异常。
1.5.Java堆
1、堆简介
- Heap(堆):通过new关键字,创建对象都会使用堆内存。
- 特点:
- 它是线程共享的,堆中对象都需要考虑线程安全的问题。
- 有垃圾回收机制
- 对于多数应用来说,Java堆(Java Heap)是Java虚拟机管理的最大一块内存。Java堆被所有线程共享,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例。
2、堆内存溢出
(1)示例代码
/** * 演示堆内存溢出 */ public class Demo1 { public static void main(String[] args) { int i = 0; try{ List<String> list = new ArrayList<>(); String a = "hello"; while(true){ list.add(a); a = a + a; i++; } }catch (Throwable e){ e.printStackTrace(); System.out.println(i); } } }
(2)分析原因
3、堆内存诊断
(1)jps工具
- 查看当前系统种有哪些java进程
(2)jmap工具
- 查看堆内存占用情况
(3)jconsole工具
- 图形化界面,多功能检测
public class Demo2 { public static void main(String[] args) throws InterruptedException { System.out.println("1..."); Thread.sleep(30000); byte[] array = new byte[1024 * 1024 * 10]; System.out.println("2..."); Thread.sleep(30000); array = null; System.gc(); System.out.println("3..."); Thread.sleep(10000000L); } }
查看但钱进程使用的堆内存情况:jmap -heap 进程号
使用jconsole图形化界面分析
1.6.方法区
1、方法区简介
方法区(Method Area)也是所有线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。也称作Non-Heap非堆内存。
Java虚拟机规范堆方法区的限制非常宽松,可以选择不实现垃圾收集,但是这部分区域的回收确实是有必要的。
平时,说到永久带(PermGen space)的时候往往将其和方法区不加区别。这么理解在一定角度也说的过去。因为,《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。
同时,大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。
在JDK1.8及以后版本,永久带被移除,新出现的元空间(Metaspace)替代了它。元空间属于Native Memory Space
- JVM1.6的方法区与JVM1.8的方法区对比
2、方法区内存溢出
- 示例代码:
public class Demo4 extends ClassLoader{ //继承ClassLoader可以用来加载类的二进制字节码 public static void main(String[] args) { int j = 0; Demo4 demo4 = new Demo4(); try{ for (int i = 0; i < 100000; 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(); //执行了类的加载 demo4.defineClass("Class"+i,code,0,code.length); //Class对象 } }finally { System.out.println(j); } } }
(1)1.8以前会导致永久代内存溢出
演示永久代内存溢出:java.lang.OutOfMemoryError: PermGen space 设置堆内存: -XX:MaxPermSize=8m
(2)1.8以后会导致元空间内存溢出
演示元空间内存溢出:java.lang.OutOfMemoryError: Metaspace 设置堆内存: -XX:MaxMetaspaceSize=8m 注意:元空间依赖于系统内存的大小,所以一般很难演示出元空间溢出
3、常量池
- 以一段代码进行分析:
//二进制字节码存放的有 :类的基本信息、常量池、类方法定义、也包含了虚拟机指令 public class Demo5 { public static void main(String[] args) { System.out.println("I am LiXiang"); } }
- javap -v Demo5.class 查看Demo5编译后的指令,java文件编译后为二进制字节码文件
(1)类的基本信息
(2)常量池
(3)类方法定义、虚拟机指令
(4)java程序编译成字节码文件的执行过程
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 获取一个静态变量,对应常量池的 #2步骤
再继续寻找常量池的#21、#22步骤
再继续寻找常量池的#28、#29、30步骤
3: ldc #3 // String I am LiXiang 读取字符串,对应常量池#3,#23
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 方法执行,调用#4、#24、#25、#31、#32、#33
4、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(
Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。
1.7.StringTable
1、常量池与串池的关系
字符串池在JDK1.7之后存在于堆中的一块区域,String s1 = "abc"
这样声明的字符串会放入字符串池中,String s1 = new String("abcd")
会在字符串池有一个"abcd"的字符串对象,堆中也有1个,2个不同。
- 字符串池可以避免重复创建字符串对象
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 它的结构为hash表结构,相同的字符串只存在一份
示例代码:
public class Demo6 { //串池是HashTable结构,不能扩容,串池中相同元素只会被添加一次 //常量池中的信息,都会被加载到运行时常量池中,这时a b ab 都是常量池的符号,还没有变为java字符串对象 //ldc #2 会把a符号变为"a"字符串对象,并且放入StringTable串池 StringTable["a"] //ldc #3 会把b符号变为"b"字符串对象,并且放入StringTable串池 StringTable["a","b"] //ldc #4 会把ab符号变为"ab"字符串对象,并且放入StringTable串池 StringTable["a","b","ab"] public static void main(String[] args) { String s1 = "a"; String s2 = "b"; String s3 = "ab"; String s4 = s1+s2; } }
2、字符串变量拼接
public static void main(String[] args) { String s1 = "a"; String s2 = "b"; String s3 = "ab"; String s4 = s1+s2; System.out.println(s3 == s4); }
3、编译器优化
public static void main(String[] args) { String s1 = "a"; String s2 = "b"; String s3 = "ab"; String s4 = "a"+"b"; System.out.println(s3 == s4); }
4、StringTable特性
- 常量池中的字符仅是符号,第一次用到的时候才变为对象。
- 利用串池的机制,来避免重复创建字符串对象。
- 字符串变量拼接的原理是StringBuilder(1.8)。
- 字符串常量拼接的原理是编译期优化。
- 可以用intern方法,主动的将串池中还没有的字符串放入串池。
5、intern()方法将对象放入串池
public class Demo8 { //["a","b"] public static void main(String[] args) { //1983 String s = new String("a") + new String("b"); //new String("ab") //堆 new String("a") new String("b") new String("ab") String s2 = s.intern();//将这个字符串尝试放入串池,如果有则并不会被放入,如果没有则放入串池。会把串池中的对象返回 //注意:intern()方法在1.6版本中是将堆中的数据拷贝一份,所以再用s取比较的时候就会出现false //s2与串池中的ab是相等的 System.out.println("s2 == ab"+s2 == "ab"); //同样s被放入串池中,所以与串池中的ab也是相等的 System.out.println("s == ab"+s == "ab"); } }
public class Demo9 { public static void main(String[] args) { String s1 = "a"; String s2 = "b"; String s3 = "a"+"b"; String s4 = s1+s2; String s5 = "ab"; String s6 = s4.intern(); System.out.println(s3 == s4); //false,s4 = new String("ab")存放在堆中,s3存放在串池中 System.out.println(s3 == s5); //true,s3存放在串池中,s5直接拿串池中的数据 System.out.println(s3 == s6); //1.8中为true,1.6为false,因为1.6复制一份副本存放在串池中 } }
public class Demo9 { public static void main(String[] args) { String x2 = new String("c") + new String("d"); x2.intern(); String x1 = "cd"; System.out.println(x1 == x2); //true,因为x2堆中的对象已经放入串池中,s1为串池中对象 } }
public class Demo9 { public static void main(String[] args) { String x1 = "cd"; String x2 = new String("c") + new String("d"); x2.intern(); System.out.println(x1 == x2); //false } }
6、StringTable的位置
StringTable在1.6时存放在永久代中,在1.8中存放在堆中
- 下面这段代码分别在1.6与1.8中执行
7、StringTable垃圾回收
字符串常量也会触发垃圾回收机制
- 初始化为1771个字符串常量
8、StringTable调优
通过调整StringTable中桶的个数来提高读取效率。默认是60013个。
调整:-XX:StringTableSize=桶个数
桶的个数调大会明显提升读取速度。
1.8.直接内存
1、直接内存简介
Direct Memory
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
2、直接内存,内存溢出问题
- 一直向List中添加100兆的数据
3、直接内存释放原理
(1)演示直接内存释放过程
(2)直接内存释放原理
Java提供了Unsafe类用来进行直接内存的分配与释放
Unsafe无法直接使用,需要通过反射来获取
(3)分析ByteBuffer.allocateDirect()怎末进行直接内存的创建与释放的
使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的 clean方法调用freeMemory来释放直接内存。
4、禁用显示回收堆直接内存的影响
- 关闭GC显示调用:-XX:+DisableExplicitGC
关闭显示调用GC,会导致直接内存无法释放的问题,我们可以通过Unsafe来释放内存。