Program Counter Register程序计数器(寄存器)
可以看做是当前线程所执行的字节码的行号指示器,字节码解释器的工作时就是通过改变这个计数器的值来选取下一条执行的字节码指令
特点
程序计数实际上是由寄存器来实现的
线程私有,且生命周期线程相同,每个方法运行时java虚拟机都会同步创建一个栈帧用于存储局部变量表,也就是栈帧是每个方法运行时需要的内存,比如方法的变,返回值等
栈的默认大小
问题
垃圾回收是否涉及栈内存?
不涉及,因为每次方法运行结束栈帧会自动弹出
栈内存分配的越大越好吗
不会,栈内存越多那么,线程数目会越少
方法内的局部变量是否线程安全
局部变量是局部私有的,但是如果static会有线程安全问ti
如果局部变量引用了对象,并逃离了该方法的作用范围也是不安全的
两个线程都会修改同一个StringBuilder对象
这里返回了说明其他线程也会拿到该对象进行修改,也不安全
定位
用top定位哪个进程对cpu占用过高
ps H -eo pid,tid,%cpu | grep 进程id
可以查看进程内的线程的cpu占用
找到死锁
就是c/c++ 编写的方法和系统底层打交道
Heap 堆
特点
java.lang.OutofMemoryError :java heap space
堆内存溢出
这就是对象一直不断被使用导致的,他不断自增
Xmx
可以控制堆空间的大小
所以排查堆内存溢出的问题,可以把堆内存设置小一点去测试(不然在内存大的机器中一开始不会暴露出问题)
案例
可视化工具jvisualvm
其中对Dump
是所有虚拟机线程共享的区域,它存储了
lt stores per-class structures such as the run-time constant pool, field andmethod data, and the code for methods and constructors, including the specialmethods (S2.9) used in class and instance initialization and interface initialization.
他逻辑上是我们堆的组成部分
但是具体实现上,不同jvm厂商实现不同
public class main1 extends ClassLoader {//可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
main1 test = new main1();
for (int i = 0; i < 10000; 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);
}
} finally {
System.out.println(j);
}
}
}
这里设置元空间内存
出现了元空间内存溢出
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.itcast.itheima.xpp.main1.main(main1.java:26)
4865
Process finished with exit code 1
老版本是永久代溢出
总结
java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Metaspace
场景
他们会在运行期间动态的生成类的字节码完成类的加载
反编译字节码文件
javap -v class文件
类的基本信息
类文件
基本时间
签名
版本
访问修饰符
类的常量池
默认构造方法
方法(包含虚拟机的指令)
获取一个静态变量(System.out)
idc:加载一个参数
invoke:虚方法调用
根据#2
查常量池里的表
然后#2
引用了一个成员变量
又接着引用了#29
和#30
类型是 java/io ,变量是out
所以常量池就是
*.class
文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址StringTable 特性
public class StringTableStudy {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
}
}
常量池中的信息,都会加载到运行时常量池中,这时a b ab都是常量池中的符号,还没有变为java字符串对象
等到执行到具体引用它的代码上
ldc #2
就会找到a这个符号,然后把他变成字符串对象,然后把他作为key在StringTable[]中去找,看有没有取值相同的key,如果没有找到话,就会把这个a这个字符串对象放进去也就是放入串池中
注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。
使用拼接字符串变量对象创建字符串的过程
public class HelloWorld {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4=s1+s2;//new StringBuilder().append("a").append("2").toString() new String("ab")
System.out.println(s3==s4);//false
//结果为false,因为s3是存在于串池之中,s4是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中
}
反编译后的结果
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
ing;
27: astore 4
29: return
通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()
最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中
字符串拼接的操作
在Java中,字符串常量池和堆是不同的内存区域,而字符串的创建方式决定了它们在哪个区域分配内存。
字符串常量池: 字符串常量池是专门用于存储字符串常量的区域。当你使用字符串字面量(比如 "abc"
)创建字符串时,Java首先会检查字符串常量池中是否已经存在相同内容的字符串。如果存在,它将返回对现有字符串的引用,而不是创建新的字符串对象。这样做有助于节省内存,因为相同的字符串在常量池中只存储一次。
在你的代码中,String s1 = "a";
、String s2 = "b";
和 String s3 = "ab";
这三个字符串都是通过字符串字面量创建的,因此它们都存储在字符串常量池中。
堆: 堆是用于存储动态分配对象的内存区域。当你使用 new
关键字创建字符串对象时,该对象会在堆中分配内存。在你的代码中,String s4 = s1 + s2;
这行代码使用了字符串拼接操作,它会创建一个新的字符串对象,其内容是 s1 和 s2 的拼接结果。因为这是在运行时动态生成的,所以会在堆中分配内存。
因此,s1
、s2
、s3
是字符串常量池中的字符串,而 s4
是通过字符串拼接在堆中动态创建的字符串对象。这也是为什么 s3 == s4
的结果可能是 false
,因为它们分别指向不同的内存位置。
使用字符串常量拼接创建字符串
public class HelloWorld {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4=s1+s2;//new StringBuilder().a|ppend("a").append("2").toString() new String("ab")
String s5="a"+"b";
System.out.println(s5==s3);//true
}
}
反编译的结果
Code:
stack=2, locals=6, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
ing;
27: astore 4
//ab3初始化时直接从串池中获取字符串
29: ldc #4 // String ab
31: astore 5
33: return
从常量池中直接取出来的ab
符号
这是javac
在编译期的优化,它认为a
和b
都是常量都是确定的,那结果已经编译期间确定为ab
而上面的拼接是变量的拼接,还会变,那么只能动态的拼接使用StringBuilder的方式
intern方法 1.8:
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
无论放入是否成功,都会返回串池中的字符串对象
public class HelloWorld {
public static void main(String[] args) {
String x = "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
}
}
intern方法 1.6:
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
无论放入是否成功,都会返回串池中的字符串对象
package com.itcast.itheima.xpp;
public class main {
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
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
String x4=new String("e")+new String("f");
x4.intern();
String x3="ef";
System.out.println(x3==x4);//true
}
}
-Xmx18m -XX:+PrintStringTableStatistics -XX: +PrintGCDetails -verbose:gc
摄者虚拟机参数,分别是设置虚拟机堆内存的最大值,打印字符串表的统计信息,打印垃圾回收的详细信息
桶的数量, 总占用空间等信息
类名啊,方法名啊也是以字符串常量存储
添加了一万多个对象,但是只有7千多个,因为内存不足触发垃圾回收
底层是一个hash表,性能和大小相关
太小的话就会容易出现hash冲突
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=xxxx
//最低为1009
考虑是否将字符串对象入池
可以通过intern方法减少重复入池,保证相同的地址在StringTable中只存储一份
常见于NIO操作,用于数据缓冲区
分配回收成本较高,但读写性能高
不受JVM 内存回收影响
这里系统缓冲区复制到java缓冲区造成了不必要的复制
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);
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);
}
}
这里是使用了directbuffer 之后的效果
操作系统画出来一片缓冲区,这片缓冲区java代码可以直接访问,这样少了一次缓冲区的复制操作
直接内存溢出是Direct buffer memory
使用了Unsafe对象完成直接内存的分配和回收,并且回收需要主动调用freeMemory方法
ByteBuffer的实现类内部,使用Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过
Cleaner的clean方法调用freeMemory来释放直接内存
/**
* 禁用显式回收对直接内存的影响
*/
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();
}
}
直接内存的释放不能通过垃圾回收只能通过底层的unsafe对象的freeMemory方法
/**
* 直接内存分配的底层原理: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);
}
}
}
当bytebuffer被回收时就会触发虚引用对象cleaner 中的create方法,然后会调用回调任务对象Delocator就会调用freeMemory方法
有时候我们会-XX:+DisableExplicitGC显式的
禁用显示的垃圾回收,因为这个很损耗性能,这样也会间接的让我们无法手动直接调用垃圾回收拉释放直接内存
这时候我们可以使用usafe
对象来释放直接内存
(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);
}
}
}
[外链图片转存中...(img-gIZ18FLN-1696576900268)]
当bytebuffer被回收时就会触发虚引用对象cleaner 中的create方法,然后会调用回调任务对象Delocator就会调用freeMemory方法
有时候我们会`-XX:+DisableExplicitGC显式的` 禁用显示的垃圾回收,因为这个很损耗性能,这样也会间接的让我们无法手动直接调用垃圾回收拉释放直接内存
这时候我们可以使用`usafe`对象来释放直接内存