JVM day01 JVMJVM内存结构直接内存
Posted halulu.me
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM day01 JVMJVM内存结构直接内存相关的知识,希望对你有一定的参考价值。
(目录
JVM
定义:
JVM(Java Virtural Machine),即 java虚拟机。
是java的class文件的运行环境(java二进制字节码的运行环境)
好处:
1、一次编写,到处运行
2、自动内存管理,垃圾回收机制(自动释放因编码不当的内存,减少负担)
3、数组下标越界的越界检查(防止数组越界覆盖其他代码的内存)
4、多态(面向对象编程)
比较:
常见的JVM:
OpenJDK、HotSpot
JVM构成:
JVM指令:
JVM指令经过解释器解释成为机器码,然后机器码就可以交给CPU执行。
JVM内存结构
1、程序计数器(线程私有)
Program Counter Register(程序计数器,寄存器)
程序计数器是java对物理硬件的抽象,它在物理上是通过寄存器实现的,寄存器是CPU组件中读取速度最快的单元。
读取JVM指令是很普遍的,所以jvm就把CPU中的寄存器当作程序计数器,让它存储指令地址。
作用:
在指令的执行过程中,记住下一条指令执行的地址。
流程:
1、JVM指令经过解释器形成机器码
2、机器码交给CPU执行的同时,程序计数器会记录下一条执行执行的地址
3、第一条指令执行完后,解释器会从程序计数器里面获取下一条指令的执行地址。
特点:
(1)线程私有
时间片:
假设有2个线程,线程1和线程2.
多个线程运行的时候,CPU会有一个调度器组件给每个线程安排一个时间片,当线程1的时间片执行完的时候会去执行线程2,线程2的时间片用完了就再执行线程1。
线程在切换的过程中,程序计数器是各自线程私有的(每个线程都有各自的程序计数器)。
(2)不会存在内存溢出(JVM规范中唯一一个)
2、虚拟机栈(线程私有)
栈的数据结构:先进后出。
每个线程拥有各自的虚拟机栈。(线程运行时需要的内存空间)
栈内是由多个栈帧组成(每个方法运行时需要的内存就是一个栈帧)
设定栈内存
(1)垃圾回收是否涉及栈内存?
垃圾回收不会也不需要涉及栈内存。栈内存是每个方法调用的时候产生的栈帧内存的组合。当方法调用完毕的时候,属于该方法的栈帧就会被弹出,不需要垃圾回收机制来释放内存。(栈内存会自动弹出)
(2)栈内存的分配越大越好吗?
物理内存是固定的,当栈内存越大的时候,虚拟机栈可存在数量就变少,也就是说线程的数量会相对减少(50M分给1M的栈,有50个,但是分给2M的栈,就只有25个)。栈内存分配大了,只是能够进行更多次的方法递归调用,并不会增强运行的效率,反而会影响线程的数量,所以一般采用默认的栈内存大小就行。
(3)方法内(栈帧)内部的局部变量是线程安全的吗?
1、如果方法内的局部变量没有逃离方法的作用范围,那么它就是线程安全的。
2、如果局部变量引用了对象,或者的局部变量得数据来源于参数(外部成员变量),或者Return结果被外部变量引用的时候就得考虑线程安全得问题。
(4)栈内存溢出
java.lang.StackOverFlowError
a、递归导致的栈内存溢出(栈帧多)
jackson-databin的包中,jackson数据的转换有可能递归造成栈内存溢出
public class App {
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();
}
}
b、栈帧大导致的内存溢出(栈帧大)
栈帧大的情况并不常见,一个int类型的数据才4B,要达到默认的1024KB,需要有很多的int类型的数据或者其他的基本数据类型。
3、线程运行诊断
(1)CPU占用高
运行java代码:
1、javac xxx.java
nohup java xxx & (nohup让class文件在后台运行)
定位线程
2、top(定位哪个进程对cpu占用高)
3、ps H -eo pid,tid,%cpu | grep 进程id (定位那个线程对cpu占用高)
4、jstack 进程id (据线程id找到有问题的线程,进而找出具体原因。)
jstack输出的线程id是十六进制的,所以原线程id需要转换。
(2)迟迟返回不到结果
原因:可能发生死锁。
步骤与上面的一致。
4、本地方法栈(线程私有)
本地方法栈(Native Method Stacks) :java虚拟机调用本地方法时提供的内存空间。
java有一定限制的,有时候不能够直接与操作系统的底层打交道,所以需要用c或者c++语言编写的本地方法与操作系统底层的API打交道。java代码可以间接地通过本地方法调用底层的功能。
例如:
Object类中wait()、clone()、hashcode()、notify()都是native的。
5、堆(线程共享)
堆(Heap) 通过new 关键字创建的对象都会使用堆内存。
特点:
1、线程共享,所以需要考虑线程安全的问题
2、有垃圾回收机制
堆内存溢出
java.lang.OutOfMemoryError: Java heap space
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);
}
}
}
修改对内存空间:
-Xmx8m
排查错误的时候建议设置内存空间小一点,尽早发现错误。
堆内存诊断
1、jps工具
查看当前系统中有那些java进程
2、jmap
查看堆内存占用情况
jmap -heap 进程id
3、jconsole
图形界面,多功能的检测工具,可以连续监测
垃圾回收以后,内存占用依旧很高
由于编程失误导致一些对象始终被引用,使得该类对象无法被垃圾回收。
使用jvisualvm工具
6、方法区(线程共享)
1、方法区是线程共享的内存区域。
2、方法区存储了类(成员变量、成员方法、构造器、方法)、运行时常量池、类加载器的信息。
3、方法区在虚拟机启动的时候被创建。
4、方法区逻辑上时堆的组成部分,但是不同的jvm厂商不一定将方法区定义在堆上,它有一个别名叫做Non-Heap(非堆)。
5、HostPot虚拟机,在jdk8之前方法区又称为永久代,永久代使用了结构内存的一部分作为方法区。jdk8之后成为元空间,元空间并不是使用结构内存,而是使用了本地内存。
方法区内存溢出
1、1.8以前导致的永久代内存溢出
-XX:MaxPermSize=10m
java.lang.OutOfMemoryError: PermGen space
2、1.8以后导致的元空间内存溢出
元空间使用的是本地内存,默认是没有上限的。
-XX:MaxMetaspaceSize=10m
java.lang.OutOfMemoryError: Compressed class space
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);
// 版本号, 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);
}
}
}
3、场景
spring、mybatis等用到cglib动态生成代理类都有可能造成方法区的内存溢出。
cglib的底层就是使用ClassWriter类来动态生成类的二进制字节码的。
方法区运行时常量池
1.8之前运行时常量池包含StringTable,1.8之后StringTable移到堆内存。
常量池
常量池 :就是一张表,虚拟机指令根据这张常量表找到要执行的类名 、方法名、参数类型、字面变量等信息。
二进制字节码(类基本信息,常量池,类方法定义(包含虚拟机指令))
类方法定义中虚拟机指令的地址指向常量池(查表)
astore往LocalVariableTable存数据,aload往LocalVariableTable取数据
javap工具反编译字节码(javap -v xxx.class)
运行时常量池
运行时常量池: 常量池是class文件中的,当该类被加载的时候,它的常量池信息就会放入运行时常量池,并把里面符号地址变为真实地址。(运行时常量池是在JVM中)
7、stringtable(垃圾回收)
public class Demo1_22 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = s1 + s2;
//底层是:new StringBuilder().append("a").append("b").toString()
//相当于new String("ab")
String s4 = "ab";
boolean b1 = s4 == s3; // false
String s5 = "a" + "b"
//ldc String ab 直接直接找到ab,而不是分开找
//javac在编译期间进行优化,结果已经在编译期间确定为ab
boolean b2 = s5 == s4; // true
}
}
1、常量池中的信息,都会被加载到运行时常量池中,这时a只是常量池中的符号。
2、只有a被执行的时候,才会经过虚拟机指令ldc ,把a符号变为“a”字符串对象(惰性)
3、转化为字符串对象之后,会准备好一块空间StringTable,在串池里面找字符串对象a,如果a没有,就把a存进串池,反之亦然。
4、StringTable俗称串池,数据结构是hash表,长度是固定的,不可扩容。
注意:
1、String s5 = “a” + “b” 会便成“ab”。 (字符串常量的拼接原理是编译期的优化)
2、String s3 = s1 + s2; (字符串变量的拼接原理是StringBuilder(1.8))
底层是:new StringBuilder().append(“a”).append(“b”).toString()
相当于:new String(“ab”)
3、StirngTable字符串是延迟加载的,即字符串是一个一个放进串池的。(第一次调用时,才会被放到串池,遇到相同的,就不放入串池)
intern方法
1、可以使用intern方法,主动将串池中还没有的字符串对象放入串池。
2、1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回。
3、1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会把此对象复制一份
放入串池, 会把串池中的对象返回。
案例1
// [ "a", "b","ab"]
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();
//ab已经存在
// 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == "ab"); // true
System.out.println( s == "ab" ); //true ,“ab”是s放进去的,所以相同。
}
案例2
// ["ab", "a", "b"]
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();
//将new String ("ab")中的ab放进串池
// 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == "ab"); // true
System.out.println( s == "ab" ); //false ,“ab”不是s放进去的,所以不相同。
}
区别在于串池中的字符串对象是由谁放进去的
StringTable的位置
1、1.6的时候StringTable是在永久代中
2、1.6之后在堆内存中。
在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
StringTable垃圾回收
-Xmx10m
-XX:+PrintStringTableStatistics
-XX:+PrintGCDetails-verbose:gc
/**
* 演示 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 < 10000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
StirngTable性能调优
StirngTable的性能是跟大小相关的,如果hash表的桶buckets的个数比较多,那么元素就比较分散,hash碰撞的几率就比较少,查找的速度就增快,反之亦然。
1、桶的数量越多,速度就越快
-Xms500m
-Xmx500m
-XX:+PrintStringTableStatistics
-XX:StringTableSize=1009(设置桶的数量)
2、考虑字符串对象放入串池中
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;
}
address.add(line.intern());
//address.add(line)
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}
address.add(line.intern())(放入串池中)
address.add(line)(不放入串池)
直接内存
Direct Memory 直接内存,操作系统的内存。
常见于NIO操作时,用于数据缓冲区
分配和回收成本较高,但读写性能高(直接使用系统内存)
不受JVM内存回收管理
ByteBuffer类就是使用直接内存
1、读写性能高
IO读写
数据存放在2个缓冲区,造成不必要的数据的复制
直接内存读写
Direcat Memory直接内存,系统和java共享,数据只需要复制1份数据。
2、直接内存不受垃圾回收管理(内存溢出)
直接内存溢出
java.lang.OutOfMemoryError: Direct buffer memory
、
3、直接内存底层的释放原理
直接内存的分配和释放是通过unsafe对象来进行管理,需要主动调用unsafe的freeMemory方法才能释放直接内存。
垃圾回收只能释放java内存,不需要手动释放内存
ByteBuffer垃圾回收底层就是使用cleaner
(虚引用类型PhantomReference,当关联的对象被垃圾回收时,cleaner会调用虚引用对象的clean方法)
来监测ByteBuffer对象,
当ByteBuffer被垃圾回收掉时(ByteBuffer是java对象,可被垃圾回收),由RefenceHandler线程(不是主线程)通过cleaner的clean方法调用unsafe对象的freeMemory方法释放内存
-XX:+DisableExplicitGC
禁用显示的垃圾回收,让System.gc()无效
System.gc()显示垃圾回收,会回收新生代和老年代(full gc),会造成程序暂停时间比较长
-XX:+DisableExplicitGC
会对直接内存的释放造成较大的影响。这是因为如果不显示回收ByteBuffer,那么在ByteBuffer被真正的垃圾回收掉的这段期间,直接内存会占用会大的空间,并不会被主动释放。
解决 :不使用System.gc(),直接使用unsafe对象的FreeMemory()方法主动释放内存。
以上是关于JVM day01 JVMJVM内存结构直接内存的主要内容,如果未能解决你的问题,请参考以下文章