JVM全称是Java Virtual Machine-java程序的运行环境(java二进制字节码的运行环境)
![]() Java内存结构
1.JVM概述 定义: 好处:
比较JVM,JRE,JDK之间的联系和区别,我们可以用一张图来解释 JVM的内存结构包括: 2.程序计数器 2.1.定义 Program Counter Register程序计数器(寄存器) 在2.2中我们将会解释程序计数器的作用及特点。 2.2.作用及特点解释 二进制字节码 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 我们可以看到这些代码,第一行System.out赋值给了一个变量,在4:中去调用println()方法。然后依次打印1,2,3,4,5。这些指令不能直接交给CPU来执行,必须经过解释器的作用。它负责把一条一条的字节码指令解释成机器码,然后机器码就可以交给CPU来执行。 程序计数器是线程私有的 3.虚拟机栈 3.1.栈的特点 栈类似现实生活中的子弹夹。栈最重要的特点是后进先出。 虚拟机栈就是我们线程运行时需要的内存空间,一个线程运行时需要一个栈。如果将来有多个线程的话,它就会有多个虚拟机栈。 总结
活动栈帧表示线程正在执行的方法。 3.2.栈的演示 public class teststacks {
public static void main(String[] args) throws InterruptedException{
method1();
}
public static void method1(){
method2(1,2);
}
public static int method2(int a,int b){
int c=a+b;
return c;
}}可以自行调试以上代码来观察栈中的变化情况。 3.3.栈的问题辨析
3.4.栈的线程安全问题 看一个变量是否线程安全,首先就是看这个变量对多个线程是共享的还是私有的,共享的变量需要考虑线程安全。 //多个线程同时执行该方法,会不会造成x值混乱呢?
//不会,因为x是方法内的局部变量,是线程私有的,互不干扰
static void m1(){
int x=0;
for(int i=0;i<5000;i++){
x++;
}
System.out.println(x);
}但是如果我们把变量的类型改为static,此时就大不一样了,x是静态变量,线程1和线程2同时拥有同一个x,static变量针对多个线程是一个共享的,不加安全保护的话,就会出现线程安全问题。 static void m1(){
static int x=0;
for(int i=0;i<5000;i++){
x++;
}
System.out.println(x);
}我们再看几个方法 public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
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;
}m1是线程安全的:m1中的sb是线程中的局部变量,它是属于线程私有的 3.5.栈内存溢出(StackOverflowError) 什么情况下会导致栈内存溢出呐? 栈内存溢出代码演示1(自己开发): 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();
}}运行结果如下 idea中设置栈内存大小: 栈内存溢出代码演示2(第三方依赖库出现): 3.6.线程运行诊断 3.6.1.案例1:cpu占用过多(linux系统为例) 排查步骤: 1.在linux中使用top命令,去查看后台进程对cpu的占用情况 2.查看线程对cpu的占用情况:ps H -eo pid,tid,%cpu 3.定位到是哪个线程占用内存过高后,再使用Jdk提供的命令(jstack+进程id)去查看进程中各线程的运行信息,需要把第二步中查到的线程id(十进制)转为十六进制,然后进行比较查询到位置后判断异常信息。 3.6.2.案例2:线程诊断_迟迟得不到结果 仍然通过jdk提供的 jstack+进程id的方式,去查看进程中各个线程的运行信息 4.本地方法栈 含义:Java虚拟机调用本地方法时,需要给本地方法提供的一些内存空间 public native int hashCode(); 5.堆 5.1.定义 1.虚拟机栈,程序计数器,本地方法栈,这些都是线程私有的,而堆和方法区,是线程公用的一块内存区域。 5.2.堆内存溢出(OutOfMemoryError:Java heap space) 对象一直存在于堆中未被回收,且占用内存越来越大,最终导致堆内存溢出(虽然堆中有垃圾回收机制,但垃圾回收机制不是回收所有的对象) 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);
}}
5.3.堆内存诊断 1.jps工具:jps,查看当前进程中有哪些Java进程,并将进程id显示出来(idea中通过terminal命令行输入命令) 6.方法区 6.1.定义 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。(与类有关的信息)。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是他却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。方法区在虚拟机启动时创建。 文原文关于虚拟机的定义:
6.2.定义 jdk1.8之前,方法区是用的堆内存,1.8之后,方法区用的操作系统内存。 6.3.方法区内存溢出(OutOfMemoryError: Metaspace) 1.8以前会导致永久代内存溢出 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);
//参数:版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 生成类,二进制字节码用byte来表示,返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length); // Class 对象
}
} finally {
System.out.println(j);
}
}}jdk1.8以后, 默认情况下,方法区用的是系统内存,所以加大还是不会导致内存溢出,循环很多次都运行成功。 而1.8以前永久代溢出报的错误是java.lang.OutOfMemoryError:PermGen space 6.4.常量池
常量池,就是一张表,虚拟机指令根据这站常量表找到要执行的类名、方法名、参数类型、字面量信息(如字符串常量、true和false)。 public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello,world");
}}以上是一个helloworld程序,helloworld要运行,肯定要先编译成一个二进制字节码。 F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\HelloWorld.class F:\IDEA\projects\jvm\out\production\untitled\是HelloWorld.class所在的路径 显示类的详细信息 Classfile /F:/IDEA/projects/jvm/out/production/untitled/HelloWorld.class Last modified 2021-1-30; size 533 bytes MD5 checksum 82d075eb7217b4d23706f6cfbd44f8f1 Compiled from "HelloWorld.java"public class HelloWorld minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER 可以看到类的文件,最后修改时间,签名。以及版本等等。有的还有访问修饰符、父类和接口等详细信息。 显示常量池 Constant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #23 // hello,world #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #26 // HelloWorld #6 = Class #27 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 LHelloWorld; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 HelloWorld.java #20 = NameAndType #7:#8 // "<init>":()V #21 = Class #28 // java/lang/System #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #23 = Utf8 hello,world #24 = Class #31 // java/io/PrintStream #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V #26 = Utf8 HelloWorld #27 = Utf8 java/lang/Object #28 = Utf8 java/lang/System #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Utf8 java/io/PrintStream #32 = Utf8 println #33 = Utf8 (Ljava/lang/String;)V 显示方法定义 {
public HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature 0 5 0 this LHelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello,world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
LocalVariableTable:
Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;}第一个方法是public HelloWorld();它是编译器自动为我们构造的无参构造方法。
以上就是JVM学习之 Java内存结构的详细内容,更多请关注模板之家(www.mb5.com.cn)其它相关文章! |
