Java教程

运行时数据区域

本文主要是介绍运行时数据区域,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一:概述

了解Java虚拟机的内存的各个区域以及各个区域的作用,在Java程序出现内存泄漏和溢出等问题的时候,能够更快速的排查错误、修正等问题。
内存模型

image

二:内存区域

2.1 程序计数器

概念:程序计数器可以看作是当前线程所执行的字节码的行号指示器。
作用:
 1.存放着下一条要执行字节码指令的地址,字节码解释工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令。
 2.在Java虚拟机中,多线程是通过线程轮流切换、分配处理器执行时间的方式实现的,当从一个线程切换到另一个线程执行的时候,为了线程切换后能够恢复到执行时候的位置,程序计数器中记录的就是当前指令的地址,每条线程都需要一个独立的程序计数器。这类内存区域成为线程私有的。
 3.程序的分支、跳转、循环、异常处理,线程恢复等基础功能都依赖程序计数器。
运用:
 1.如果线程执行的是一个Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址。
 2.如果线程执行的是一个Native(本地)方法,程序计数器值为空(Undefined)。
程序计数器是唯一一个在《Java虚拟机规范》中没有规定OutOfMemoryError的内存区域。

2.2 虚拟机栈

概述
每个线程所需要的内存,称为虚拟机栈,每个栈可以由存储多个栈帧,一个栈帧对应着一次方法调用时所占用的内存。虚拟机栈是线程私有的。
栈帧
 每个Java方法执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈。方法出口等信息。一个方法从被调用到执行完毕,对应的就是一个栈帧在虚拟机栈入栈出栈的过程。每个线程只能有一个活动栈帧,对应着正在执行的方法。
栈容量的默认大小为1M,可以通过参数指定容量大小`

-Xss256k  #设置虚拟机栈内存大小为256k

问题:
1.垃圾回收是否涉及栈内存
 由于栈在方法执行完毕后,会自动将栈帧弹出栈,所以不会涉及到垃圾回收。
2.栈内存分配越大越好吗
 物理内存的大小是固定的假设为1000M,如果设置栈内存为1M,则能够并发的执行1000个线程,如果将栈内存设置为2M,则只能够并发的执行500个线程,所以并不是栈内存分配越大越好。
3.方法内的局部变量是否线程安全
 1.如果方法内部的局部变量没有逃离方法(将静态变量作为返回值)的作用范围,它是线程安全的。
 2.如果局部变量引用了对象,并逃离方法的作用范围,则需要考虑线程安全。
栈内存溢出(StackOverflowError)
 1.定义了大量的本地遍历,增大了栈帧的大小超出了栈的容量。
 2.栈帧过多导致栈内存溢出,例如递归时没有设置方法出口。

/*
	VM Args:-Xss256k
*/
public class StackOver {
    private int i = 1;
    public static void main(String[] args) {
        StackOver s = new StackOver();
        try{
            s.stackLeak();
        }catch (Throwable e){
            System.out.println("stack Lnegth:" + s.i);
            throw e;
        }
    }
    public void stackLeak(){
        i++;
        stackLeak();
    }
}

内存溢出(OutOfMemoryError)
 栈内存允许动态扩展,当栈容量无法申请到足够多的内存时,抛出OutOfMemoryError异常。由于HotSpot虚拟机不支持扩展,所以除非在创建线程申请内存时就因为无法获得足够内存而出现OutOfMemoryError异常,否则线程运行时只会因栈容量不够报StackOverflowError异常。
定位异常
 在linux下运行的Java程序,快读定位异常代码

top  #检测后台进程对CPU的使用情况
ps H -eo pid,tid,%cpu |grep 进程id  #(定位是由哪个线程引起的cpu占用过高)
jstack 进程id  #根据线程id找到有问题的进程,进一步定位到问题代码的源码行号

2.3 本地方法栈

 本地方法栈是为虚拟机使用到的本地(Native)方法服务,在虚拟机调用本地方法时需要给本地方法分配的内存空间,《Java虚拟机规范》中并没有强制规定本地方法中方法使用的语言、使用方式、数据结构。一般都是由c/c++编写,可以直接跟操作系统更底层的API打交道,而Java可以通过调用本地方法间接的调用到更底层的功能。
本地方法栈也会在栈深度溢出或者栈扩展失败是抛出StackOverflowError和OutOfMemoryError异常。

2.4 堆

概述
 堆是Java虚拟机管理的内存中最大的一块,是线程共享的,在虚拟机启动的时候创建。此内存区域唯一的目的就是存放对象实例,堆中的对象都需要考虑线程安全的问题。堆是垃圾收集器管理的内存区域。
通过设置参数 -Xmx 和 -Xms扩展堆的大小

-Xms20m #堆的最小值   -Xmx20m #堆的最大值

堆溢出(heap)
 Java堆用于存储对象实例,我们不断创建对象并保证GC Roots到对象之间有可达路径来避免垃圾回收时清除这些对象,当对象总容量超过堆的最大容量之后抛出内存溢出异常

/*
*       堆内存溢出
*       虚拟机参数:-Xmx8m
*/
public class Heap_01 {
    public static void main(String[] args) {
        int i = 0;
        try{
            List<String> list = new ArrayList<>();
            String a = "hello";
            while(true){
                list.add(a);
                a = a + a;
                i++;
            }
        }catch (Throwable e){
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

堆内存溢出
在控制台运行以下指令进行堆内存诊断

jps  #查看当前系统中有哪些Java进程
jmap -heap 进程id   #查看堆内存占用情况
jconsole   #图形化界面,多功能的监测工具,可以连续监测
jvisualvm   #另一个图形化多功能监测工具

选择需要连接的进程
image
堆内存诊断
首先确定内存中导致OOM的对象是否是必要的,分清楚是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。如果是内存泄漏,通过分析工具插看泄漏对象到GC Roots是怎么的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确的定位到这些对象创建的位置,找出产生内存泄漏的代码的具体位置。如果不是内存泄漏,说明内存中的对象都是必须存货的,那么应当检查Java虚拟机堆参数(-Xms和-Xmx)设置,与机器内存对比看是否还能向上调整,再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况。

2.5 方法区

概述
 方法区是线程共享的,用于存储类相关的信息,例如常量、静态变量、构造器、成员方法、即时编译器编译后的代码缓存等数据。在JDK8之前,方法区被称为永久代,在JDK7的时候,HotSpot将原来存放在永久代的字符串常量池、静态变量等移到了Java堆中,在JDK8之后,完全废弃了永久代的概念,改用了在本地内存中实现的元空间来代替。
对方法区进行内存回收目标主要是针对常量池的回收和对类型的卸载。
内存结构
image
image
运行时常量池
 运行时常量池是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。对于运行时常量池,《Java虚拟机规范》并没有做任何要求,不同提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。

public class StringTable02 {
    public static void main(String[] args) {
        //常量池中的信息,都会被加载到运行时常量池中,这时a b ab 都是常量池中的符号还没有变为 java 字符串对象

        //常量池中的字符串仅仅是符号,第一次用到时才变为对象
        //ldc  #7   会把 a 符号变为 "a" 字符串对象
        //ldc  #9   会把 b 符号变为 "b" 字符串对象
        //ldc  #11  会把 ab 符号变为 "ab" 字符串对象
        //StringTable["a","b","ab"]
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";

        //字符串拼接原理
        //new StringBuild().append("a").append("b").toString()
        //toString()创建了一个新的字符串对象 new String("ab");
        String s4 = s1 + s2;

        // ldc  #11   在串池中找到 ab 符号,串池中已经找到,则不会再创建
        String s5 = "a" + "b";

        //堆 new String("a")  new String("b")  new String("ab")
        String s6 = new String("a") + new String("b");
        s5.intern();  //将字符串尝试放入串池,如果串池有则不会加入
    }
}

简单来说,常量池,就是一张再*.class文件中的表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。运行时常量池,当该类被加载的时候,它的常量池信息就会被放入到运行时常量池,并把里面的符号地址变为真实地址。
元空间的一些参数

#设置原空间的最大值,默认是-1,即不限制,只受限本地内存大小
-XX:MaxMetaspaceSize: 

#指定元空间的初始空间大小,以字节为单位,达到该值就触发垃圾收集进行类型卸载
-XX:MetaspaceSize: 

#垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率
-XX:MinMetaspaceFreeRatio:  
这篇关于运行时数据区域的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!