1. 程序计数器

2. 虚拟机栈

3. 本地方法栈

4. 堆

5. 方法区

1. 程序计数器

JVM第一天 -(一)内存结构_后端

1.1定义

Program Counter Register 程序计数器(寄存器) 物理上是通过寄存器实现的

作用

● 记住下一条jvm指令的执行地址

特点

● 是线程私有的,每个线程都有自己的程序计数器

● 不会存在内存溢出

1.2作用

        例如下面代码:getstatic指令交给解释器,解释器将其变成机器码,再交给CPU运行。与此同时,把下一条指令的地址(这里是astore_1的地址3),放入程序计数器。第一条指令执行完以后,解释器从程序计数器里根据3取到下一条指令,再重复刚才的过程。这就是程序计数器 记住下一条jvm指令的执行地址。

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

 第一条jvm指令执行:

JVM第一天 -(一)内存结构_后端_02

第二条jvm指令执行:

JVM第一天 -(一)内存结构_开发语言_03

解释器把每条指令解释成机器码,机器码再到CPU上运行(CPU只认识机器码。)

JVM第一天 -(一)内存结构_后端_02

2.虚拟机栈

JVM第一天 -(一)内存结构_内存溢出_05

2.1 定义

Java Virtual Machine Stacks (Java 虚拟机栈)

■ 每个线程运行时所需要的内存,称为虚拟机栈

■ 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

■ 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法(栈顶部的方法)

问题辨析

1. 垃圾回收是否涉及栈内存?

        不涉及,栈帧内存在每一次方法调用完成后,都会弹出栈,被自动回收掉。

2. 栈内存分配越大越好吗?

       不是,栈的内存越大,线程数越少。如物理内存大小一定,若一个线程用1MB内存,总共的物理内存有500MB,理论上有500个线程可以同时运行。如果为每个线程的栈内存设置为2MB内存,则理论上只能有250个线程同时运行。

        栈内存大了,只是可以进行更多次的方法递归调用。

3. 方法内的局部变量是否线程安全?

        ■ 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的(线程私有)

        ■ 如果是局部变量引用了对象,并逃离方法的作用范围(作为返回值返回),需要考虑线程安全(多个线程共享)

/**
* 局部变量的线程安全问题
*/
public class Demo1_17 {
public static void main(String[] args) {
//主线程里创建了一个对象,并进行操作。
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
//创建新线程,将同一个对象传给了m2方法
//则两个线程使用的是同一个对象,对象不再是线程私有因此m2是非线程安全的
new Thread(()->{
m2(sb);
}).start();
}

//线程安全
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}

//非线程安全
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}


//非线程安全,该方法把局部对象返回了,其他线程可能拿到该对象进行并发修改
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}

2.2 栈内存溢出

● 栈帧过多导致栈内存溢出

1.如递归调用没有出口,一直进行方法调用,产生栈帧。

/**
* 演示栈内存溢出 java.lang.StackOverflowError
* -Xss256k 指定占内存大小为256KB
*/
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();
}
}

2.第三方库导致的栈内存溢出

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;

/**
* 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
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占用过多

package cn.itcast.jvm.t1.stack;

/**
* 演示 cpu 占用过高
*/
public class Demo1_16 {

public static void main(String[] args) {
new Thread(null, () -> {
System.out.println("1...");
while(true) {

}
}, "thread1").start();


new Thread(null, () -> {
System.out.println("2...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();

new Thread(null, () -> {
System.out.println("3...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread3").start();
}
}

定位

■ 用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. 本地方法栈

JVM第一天 -(一)内存结构_内存溢出_06

作用:

        给本地方法的执行提供内存空间。

本地方法:

        就是不是由java代码编写的代码,使用C或C++编写的本地方法与操作系统底层打交道,本地方法使用的内存就是本地方法栈。本地方法如Object的clone方法等。

 4.堆

JVM第一天 -(一)内存结构_后端_07

 4.1 定义

Heap 堆

 ●  通过 new 关键字,创建对象都会使用堆内存

特点

●  它是线程共享的,堆中对象都需要考虑线程安全的问题

●  有垃圾回收机制,没人使用的对象会被回收

4.2 堆内存溢出

package cn.itcast.jvm.t1.heap;

import java.util.ArrayList;
import java.util.List;

/**
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m 参数,指定堆空间大小为8mB
*/
public class Demo1_5 {

public static void main(String[] args) {
int i = 0;
try {
//list对象在catch块前面一直有效
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 堆内存诊断

1. jps 工具(命令行窗口)

        ●  查看当前系统中有哪些 java 进程

2. jmap 工具

        查看内存堆栈信息
        jhsdb jmap --heap --pid 16279

       ●   查看堆内存占用情况 jmap - heap 进程id

JVM第一天 -(一)内存结构_java_08

3. jconsole 工具

       ●   图形界面的,多功能的监测工具,可以连续监测

JVM第一天 -(一)内存结构_后端_09

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);
}
}

案例
        ● 垃圾回收后,内存占用仍然很高

        可使用jvisualvm打开图形化工具进行对jvm的监测。

        查看哪些对象占用内存过大,进行对代码的排查。

        案例代码

package cn.itcast.jvm.t1.heap;

import java.util.ArrayList;
import java.util.List;

/**
* 演示查看对象个数 堆转储 dump
*/
public class Demo1_13 {

public static void main(String[] args) throws InterruptedException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
// Student student = new Student();
}
Thread.sleep(1000000000L);
}
}
class Student {
private byte[] big = new byte[1024*1024];
}

JVM第一天 -(一)内存结构_java_10

 

JVM第一天 -(一)内存结构_java_11

 5. 方法区

 

JVM第一天 -(一)内存结构_java_12

 5.1定义

 

JVM第一天 -(一)内存结构_开发语言_13

Java虚拟机有一块方法区域在所有的虚拟机线程池之间共享。
方法区与传统语言编译代码存储区或者操作系统进程的正文段很相似。
它存储了每个类的结构,像运行时常量池,字段、方法数据,方法、构造器的代码,
包括类、对象初始化、接口初始化的特殊方法。
 
方法区在虚拟机启动时创建,虽然方法区在逻辑上是堆的组成部分,
但是简单的实现可以选择不进行垃圾回收集与压缩(回收)。
此规范也不现在实现方法区的内存位置或者编译代码的管理策略。
方法区的大小可以是固定大小或者根据计算和扩展、大的不需要的方法区进行回收。
方法区内存不需要是连续的。
 
Java虚拟机实现可以提供程序或用户控制初始化的堆大小。
如果堆可以动态扩展或回收,需要控制最大和最小方法区大小。
 
以下异常条件与方法区有关:
•如果方法区的内存空间不能满足内存分配需求,
Java虚拟机抛出一个内存不足错误(OutOfMemoryError)。

 5.2 组成

1.6版本串池在永久代中

JVM第一天 -(一)内存结构_开发语言_14

 1.8版本以后方法区被移出到本地内存当中,串池在堆中。

JVM第一天 -(一)内存结构_内存溢出_15

 5.3 方法区内存溢出

         方法区,它存储了每个类的结构,像运行时常量池,字段、方法数据,方法、构造器的代码,包括类、对象初始化、接口初始化的特殊方法。

        1.8版本的jdk,方法区的实现是元空间,默认使用的是系统内存,默认没有设置上限。

        下面的代码由于加载了10000个类到方法区当中,方法区大小设置为8m,内存不足,造成元空间的内存溢出。

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class test extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
test test = new test();
for (int i = 0; i < 10000; i++, j++) { //加载10000个新的类
// 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);
}
}
}

  ● 1.8 以前会导致永久代内存溢出

* 演示永久代内存溢出 报错:java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m

  ● 1.8 之后会导致元空间内存溢出

* 演示元空间内存溢出 报错:java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m

 动态加载类的场景非常多,使用不当会产生方法区内存溢出

spring

mybatis

5.4 运行时常量池

        5.4.1 常量池和运行时常量池的关系

        ● 常量池,就是一张表,是 *.class 文件中的。虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
        ● 运行时常量池,在方法区中。常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

        示例代码

package cn.itcast.jvm.t5;

// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
// System.out.println("hello world");
}
}

        进入当前目录进行对class文件反编译:javap -v test.class,-v表示显示详细参数。

        类基本信息:

JVM第一天 -(一)内存结构_后端_16

         这个类的常量池,运行时放在内存中的位置称为运行时常量池。将前面的#数字,变为内存中的地址,然后去找相应的类名等。

JVM第一天 -(一)内存结构_java_17

         类方法定义

JVM第一天 -(一)内存结构_java_18

5.4.2常量池与串池的关系

//String s = "a"; // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象 // 执行到 ldc #2 会把 a 符号变为 "a" 字符串对象,#2表示常量池中对应的地址 // 然后该对象作为key在StringTable里找,看有没有取值相同的key,有的话直接获取 // 没有则将该对象放入StringTable,

        示例代码1:

/**
* 2 * @Author: Mr.Li
* 3 * @Date: 2021/11/1 21:27
* 4
*/
public class Test2 {
public static void main(String[] args) {
String s1 = "a"; //s1引用的是字符串常量池中的对象
String s2 = new String("a"); //创建到堆中的对象
String s3 = "a";
System.out.println(s1==s2); //false 存储位置不同,是两个不同的对象
System.out.println(s1==s3); //true
}

}

反编译结果1

JVM第一天 -(一)内存结构_开发语言_19

 

JVM第一天 -(一)内存结构_内存溢出_20

 示例代码2:

/**
* 2 * @Author: Mr.Li
* 3 * @Date: 2021/11/1 21:27
* 4
*/
public class Test2 {
public static void main(String[] args) {

String s2 = new String("a"); //"a"是常量,先入串池,然后又到堆中创建对象。s2引用的是堆中的对象
String s1 = "a"; //s1引用字符串常量池中的对象
System.out.println(s1==s2); //false 两个对象存储位置不同,是两个不同的对象
}

}

反编译结果2

JVM第一天 -(一)内存结构_java_21

5.4.3 字符串延迟加载

        常量池中的信息,都会被加载到运行时常量池中, 这时出现的字符串都是常量池中的符号,还没有变为 java 字符串对象。执行一行代码,遇到没见过的字符串(这里如字符串字面量"1"),才放入串池。若字符串字面量在串池中已经出现过,则直接引用串池中的字符串对象。

示例代码:演示字符串字面量也是【延迟】称为对象的。

package cn.itcast.jvm.t1.stringtable;

/**
* 演示字符串字面量也是【延迟】成为对象的
*/
public class TestString {
public static void main(String[] args) {
int x = args.length;
System.out.println(x); // 字符串个数 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); // 字符串个数 2285
}
}

 5.5 StringTable

 5.5.1 先看几道面试题

1.解题前示例代码1:

package cn.itcast.jvm.t1.stringtable;

// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
// 每个取值不同的字符串对象在串池中是唯一的。
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class test {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// 执行到 ldc #2 会把 a 符号变为 "a" 字符串对象,#2表示常量池中对应的地址
// 然后该对象作为key在StringTable里找,看有没有取值相同的key,有的话直接获取
// 没有则将该对象放入StringTable

//注意 每个字符串对象并不是事先就放在串池里,而是执行到用到他的那段代码,才开始创建 属于懒惰的行为

// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象

public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab"; // aload_1 aload_2
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() StringBuilder的toString方法新创建了一个String对象,
// 即return new String(value,0,count); value是StringBuilder的value, 相当于new String("ab")(s1和s2是变量,无法确定,因此必须在运行期间使用这种方法拼接)
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab,根据反编译结果,这个ab是串池中的ab对象("a"和"b"是常量)

System.out.println(s3 == s5); //true
System.out.println(s3 == s4); //false



}
}

反编译结果

JVM第一天 -(一)内存结构_java_22

局部变量表

 

JVM第一天 -(一)内存结构_java_23

2.面试题

String s1 = "a";    //创建存储在串池中
String s2 = "b"; //创建存储在串池中
String s3 = "a" + "b"; //属于常量的拼接,会在编译期间优化为ab,入串池
String s4 = s1 + s2; //属于两个变量的拼接,会在运行时使用StringBuilder拼接,产生一个新的字符串,new String("ab") 在堆中的对象
String s5 = "ab"; //会先检查串池中的内容,此时已有ab,不会创建新的对象了,有则直接引用
String s6 = s4.intern(); //入池失败,返回池中已有对象
// 问
System.out.println(s3 == s4); //false 前者是串池中的对象,后者是堆中的对象
System.out.println(s3 == s5); //true 前者是串池的ab对象,后者会直接引用
System.out.println(s3 == s6); //true
String x2 = new String("c") + new String("d"); //先c,d入串池,又分别在堆中创建。然后字符串变量拼接,相当于在堆中创建了"cd"字符串对象。
String x1 = "cd";
x2.intern();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2); //false

5.5.2 StringTable特性

● 常量池中的字符串仅是符号,第一次用到时才变为对象
● 利用串池的机制,来避免重复创建字符串对象
● 字符串变量拼接的原理是 StringBuilder (1.8)
● 字符串常量拼接的原理是编译期优化,若结果在串池中已出现过,则直接引用串池中的。(这段话先保留,若未出现过,则只创建一个对象,入串池,即这个对象在串池中;例如String = "a"+"b"+360;,这条语句属于常量拼接,会在编译期优化为"ab360",不论"a","b"是否在串池中,都只创建一个对象。)
● 可以使用 intern 方法,主动将串池还没有的字符串对象放入串池
      ☆ 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入。如果没有则放入串池, 最后无论放入成功与否都会把串池中的对象返回。若成功放入串池,由于1.8版本串池在堆中,因此这个对象也同时存在于堆中,是同一个对象。
      ☆ 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,最后无论放入成功与否都会把串池中的对象返回。若成功放入串池,由于1.6版本串池在永久代中,串池中的对象是副本,所以此时串池中的对象和处于堆中的这个字符串对象不是同一个对象。

5.5.3 StringTable_intern_1.8示例代码

注意:

        String s = new String("a") + new String("b");  ,变量拼接是用StringBuilder,只是相当于new String("ab"); 在堆中创建"ab"对象,与直接new String("ab")不同。直接new String("ab"),若串池中没有字符串常量"ab"对象,则"ab"会入串池,然后还会在堆中创建ab对象,而且串池中的和堆中的是两个不同的对象。

package cn.itcast.jvm.t1.stringtable;

public class Demo1_23 {

// ["ab", "a", "b"]
public static void main(String[] args) {

String x = "ab";
String s = new String("a") + new String("b"); //变量拼接是用StringBuilder,相当于new String("ab"); 在堆中创建"ab"对象。注意,变量动态拼接出来的对象并不会把常量"ab"入串池。即与直接new String("ab")不同

// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 无论放入与否都会把串池中的对象返回

System.out.println( s2 == x); //true
System.out.println( s == x ); //false 前者是堆中的对象,因为尝试放入时,串池中已经有"ab"了
}

}

5.5.4 StringTable_intern_1.6示例代码

package cn.itcast.jvm.t1.stringtable;

public class Demo1_23 {

// ["ab", "a", "b"]
public static void main(String[] args) {

String s = new String("a") + new String("b"); //变量拼接是用StringBuilder,相当于new String("ab"); 在堆中

// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 无论放入与否都会把串池中的对象返回
//1.6版本下s拷贝一份 放入串池
String x = "ab";
System.out.println( s2 == x); //true 两者都是串池中的
System.out.println( s == x ); //false 前者是堆中的 后者是串池中的
//若在1.8版本下,则结果都为true,成功放入串池,由于1.8版本串池在堆中,因此这个对象也同时存在于堆中,是同一个对象。

}

}

5.6 StringTable 位置

JVM第一天 -(一)内存结构_内存溢出_24

         1.为什么从jdk1.7开始要将串池移动到堆中:永久代的内存回收效率很低,永久代需要父gc的时候才会触发永久代的垃圾回收。父gc得等到整个老年代的空间不足时才会触发,触发的时机晚,间接的导致StringTable的回收效率并不高,但是StringTable使用非常频繁,都是存的字符串常量,一个Java应用程序中有大量的字符串常量对象,都会分配到StringTable里,如果它的回收效率不高,就会占用大量的内存,从而导致永久代的内存溢出。因此,从1.7开始,jvm的工程师们逐渐把StringTable移动到堆中。

        2.验证1.6和1.8的StringTable位置不同,前者在永久代中,后者在堆中。(前者会报永久代空间不足,后者会报堆空间不足)

        测试代码

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;
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);
}
}
}

        (1)1.6 报错结果

JVM第一天 -(一)内存结构_方法区_25

         (2)1.8 报错结果

VM设置为-XX:+UseGCOverheadLimit时报错

JVM第一天 -(一)内存结构_后端_26

 VM设置为-XX:-UseGCOverheadLimit时报错

JVM第一天 -(一)内存结构_方法区_27

 5.7 StringTable垃圾回收

        当内存空间不足时,StringTable中没有被引用的字符串常量仍然会被垃圾回收。

        演示StringTable垃圾回收代码

package cn.itcast.jvm.t1.stringtable;

import java.util.ArrayList;
import java.util.List;

/**
* 演示 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);
}

}
}

没有for循环时的运行结果

JVM第一天 -(一)内存结构_java_28

 循环100次后,Number of entries增大100,不会触发垃圾回收。

循环10000次运行结果,串池中并没有10000多个字符串对象,证明StringTable会发生垃圾回收。

JVM第一天 -(一)内存结构_内存溢出_29

 5.8 StringTable 性能调优

        StringTable底层是hashtable实现,hashtable性能与桶的个数有关,桶越多,hash碰撞的几率减少,链表较短,查找效率变高,反之变低。如果程序中字符串常量个数非常多,建议加大串池的桶的个数。减少hash碰撞。

 ▲ 调整-XX:StringTableSize=桶个数,提高查找效率。

▲ 考虑将字符串对象是否入池(使用字符串的intern()方法,将字符串入池,并且重复的字符串不会入池),从而节省堆内存占用。

        案例演示

1.调整-XX:StringTableSize=桶个数来提高StringTable查找速率

//计算入池花费的时间
//已经设置桶个数为200000 桶个数较多
// -XX:StringTableSize=200000
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;
}
line.intern(); //垃圾回收只有在内存紧张时触发
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}

运行结果:设置桶个数为200000时,耗费0.4s入池;设置桶个数为1009时,耗费12秒入池。

桶个数为200000

JVM第一天 -(一)内存结构_内存溢出_30

桶个数为1009

 

JVM第一天 -(一)内存结构_后端_31

2.考虑将字符串对象是否入池来减少堆内存占用

        问题原型:有大量用户的重复地址,需要去重,否则堆内存占用过高。

        (1)将从文件中读取的每一行字符串对象加入生命周期更长的address集合中,防止由于堆内存不够,被垃圾回收掉。对比加入前后堆内存占用情况,发现,加入后,堆内存占用较高。而且由于循环10次加入,因此有重复字符串加入到address集合中,造成不必要的堆内存占用。

package cn.itcast.jvm.t1.stringtable;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

/**
* 演示 intern 减少内存占用
* 设置桶个数为200000
* -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;
} //加入到list中防止被垃圾回收
address.add(line); //垃圾回收只有在内存紧张时触发
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();


}
}

使用Java VisualVM可视化查看读取文件前堆内存占用情况:

JVM第一天 -(一)内存结构_内存溢出_32

 读取文件后:

JVM第一天 -(一)内存结构_后端_33

         (2)第29行改为address.add(line.intern()),则字符串占的堆内存会大大降低,节约堆内存。

        原因:

       由于line.intern(),返回的是串池中的对象,如果line引用的字符串对象的字面量在串池中已出现过,则不会放入串池。如果从文件中读取的每一行字符串先入串池,再加入address集合中(即第29行改为address.add(line.intern())),则加入address集合的都是串池中的对象。因此,加入串池的对象由于加入到了address集合中,所以不会被垃圾回收掉。未加入串池中的对象由于未加入到address集合中所以都会被垃圾回收掉,line.intern()防止了重复字符串加入到address集合,减少了堆内存占用。

JVM第一天 -(一)内存结构_方法区_34

 总结:未入池就直接加入时,address中的字符串占用300MB,入池再加入,address中的字符串占用40MB堆内存,通过line.intern()入池操作,减少了不必要的堆内存占用。

6. 直接内存

Direct Memory   属于操作系统的内存
● 常见于 NIO 操作时,用于数据缓冲区
● 分配回收成本较高,但读写性能高
● 不受 JVM 内存回收管理

JVM第一天 -(一)内存结构_内存溢出_35

6.1 用于数据缓冲区

示例代码,发现使用直接内存,读写文件性能更高

package cn.itcast.jvm.t1.direct;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
* 演示 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); //分配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);
}
}

原因

文件读写过程:

       1. java本身并不具备磁盘读写的能力,必须调用操作系统提供的函数,CPU切换到内核态,就可由CPU的函数真正读取磁盘文件的内容,会先把文件读取到系统内存的系统缓冲区中,然后再到Java堆内存划一个java缓冲区,java代码把系统缓冲区中的数据再读入到java缓冲区,再调用输出流的写入操作

JVM第一天 -(一)内存结构_开发语言_36

 2.通过直接内存改进后

直接内存既可以被操作系统访问,还可以被java访问,少了缓冲区的一个复制,速度得到成倍的提升。

JVM第一天 -(一)内存结构_内存溢出_35

6.2 直接内存的分配与回收

6.2.1 演示直接内存溢出

package cn.itcast.jvm.t1.direct;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;


/**
* 演示直接内存溢出
*/
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 对方法区的实现称为元空间
}
}

运行结果

JVM第一天 -(一)内存结构_开发语言_38

 6.2.2 分配和释放原理

        1.直接内存不受jvm管理

                内存分配与释放示例代码

package cn.itcast.jvm.t1.stringtable;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;


public class test {

static int _1Gb = 1024 * 1024 * 1024;

/*
* -XX:+DisableExplicitGC 显式的
*/
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
}
}

        垃圾回收前运行结果

JVM第一天 -(一)内存结构_java_39

       垃圾回收后(即System.gc()),byteBuffer对象被回收。给byteBuffer对象分配的直接内存也被回收。发现直接内存被回收,但是却不是jvm回收的

JVM第一天 -(一)内存结构_开发语言_40

         2.直接内存分配和释放主要依赖:Unsafe类

       通过Unsafe分配和释放直接内存,调用Unsafe的allocateMemory方法,分配直接内存。但是一般是jdk内部自己使用Unsafe类,普通程序员不使用。通过反射,拿到Unsafe对象。

  直接内存分配底层原理代码演示

package cn.itcast.jvm.t1.direct;

import sun.misc.Unsafe;

import java.io.IOException;
import java.lang.reflect.Field;

/**
* 直接内存分配的底层原理:Unsafe
*/
public class Demo1_27 {
static int _1Gb = 1024 * 1024 * 1024;

public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();

// 释放内存
unsafe.freeMemory(base);
System.in.read();
}

public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

通过Unsafe分配内存:

JVM第一天 -(一)内存结构_后端_41

通过Unsafe释放内存。

JVM第一天 -(一)内存结构_java_42

         3.结论

        直接内存的释放不是通过jvm,而是通过Unsafe类,直接内存需要调用freeMemory方法完成释放

        4.底层原理

(1)ByteBuffer的构造器里已经调用了Unsafe的allocateMemory方法,分配直接内存

JVM第一天 -(一)内存结构_java_43

(2)run方法里调用freeMemory方法主动释放直接内存

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

        Cleaner在java类库里是虚引用类型,当他所关联的对象(this,这里是ByteBuffer对象)被回收时,就会触发他的clean方法(是在referencehander线程中执行),

JVM第一天 -(一)内存结构_方法区_44

        clean方法再调用ByteBuffer的run方法,run方法里调用unsafe的freeMemory方法,释放直接内存。

 

JVM第一天 -(一)内存结构_方法区_45



6.3 分配和回收原理总结

 ● 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法

 ● ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦
ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调
用 freeMemory 来释放直接内存

 ● 做jvm调优时,经常会加vm参数:-XX:+DisableExplicitGC,来禁用显式回收  ,即使得代码中的System.gc()无效。如果加上该参数,则会使直接内存只有在真正的垃圾回收时,才会被释放掉。造成直接内存占用较大,常常得不到释放。可以直接用Unsafe对象调用freeMemory方法释放直接内存,手动管理直接内存。

public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,触发的是Full GC,回收新生代和老年代,造成程序暂停时间较长。
        System.in.read();
    }

 本文是根据某马教程所做的笔记,便于日后复习,仅供参考!!!

如有错误欢迎指正,共同学习,共同进步!!!