jvm运行内存结构,java内存模型和jvm内存模型
如何解决写爬虫IP受阻的问题?立即使用。
Java内存结构
1.JVM概述2。程序计数器2.1。定义2.2。功能和特点说明3。虚拟机堆栈3.1。stack 3.2的特性。堆栈3.3的演示。栈3.4的问题分析。stack 3.5的线程安全问题。StackOverflowError)3.6。3.6.1线程运行诊断。案例一:cpu占用过多(linux 3.6.2。情况2:线程诊断_延迟结果4。本地方法堆栈5。堆5.1。定义5.2。内存不足错误(Java堆空间)5.3。堆内存诊断6。方法区域6.1。定义6.2。定义6.3。方法区内存溢出(内存不足错误)。
定义:1.JVM概述
好处:
写一次,到处运行(跨平台)自动内存管理,用垃圾收集函数数组下标检查多态性JVM全称是Java Virtual Machine-java程序的运行环境(java二进制字节码的运行环境)。我们可以用一张图来解释如图所示的比较JVM,JRE,JDK之间的联系和区别。
从java源代码编译一个类后(。Java文件)转换成Java二进制字节码,它必须通过类装入器才能装入JVM并运行。
我们通常把类放在方法区。以后类创建的对象会放在堆里,堆里的对象在调用方法的时候会用到虚拟机栈和程序计数器以及本地开发。
当执行该方法时,执行引擎中的解释器逐行执行每一行代码。方法中的热代码是频繁调用的代码,由实时编译器编译和执行。GC会回收垃圾。
我们可以通过本地方法接口调用操作系统提供的函数。
JVM的内存结构包括:
1.方法区域
2.程序计数器
3.虚拟机堆栈
4.本地方法堆栈
堆积
JVM体系结构
2.程序计数器
程序计数器寄存器
角色:
记住下一条jvm指令的执行地址。
特性
是线程私有的。
不会有内存溢出(内存结构中唯一不会溢出的结构)
在2.2中,我们将解释程序计数器的功能和特性。
2.1.定义
二进制字节码JVM指令Java源代码0:get static # 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执行了。
也就是2.2.作用及特点解释。
实际上,程序计数器的作用是在指令执行过程中记住下一条JVM指令的执行地址。
上面我们二进制字节码前面的数字0,3,4…我们可以理解为地址。根据这个地址信息,我们可以找到要执行的命令。二进制字节码-解释器-机器码-CPU。在每次拿到指令交给CPU执行之后,程序计数器就会把下一条指令的地址放入到程序计数器中,等一条指令执行完成之后,解释器就会到程序计数器中取到下一条指令的地址。再把其经过解释器解释成机器码然后交给CPU执行。然后一直重复这样的过程。
在物理上,实现程序计数器是通过寄存器来实现的。寄存器是CPU组件里读取最快的存储单元
假设上面的代码都在线程1中运行,线程2和线程3同时运行。当多个线程运行时,CPU将分配时间片给每个线程和线程1。如果线程1没有在指定的时间内完成运行,它会临时存储状态并切换到线程2,线程2会执行自己的代码。线程2执行完毕后,继续执行线程1的代码。在线程切换的过程中,要记住下一条指令的执行地址。你需要使用程序计数器。假设线程1刚刚开始执行第9行代码,此时CPU切换到线程2执行。此时它会将下一条指令的地址10记录到程序计数器中,程序计数器是线程私有的,属于线程1。当线程2执行完代码,线程1获取时间片时,它将从自己的程序计数器中取出下一行代码。每个线程都有自己的程序计数器。
程序计数器是线程私有的
3.虚拟机栈
堆栈类似于现实生活中的子弹夹。3.1.栈的特点
如图,1是第一个入栈,3是最后一个入栈,但在出栈时,3是第一个出栈,1是最后一个出栈。也就是说,它们按照1、2、3的顺序进入堆栈,按照3、2、1的顺序退出堆栈。
栈最重要的特点是后进先出。。如果将来有多个线程,它将有多个虚拟机堆栈。虚拟机栈就是我们线程运行时需要的内存空间,一个线程运行时需要一个栈,例如上图中的每个元素1、2、3都可以看作一个堆栈帧。每个栈可以看成是由多个栈帧组成一个栈帧就对应着Java中一个方法的调用,即栈帧就是每个方法运行时需要的内存。每个方法运行时需要的内存一般有参数,局部变量,返回地址,这些都需要占用内存,所以每个方法执行时,都要预先把这些内存分配好。。
一个堆栈中可以有多个堆栈帧。
当我们调用第一个方法栈帧时,它就会给第一个方法分配栈帧空间,并且压入栈内,当这个方法执行完了,就会把这个方法栈帧出栈,释放这个方法所占用的内存
Java机器栈(Java虚拟机栈)
总结每个线程运行时所需要的内存,称为虚拟机栈每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存活动堆栈帧表示线程正在执行的方法。
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法(位于栈顶)
公共类测试堆栈{
公共静态void main(String[] args)引发InterruptedException{
method 1();
}
公共静态void方法1(){
方法2(1,2);
}
public static int method2(int a,int b){
int c=a b;
返回c;
}}可以自己调试上面的代码,观察堆栈的变化。
堆叠顺序:主方法1-方法2
堆叠顺序:方法2-方法1-主
3.2.栈的演示
3.3.栈的问题辨析垃圾回收是否涉及栈内存?堆栈内存分配越大越好?不涉及,垃圾回收只是回收堆内存中的无用对象,栈内存不需要对它执行垃圾回收,随着方法的调用结束,栈内存就释放了。
其次,因为电脑内存是一定的,如果有100Mb,如果堆栈内存指定为2Mb,最多只能有50个线程,所以越大越好,首先栈内存可以指定:-Xss size(如果不指定栈内存大小,不同系统会有一个不同的默认值)。栈内存较大一般是可以进行较多次的方法递归调用,而不会增强线程效率,反而会使线程数量减少,一般使用默认大小
3.4.栈的线程安全问题看一个变量是否线程安全,首先就是看这个变量对多个线程是共享的还是私有的,共享的变量需要考虑线程安全。
例如:
代码中的以下局部变量是私有的并且是线程安全的。
//多个线程同时执行这个方法,会不会造成x值的混淆?
//不能,因为X是方法内的局部变量,是线程私有的,互不干扰。
静态void m1(){
int x=0;
for(int I=0;i5000i ){
x;
}
system . out . println(x);
}但是如果我们把变量的类型改成static,这时候就大不一样了。X是一个静态变量,线程1和线程2同时拥有相同的X。静态变量是多线程共享的,所以如果没有安全保护,就会出现线程安全问题。
静态void m1(){
静态int x=0;
for(int I=0;i5000i ){
x;
}
system . out . println(x);
}我们再来看几个方法
公共静态void main(String[] args) {
StringBuilder sb=new StringBuilder();
某人追加(4);
某人追加(5);
某人追加(6);
新线程(()-{
m2(sb);
}).start();
}
公共静态void m1() {
StringBuilder sb=new StringBuilder();
某人追加(1);
某人追加(2);
某人追加(3);
system . out . println(sb . tostring());
}
公共静态void m2(StringBuilder sb) {
某人追加(1);
某人追加(2);
某人追加(3);
system . out . println(sb . tostring());
}
公共静态StringBuilder m3() {
StringBuilder sb=new StringBuilder();
某人追加(1);
某人追加(2);
某人追加(3);
归还某人;
}m1是线程安全的:m1中的sb是线程中的局部变量,是线程私有的。
M2线程不安全:sb它是方法的一个参数。如果其他线程可以访问它,它将不再是该线程的私有,而是由多个线程共享。
M3不是线程安全的:它是作为返回结果返回的,其他线程有可能获得该对象并进行并发修改。
其次局部变量也不能保证是线程安全的,需要看此变量是否逃离了方法的作用范围(作为参数和返回值逃出方法作用范围时需要考虑线程安全问题)
什么情况下会发生堆栈内存溢出?
1.3.5.栈内存溢出(StackOverflowError)
这里最简单的场景是函数的递归调用。
2.栈帧过多导致栈内存溢出(一般递归调用次数太多,进栈太多导致溢出)
栈帧过大导致栈内存溢出(不太容易出现)
测试下面的程序,其中递归函数没有递归边界。
公共类Demo1_2 {
私有静态int计数;
公共静态void main(String[] args) {
尝试{
method 1();
} catch(可投掷e) {
e . printstacktrace();
system . out . println(count);
}
}
私有静态void方法1() {
数数;
method 1();
}}运行结果如下
…
栈内存溢出代码演示1(自己开发):报在这里。
总共进行了22846次递归调用。
错误StackOverflowError
把堆栈内存设置的小一点,递归调用5000次以上就会溢出。
idea中设置栈内存大小:
在这种情况下,JsonIgnore注释可以用来解决循环依赖。转换数据时,只有department类与employee类关联,employee类不再与department类关联。将@JsonIgnore注释添加到employee类的department属性(dept)中。详情请点击此处。
栈内存溢出代码演示2(第三方依赖库出现):
3.6.线程运行诊断
故障排除步骤:
3.6.1.案例1:cpu占用过多(linux系统为例)
注意,在此之前,我们运行了一个Java程序。
Java代码占99.3%。顶级CPU。top命令只能定位进程,不能定位线程。
2.检查线程对cpu的占用情况:ps H -eo pid,tid,%cpu
如果显示太多,可以用ps H -eo pid,tid,%cpu grep进程id过滤掉一些不想看到的进程。
注意:ps不仅可以查看进程,还可以查看线程的CPU使用情况。显示H进程中所有线程的信息。-eo指定了感兴趣的输出内容,这里我们要查看pid、tid和CPU %cpu的占用率
当线程太多,不方便排查时,我们会拨打1.在linux中使用top命令,去查看后台进程对cpu的占用情况。
ps H -eo pid,tid,%cpu grep 32655
3.可以用grep pid来进行筛选,过滤掉不感兴趣的进程。
线程1、线程2和线程3是我们自己定义的线程。
根据线程id,可以找到有问题的线程,并进一步定位有问题代码的源代码行号。
定位到是哪个线程占用内存过高后,再使用Jdk提供的命令(jstack+进程id)去查看进程中各线程的运行信息,需要把第二步中查到的线程id(十进制)转为十六进制,然后进行比较查询到位置后判断异常信息
您仍然可以使用jdk提供的jstack进程id来检查进程中每个线程的运行信息。
3.6.2.案例2:线程诊断_迟迟得不到结果
4.本地方法栈
本地方法不是Java写的代码。因为Java有时候不能直接和操作系统打交道,所以需要C/C语言来和操作系统打交道。那么Java就可以通过调用本地方法来获得这些函数。本地方法有很多,比如Object类的clone(),hashCode方法,wait方法,notify方法。
public native int hashCode();含义:Java虚拟机调用本地方法时,需要给本地方法提供的一些内存空间
5.堆
1.5.1.定义。
2.虚拟机栈,程序计数器,本地方法栈,这些都是线程私有的,而堆和方法区,是线程公用的一块内存区域
3.通过new关键字创建的对象都会使用堆内存
4.由于堆是线程共享的,堆内的对象都要考虑线程安全问题(也有一些例外)
堆有垃圾回收机制,不再被引用的对象会被回收
5.2.堆内存溢出(OutOfMemoryError:Java heap space)(虽然堆中有垃圾收集机制,但垃圾收集机制并不是收集所有对象)
我们可以看看下面的代码。
公共静态void main(String[] args) {
int I=0;
尝试{
ListString list=new ArrayList();
String a= hello
while (true) {
list . add(a);//你好,hellohello,hellohellohello.
a=a a//hellohellohellohello
我;
}
} catch(可投掷e) {
e . printstacktrace();
system . out . println(I);
}}对象一直存在于堆中未被回收,且占用内存越来越大,最终导致堆内存溢出
每次代码中拼接一个hello,因为定义的列表集是在try语句中创建的,所以在for循环的连续执行过程中,列表集不会被回收,只要程序没有到达catch,它就一直有效。字符串对象被追加到集合中,并且字符串对象不会被回收,因为它们一直都在使用。
我们可以通过-Xmx设置堆空间大小。
我们把堆内存改成了8M(之前内存是4G),此时只运行了17次。
报了错误java.lang.OutOfMemoryError
1.jps工具:jps,检查当前进程中有哪些Java进程,显示进程id(在idea中通过终端命令行输入命令)
2.jmap工具:jmap -heap进程id查询某一时刻堆内存的占用情况。
3.jconsole工具:一个多功能的监控工具,图形界面,可以持续监控。流程图如下(1-2-3):
5.3.堆内存诊断
6.方法区
6.1.定义。虽然Java虚拟机规范方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。(与类有关的信息)。把方法区描述为堆的一个逻辑部分,但是他却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
对于习惯在HotSpot虚拟机上开发和部署程序的开发人员来说,许多人更喜欢将方法称为“永久生成”。本质上,两者并不等同,只是因为HotSpot虚拟机的设计团队选择了将GC生成集合扩展到方法区,或者使用永久生成来实现方法区。这样HotSpot的垃圾收集器就可以像Java heap一样管理这部分内存,可以省去为方法区写内存管理代码的工作。对于其他虚拟机(如BEA JRockit、IBM J9等)没有永久生成的概念。).原则上,如何实现方法区域是虚拟机实现细节,不受虚拟机规范的约束。但是用永久生成来实现方法区似乎并不是一个好主意,因为这样更容易遇到内存溢出(永久生成的上限是-XX:MaxPermSize,J9和JRockit只要不触及进程的可用内存上限就不会有问题,比如32位系统的4GB),而且方法很少(比如String.intern因此, 根据官方路线图信息,热点虚拟机现已放弃永久生成,逐步改为Navtive内存,实现方法区的规划。 在JDK1.7的HostSpot中,原本放在永久代中的字符串常量池已经被移除,这个常量池后来在jdk1.8中被称为元空间,它使用的是操作系统内存。
Java虚拟机规范对方法区域的限制非常宽松,除了不需要像Java堆那样需要连续内存并且可以有噪音、大小固定或者可扩展,还可以选择不实现垃圾回收。相对来说,垃圾收集行为在这方面比较少见,但并不是数据进入方法区就像永久代的名字一样“永久”存在。这个区域的内存回收目标主要是常量池的回收和类型的卸载。总的来说,这一地区的恢复“成绩”不尽如人意,尤其是类型的卸载。条件相当艰苦,但收复这部分地区确实很有必要。在Sun公司的BUG列表中,已经出现了几个比较严重的BUG,就是低版本的HotSpot虚拟机对这个区域恢复不完全导致的内存泄漏。
根据Java虚拟机的规范,方法区在虚拟机启动时创建
虚拟机的原始定义:
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
6.2.定义
这一块不太清楚,可以参考这个博客点击查看jdk1.8之前,方法区是用的堆内存,1.8之后,方法区用的操作系统内存。,而静态常量池存在于类文件中。
常量池分为静态常量池和动态常量池,下图中的常量池指的是动态常量池,因为它们已经被读入内存中去
1.8用来造成永久一代内存溢出。
1.8以后,元空间内存会溢出。
/**
*演示元空间java.lang的内存溢出。内存不足错误:元空间
* -XX:MaxMetaspaceSize=8m
*/public class demo 1 _ 8 extends class loader {//可用于加载类的二进制字节码。
公共静态void main(String[] args) {
int j=0;
尝试{
demo 1 _ 8 test=new demo 1 _ 8();
for(int I=0;我10000;I,j ) {
//ClassWriter用于生成类的二进制字节码。
class writer CW=new class writer(0);
//参数:版本号、public、类名、包名、父类、接口
cw.visit(操作码V1_8,操作码。ACC_PUBLIC, Class i,null, java/lang/Object ,null);
//生成类,二进制字节码用byte表示,byte[]
byte[]code=CW . tobytearray();
//执行了类的加载。
test.defineClass(Class i,code,0,code . length);//类对象
}
}最后{
system . out . println(j);
}
}}6.3.方法区内存溢出(OutOfMemoryError: Metaspace)jdk1.8以后, 默认情况下,方法区用的是系统内存,所以加大还是不会导致内存溢出,循环很多次都运行成功。
1.8之前永久生成溢出报告的错误是java.lang.out of memory错误:permgenspace。
当设置了-XX:MaxMetaspaceSize=8m,到了5411次就溢出了。报的是java.lang.OutOfMemoryError: Metaspace错误
6.4.常量池。运行时常量池,它位于。类文件。类加载时,它的常量池信息会放入运行时常量池,里面的符号地址会改成实地址*。
公共类HelloWorld {
公共静态void main(String[] args) {
System.out.println(hello,world );
}}以上是一个编译程序,helloworld要运行,肯定要先编译成一个二进制字节码。常量池,就是一张表,虚拟机指令根据这站常量表找到要执行的类名、方法名、参数类型、字面量信息(如字符串常量、true和false)。
反编译HelloWorld(之前需要运行将。爪哇岛文件编译成。班级文件)
使用想法工具
F:\ IDEA \ projects \ JVM javap-v F:\ IDEA \ projects \ JVM \ out \ production \ untitled \ hello world。class f:\ IDEA \ projects \ JVM \ out \ production \ untitled \是HelloWorld.class所在的路径
显示类的详细信息
类文件/F:/IDEA/projects/JVM/out/production/untitled/hello world。班级
最后修改2021-1-30;大小533字节
讯息摘要5校验和F6 cfbd 44 F8 f 1
从" HelloWorld.java "公共类编译编译
次要版本:0
主要版本:52
标志:ACC_PUBLIC、ACC_SUPER可以看到类的文件,最后修改时间,签名。以及版本等等。有的还有访问修饰符、父类和接口等详细信息。
显示常量池
常量池:
#1=方法参考#6。#20 //java/lang/Object .init:()V
#2=Fieldref #21 .# 22//Java/lang/system。out:Ljava/io/PrintStream;
#3=字符串#23 //你好,世界
#4=方法参考#24。# 25//Java/io/printstream。println:(Ljava/lang/String;)V
#5=Class #26 //HelloWorld
#6=Class #27 //java/lang/Object
#7=Utf8初始化
#8=Utf8 ()V
#9=Utf8代码
#10=Utf8线路编号表
#11=Utf8本地变量表
#12=Utf8 this
# 13=Utf8 LHelloWorld
#14=Utf8干线
# 15=Utf8([Ljava/lang/String;)V
#16=Utf8参数
# 17=Utf8[Ljava/lang/String;
#18=Utf8源文件
# 19=Utf8 HelloWorld.java
# 20=和类型#7:#8 //init:()V
#21=Class #28 //java/lang/System
# 22=名称和类型# 29:# 30//out:Ljava/io/PrintStream;
#23=Utf8你好,世界
# 24=Class # 31//Java/io/PrintStream
# 25=name and type # 32:# 33//println:(Ljava/lang/String;)V
#26=Utf8 HelloWorld
#27=Utf8 java/lang/Object
#28=Utf8 java/lang/System
#29=Utf8输出
# 30=Utf8 Ljava/io/PrintStream;
#31=Utf8 java/io/PrintStream
#32=Utf8打印
# 33=Utf8(Ljava/lang/String;)V显示方法定义
{
公共hello world();
描述符:()V
标志:ACC_PUBLIC
代码:
堆栈=1,局部变量=1,参数大小=1
0: aload_0 1: invokespecial #1 //方法Java/语言/对象.init:()V
4:退货
行号表:
第一行:0
本地变量表:
开始长度槽名签名这个世界
公共静态void main(Java。郎。string[]);
描述符:([Ljava/lang/String;)V
标志:ACC_PUBLIC,ACC_STATIC
代码:
堆栈=2,局部变量=1,参数大小=1
0:获取静态# 2//字段Java/lang/system。out:Ljava/io/PrintStream;
3: ldc #3 //String hello,world
5: invokevirtual #4 //方法Java/io/printstream。println:(Ljava/lang/String;)V
8:返回
行号表:
第3行:0
第四行:8
本地变量表:
开始长度槽名签名0 9 0 args[Ljava/lang/String;}第一个方法是公共hello world();它是编译器自动为我们构造的无参构造方法。
第二个是公共静态void main(Java。郎。string[]);即主要的方法
方噶里面就包括了虚拟机的指令了。
getstatic获取一个静态变量,即获取System.out静态变量
最不发达国家是加载一个参数,参数是字符串你好,世界
invokevirtual虚方法调用,println方法
返回执行结束。
我们在getstatic,ldc,invokevirtual之后都有一个#2,#3,#4。当解释器翻译这些虚拟机指令时,它将把这些#2、#3、#4翻译成查找表。比如getstatic #2,查常量池的表。在恒常池中
#2=Fieldref #21。#22是指成员变量# 21和# 22。
#21=Class #28 //java/lang/System
# 22=name and type # 29:# 30//out:Ljava/io/PrintStream;
然后去#28.29,30。
#28=Utf8 java/lang/System
#29=Utf8输出
# 30=Utf8 Ljava/io/PrintStream;
所以现在我知道我要找的是java.lang.system类下调用出来的成员变量,它的类型是java/io。
同样,ldc找的是#3=String #23 Utf8 hello,world,是一个虚拟机常量池的字符串。将helloworld常量加载到string对象中。
KeVIRTUAL # 4方法参考# 24。# 25等等
所以常量池的作用就是为我们的指令提供一些常量符号。根据这些常量符号,我们可以通过查表找到它,让虚拟机成功执行。
以上是JVM学习到的Java内存结构细节。更多请关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。