JVM内存管理学习

在这里插入图片描述

跨平台:我们平常写的.java经过javac编译成.class文件之后交给jvm,在不同系统之间的运行结果都是一样。
跨语言:.class不是java的专有,例如kotlin等其他语言编程成.class文件也是一样运行的道理。

Java SE体系架构
在这里插入图片描述

JVM整体

解释执行:可以理解:jvm将class解释成机器码,执行一行翻译一行,或者全部翻译。这种较慢,一般是第一种,一行只是个说明量词,不是真的一行。

JIT:热点数据,执行了多次的代码会被编译成本地代码(机器码),提高效率。
在这里插入图片描述

线程私有:虚拟机栈、本地方法栈、程序计数器

程序计数器也是JVM中唯一不会OOM(OutOfMemory)的内存区域,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。
程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。

虚拟机栈

栈是什么样的数据结构?先进后出(FILO)的数据结构,
虚拟机栈在JVM运行过程中存储当前线程运行方法所需的数据,指令、返回地址。
Java 虚拟机栈是基于线程的。哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。
栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。
每个栈帧,都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)
栈的大小缺省为1M,可用参数 –Xss调整大小,例如-Xss256k

局部变量表:顾名思义就是局部变量的表,用于存放我们的局部变量的。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型,一般32位就可以存放下,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。

操作数据栈:存放我们方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的,操作数栈运行方法就是JVM一直运行入栈/出栈的操作

动态连接:Java语言特性多态(需要类运行时才能确定具体的方法)。例如:多态

返回地址:正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定) 完成出口

在这里插入图片描述
上面意思是:执行了main方法,在main方法里面调用了work方法,work方法将main方法压到栈底,之后开始执行work方法。
public int work(){
int x = 1;
int y = 2;
int z = (x + y)* 10;
return z;
}
首先执行指令iconst_1,将局部变量x拉到栈顶,然后赋值 = 1,之后又压回去,然后又拉出y,然后赋值=2,然后又压回去。
然后分别拉出x和y进入栈顶,然后拉出来相加,之后将10的值扩展成int类型10.
然后拉出相加之后的结果再和10进行乘法。
最后压回去变成将这个临时值结果压回给z,整个方法运行过程结束。

从栈顶啦出来是CPU那里,是CPU进行了运算。

在这里插入图片描述
java:解释执行是栈(操作数栈)
c是寄存器运算。移植性查,make install 运算速度–快

本地方法栈

本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的。
本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot直接把本地方法栈和虚拟机栈合二为一 。例如:Object里面的hashCode方法

线程共享的区域
在这里插入图片描述
方法区/永久代

很多开发者都习惯将方法区称为“永久代”,其实这两者并不是等价的。
HotSpot 虚拟机使用永久代来实现方法区,但在其它虚拟机中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一说。因此,方法区只是 JVM 中规范的一部分,可以说,在 HotSpot 虚拟机中,设计人员使用了永久代来实现了 JVM 规范的方法区。
方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池。

JVM 在执行某个类的时候,必须先加载。在加载类(加载、验证、准备、解析、初始化)的时候,JVM 会先加载 class 文件,而在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用。
字面量包括字符串(String a=“b”)、基本类型的常量(final 修饰的变量),符号引用则包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。

而当类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。
例如,类中的一个字符串常量在 class 文件中时,存放在 class 文件常量池中的;在 JVM 加载完类之后,JVM 会将这个字符串常量放到运行时常量池中,并在解析阶段,指定该字符串对象的索引值。运行时常量池是全局共享的,多个类共用一个运行时常量池,class 文件中常量池多个相同的字符串在运行时常量池只会存在一份。

方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地

java8 为什么使用元空间替代永久代,这样做有什么好处呢?

官方给出的解释是:
移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。
永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

好处:方便拓展
坏处:挤压堆
既然元空间的存储是本地,如果你有20G,元空间占据了15G,堆设置的上限也是15G,但是我先占据了15G,那么你只剩下5G能用了。

直接内存
不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;
这块内存不受java堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常。
本地内存跟元空间类似,但是不是jvm虚拟机规定的区域。在java后台这块是核心,但是在android里面不是,NIO在android中用得还是比较少。

理解运行时数据区

public class JVMObject {
    public final static String MAN_TYPE = "man"; // 常量
    public static String WOMAN_TYPE = "woman";  // 静态变量

    public static void  main(String[] args)throws Exception {//栈帧
        Teacher T1 = new Teacher();//堆中   T1 是局部变量
        T1.setName("Mark");
        T1.setSexType(MAN_TYPE);
        T1.setAge(36);
        for (int i=0;i<15;i++){//进行15次垃圾回收
            System.gc();//垃圾回收
        }
        Teacher T2 = new Teacher();
        T2.setName("King");
        T2.setSexType(MAN_TYPE);
        T2.setAge(18);
        Thread.sleep(Integer.MAX_VALUE);//线程休眠很久很久
    }
}

class Teacher{
    String name;
    String sexType;
    int age;//堆

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public String getSexType() {
        return sexType;
    }
    public void setSexType(String sexType) {
        this.sexType = sexType;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

运行前:
在这里插入图片描述
1、申请内存。
2、类加载-class进入方法区
3、常量、静态变量进入方法区。
4、虚拟机栈入栈帧。
5、栈帧的方法执行。
T1 在堆中经过了15次垃圾回收进入了老年区。
在堆中分为新生代和老年代,其中新生代包括:Eden、From、To,这些区域的内存地址是连续的。
在这里插入图片描述
深入辨析堆和栈

以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;

线程独享还是共享
栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。

空间大小
栈的内存要远远小于堆内存

内存溢出

1、栈溢出
如果你在一个方法里面不停地递归调用自己,相当于不停地入栈,这个时候就会报出:StackOverflowError。
OutofMemory:栈执行。
如果你有1000个线程同时执行,(虚拟机栈1m),如果你的机器只剩下500m,这个时候会报这个错误。

2、堆溢出
如果你的堆只剩下30m,你创建一个35m的数组,这个时候就会报出:java.lang.OutOfMemoryEroor:java heap sapce
还有一种是堆使用率达到一定程度不停地触发垃圾回收,但是垃圾回收又回收不了多少空间,这个时候也会报这个错误。

3、方法区溢出
当你一直不停地动态生成类的话不用也知道会报OOM了,指向是Metaspace,元空间内存不足。

4、直接内存溢出
跟堆内存第一种情况是一个道理。

字符串与常量池(方法区)

常量池分为运行时常量池和静态常量池。

静态常量池:通常指我们的class,类的,方法的信息。
字面量:String o = “aaa”:
符号引用:String这个类,str,java.lang.String

运行时常量池:类加载-运行时数据区-方法区(逻辑区域)存放常量的实体

字符串

以我们对java的认知,被final修饰的类不可被继承。
String的实现:private final char value[] 数组定义之后不可变且是私有的

String str = “abc”
jvm首先会检查该对象是否在字符串常量池中,如果在,则返回该对象引用,否则新的字符串则在常量池中被创建。减少重复创建,节约内存。 创建了1个对象。

String str1 = new String("abc):
1、首先在编译时,常量字符串abc会被放入到常量结构中(静态常量池class模板),在类加载时,abc则会在常量池中被创建。
2、其次,在运行时,调用new时,jvm将会调用String的构造函数创建,同时会引用常量池中的“abc”字符串,将符号引用替换成直接引用。
3、在堆内存中创建一个String对象,最后,str1将引用String对象。
创建了2个对象。所以说没事写这种代码的人是要被拉出来殴打。

String str2 = “ab” + “cd” + “df”;
首先生成ab,再生成abcd,最后生成abcddf,共有三个对象。

public class Person{
public String a;
public String b;
…省略构造函数和set、get
}
Person p = new Person();
p.seta(“广州”);
p.setb(“深圳”);
当你给它设值的时候字符串常量池有,设置完之后堆中也有,有两个实体了。

如果String Str3 = new String("abc).intern();
会去字符串常量池中找,有则复用,它跟上面指向的是同一个对象abc实体。
有了intern之后,堆里面存的是引用地址,但是它效率会低一些,因为你要去查找,正常不用的话直接创建。当然这中间得对比一下你查找的数量等,其实用在这里没什么意义,真正的用的地方看个人需求。

第二篇 java内存回收学习

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值