Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)
好处:
比较:
jvm、jre、jdk


Program Counter Register 程序计数器(寄存器)
作用:是记住下一条 JVM 指令的执行地址。

特点:
是线程私有的

不会存在内存溢出

Java Virtual Machine Stacks(Java 虚拟机栈)
垃圾回收是否设计栈内存?
栈内存分配越大越好吗?

public class Demo1_18 {
//多个线程同时执行此方法
static void m1() {
int x=0;
for(int i=0; i<5000; i++) {
x++;
}
System.out.println(x);
}
}

对于局部变量并不会发生内存安全问题,因为对于每一个线程的栈都是私有的,其中会分别拥有一个自己的局部变量,互不影响。
但是,对于 static 不一样,static 静态变量是共享的,会产生线程安全问题。如下图:

判断以下的方法会不会发生局部变量的线程安全问题?
public class Demo1_17 {
public static void main(String[] args) {
}
// m1 不会发生局部变量的线程安全问题,原因跟上面一样
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
// m2 不是线程安全,如下
/*public static void main(StringI] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}*/
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
// m3 不是线程安全,理由同 m2
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
栈帧过多导致栈内存溢出

栈帧过大导致栈内存溢出

定位:
top 定位哪个进程对 cpu 的占用过高ps H -eo pid,tid,%cpu | grep 进程id,用 ps 命令进一步定位是哪个线程引起的 cpu 占用过高。jstack 进程id
nohup java cn.itcast.jvm.t1.Demo1_3 &
Java 虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
本地方法栈,也是线程私有的。
允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
StackoverflowError 异常。OutofMemoryError 异常。本地方法是使用 C语言 实现的。
它的具体做法是 Native Method Stack 中 登记 native 方法,在 Execution Engine 执行时加载本地方法库。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虛拟机限制的世界。它和虚拟机拥有同样的权限:
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
它甚至可以直接使用本地处理器中的寄存器
直接从本地内存的堆中分配任意数量的内存。
并不是所有的 JVM 都支持本地方法。因为 Java虚拟机规范 并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果 JVM 产品不打算支持 native 方法,也可以无需实现本地方法栈。
在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
通过 new 关键字,创建对象都会使用堆内存。
特点:
/**
* 演示堆内存溢出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; // hellohellohellohe1lo
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
jps工具
jmap工具
jmap -heap -进程idjconsole工具
jvisualvm工具



1.8 以前会导致永久代内存溢出
/**
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m
*/
1.8 以后会导致元空间内存溢出
/**
* 演示元空间内存溢出 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<10000; i++, j++){
// ClassWriter作用是生成类的二进制字节码
ClassWriter CW = new ClassWriter(0);
// 版本号, pub1ic, 类名, 包名, 父类, 接口
CW.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/0bject", null);
// 返回byte[]
byte[] code = CW.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
使用以下命令可以反编译 *.class 文件,达到判断的效果
javap -c 文件名

类的基本信息:

常量池:

类方法定义:

*.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址先看几道面试题:
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
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
System.out.println(x1 == x2); // false
//问,如果调换了[最后两行代码]的位置呢
System.out.println(x1 == x2); // true
// 问,如果调换了[最后两行代码]的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2); // false
intern 方法,主动将串池中还没有的字符串对象放入串池
// StringTable [ "a", "b", "ab" ] hashtable 结构,不能扩容
public class Demo1_22 {
//常量池中的信息,都会被加载到运行时常量池中,这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// Idc #2 会把 a 符号变为 "a" 字符串对象
// Idc #3 会把 b 符号变为 "b" 字符串对象
// Idc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
String s1 = "a"; //懒惰的
String s2 = "b";
String s3 = "ab";
}
}
类方法定义:

运行时常量池:

// StringTable [ "a", "b", "ab" ] hashtable 结构,不能扩容
public class Demo1_22 {
//常量池中的信息,都会被加载到运行时常量池中,这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// Idc #2 会把 a 符号变为 "a" 字符串对象
// Idc #3 会把 b 符号变为 "b" 字符串对象
// Idc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
String s1 = "a"; //懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
System.out.println( s3==s4 ); //false
}
}
// StringBuilder 的 toString()方法
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count) ;
}
类方法定义:

// StringTable [ "a", "b”, "ab" ] hashtable 结构,不能扩容
public class Demo1_22 {
//常量池中的信息,都会被加载到运行时常量池中,这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// Idc #2 会把 a 符号变为 "a" 字符串对象
// Idc #3 会把 b 符号变为 "b" 字符串对象
// Idc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为 ab
System.out.println( s3==s5 ); //true
}
}

// intern() 函数
// StringTable [ "a", "b" , "ab"(到12行放入)]
public class Demo1_23 {
public static void main(String[] args) {
String s = new String("a") + new String("b"); // new String("ab")
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
System.out.println( s2=="ab" ); //true
}
}
// intern() 函数
// StringTable [ "ab" , "a", "b" ]
public class Demo1_23 {
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
// 堆 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
}
}

/**
* 演示 StringTable 垃圾回收
* -Xmx1Om -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:go
*/
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i=0;
try {
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}

/**
* 演示 StringTable 垃圾回收
* -Xmx1Om -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:go
*/
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i=0;
try {
for (int j = 0; j < 10000; j++) { // j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}

-XX:StringTableSize=桶个数的值,使得有合适的哈希分布,减少哈希冲突。Direct Memory


package cn.itcast.jvm.t1.direct;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* 禁用显式回收对直接内存的影响
*/
public class Demo1_26 {
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();
}
}
1 处:

2处,通过 unsafe 回收掉了 1Gb 内存,而不是 jvm 回收。

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);
}
}
}
缺点:当循环引用的时候。


package cn.itcast.jvm.t2;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 演示GC Roots
*/
public class Demo2_2 {
public static void main(String[] args) throws InterruptedException, IOException {
List<Object> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
System.out.println(1); // 1
System.in.read();
list1 = null;
System.out.println(2); // 2
System.in.read();
System.out.println("end...");
}
}
1处:


2处:

将以上两个 bin 文件导入到 MAT 工具
1.bin 文件






2.bin 文件

以下的情况是,A1 对象、A2 对象、A3 对象等所有对象都被 B 对象强引用,并且 A1 还被 C对象 强引用。

当以下情况,就可以将 A1 对象 回收。

当以下情况,只要满足 发生了一次垃圾回收,并且现在的内存不够 的情况,A2 对象(软引用对象)就可以被回收。

当以下情况,只要满足 发生了一次垃圾回收 的情况,A3 对象(弱引用对象)就可以被回收。

对于 软/弱引用 它们自身也会占用一定的内存,所以会有一个引用队列的操作,如果 软/弱引用 指向的对象被回收了,它们自己就会被放进引用队列,如果需要将这些空间垃圾回收,就需要从引用队列中找到响应的空间回收。
![]() | |
![]() | |
对于后面的两个 虚引用 和 终结器引用 都需要配合引用队列使用。当 虚/终结器引用 创建的时候,回关联一个引用队列中的对象

在创建 虚引用对象 的时候,ByteBuffer 会分配一个直接内存,并且把直接内存地址传递给 虚引用 Cleaner 对象,

当没有强引用引用 ByteBuffer 的时候,其可能会被垃圾回收掉。

但是,只把 ByteBuffer 回收不够,还需要把直接内存给回收。这时候可以把 虚引用 Cleaner 放到引用队列中。

并且会有一个 referencehandler 定时的查找有没有 cleaner ,如果有就调用 Cleaner 中的 Clean 方法,比如 Usafe.freeMemory() 将其释放掉。

当没有强引用引用这个对象时,虚拟机会帮我们创建终结器引用。

当 A4 对象被垃圾回收时,把对应的 终结器引用 加入队列。

在用一个 优先级很低的 finalizehandler 线程,去查看引用队列查看是否有终结器引用,如果有,则根据其找到 A4 对象,并把占用的内存都清空。

待补充
Mark Sweep)对于内存中的空间,若存在没有 GC Root 指向的内存区域,就可以选择标记。

标记以后,就可以进行清除操作,清除操作后的图如下。

注意这边的 清除操作 并不是将 内存空间中所有的内容 清零 ,只需要将 内存 起始地址 和 结束地址 记录下来,存储到一个空闲基础列表里就可以了。
对于这种 标记/清除 的操作的优点是:操作方便,速度较快,
缺点是:会产生内存碎片。
Mark Compact)这个方法在 标记 的这一步,和 3.2.1 标记清除 中的一样 。

区别在第二步整理部分,在原来的基础上,将内存空间进行整理
![]() | |
![]() | |

优点是没有内存碎片,缺点是运行速度较慢。

复制算法比较特殊,把内存区划分成大小相同的两块区域。

并且把没有 GC Root 引用的标记为垃圾,并且删除。

然后将 FROM 区中的内容复制到 TO 区中。

优点:不会产生碎片
缺点:会占用双倍的内存空间
在分代垃圾回收算法中,把那些需要长时间使用的对象存储在老年代当中,那些用完就可以丢弃的对象存储在新生代当中。
对象首先分配在伊甸园区域。

当新生代伊甸园空间不足时,就会触发垃圾回收,一般我们把新生代的垃圾回收叫做 Minor GC 。Minor GC 会引发 stop the world ,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

将 伊甸园 中的存活的对象复制到 幸存区to 中。

这时,会把幸存的对象的年龄
+
1
+1
+1 ,删除新生代伊甸园中的内容,并且交换 幸存区from 和 幸存区 to 。

这时,又可以把对象放入到新生代伊甸园中了。

当对象寿命超过阈值时,会晋升至老年代,最大寿命是 15 (4 bit)
![]() | |
![]() | |
当老年代空间不足,会先尝试触发 minor gc ,如果之后空间仍不足,那么触发 full gc ,STW 的时间更长。

| 含义 | 参数 |
|---|---|
| 堆初始大小 | -Xms |
| 堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
| 新生代大小 | -Xmn 或 (-XX:NewSize=size+-XX:MaxNewSize=size) |
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
| 幸存区比例 | -XX:SurvivorRatio=ratio |
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold |
| 晋升详情 | -XX:+PrintTenuringDistribution |
GC 详情 | -XX:+PrintGCDetails-verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGc |
代码例子待补充
例子:
-XX:+UseSerialGC = Serial + SerialOld
SerialOld 属于老年代,采用 标记整理算法 。
若要进行垃圾回收,就在 四核CPU 运行到某一个安全点的时候,将其阻塞,然后进行单个线程的垃圾回收。
cpuSTW 的时间最短 (0.2 0.2 = 0.4)-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy // 采用自适应的大小调整策略
-XX:GCTimeRatio=ratio // 调整垃圾回收的时间和总时间的一个占比 1/(1+ratio)
-XX:MaxGCPauseMillis=ms // 最大暂停毫秒数 默认为 200ms
-XX:ParallelGCThreads=n // 线程数

cpuSTW 的时间最短 (0.1 0.1 0.1 0.1 0.1 = 0.5)-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads // 并发时的 GC 线程数
-XX:CMSInitiatingOccupancyFranction=percent // 预留一些空间给浮动垃圾
-XX:+CMSScavengeBeforeRemark //

定义 Garbage First
适用场景:
Throughput )和低延迟( Low latency ),默认的暂停目标是 200msRegion ,标记+整理 算法,两个区域之间是复制算法。相关 JVM 参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
G1 垃圾回收阶段
会 STW
