二、深入理解JVM的内存区域

一、JVM内存处理流程

        JVM申请内存 --> 初始化运行数据区 --> 类加载 --> 执行方法和创建对象

1、JVM申请内存

        JVM根据配置或者默认配置参数向操作系统申请内存

2、初始化运行时数据区

        JVM 获得内存空间后,根据配置参数进行运行时数据区内存分配

        -Xms30m 堆初始化大小

        -Xmx30m 堆最大大小

        -Xss1m 栈默认大小

        -XX:MaxMetaspaceSize=30m 元空间大小

3、类加载

        把类加载到方法区,class中的静态变量和常量也要放入方法区,创建类的类型对象(class对象)到堆。

        类加载机制:Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

4、执行方法和创建对象

        启动main线程,执行main方法,开始执行第一行代码。此时堆内会创建一个Teacher对象,对象引用teacher就存在栈中。后续代码再遇到new关键字,会再创建一个Teacher对象,对象引用t1,t2就存在栈中。(代码在下面)

总结 JVM 运行内存的整体流程

                JVM 在操作系统上启动,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区,最后执行方法。

方法的执行和退出过程在内存上的体现上就是虚拟机栈中栈帧的入栈和出栈。

        同时在方法的执行过程中创建的对象一般情况下都是放在堆中,最后堆中的对象也是需要进行垃圾回收清理的。

二、堆空间分代划分

 参考:Hotspot学习利器:HSDB和CLHSDB_java clhsdb 使用-CSDN博客

堆空间划分:

        新生代和老年代(Tenured),新生代又分为Eden区,Survivor区(或者叫from区和to区)

三、GC概念

        Garbage Collection 垃圾回收,在JVM中是自动化的垃圾回收机制。

        在JVM中 GC的重要区域是堆空间(还有方法区)。我们也可以主动发起,System.gc(项目中切记不要使用)

四、JHSDB工具

        JHSDB是一款基于服务性代理实现的进程外调试工具。服务性代理是 HotSpot 虚拟机中一组用于映射 Java 虚拟机运行信息的,主要基于 Java 语言实现的API 集合。

在JDK8中的开启方式

        Jdk1.8 启动 JHSDB 的时候必须将 sawindbg.dll(一般会在 JDK 的目录下)复制到对应目录的 jre 下(注意在 win 上安装了 JDK1.8 后往往同级目录下有一个jre 的目录)

然后到目录:C:\Program Files\Java\jdk1.8.0_101\lib 进入命令行,

执行 java -cp ./sa-jdi.jar sun.jvm.hotspot.HSDB 或 直接执行 java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

mac下执行sudo java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

JDK1.9 及以后的开启方式

进入 JDK 的 bin 目录下,我们可以在命令行中使用 jhsdb hsdb 来启动它

示例代码-启动

/**
 * JVM参数
 * 堆大小:-Xms30m -Xmx30m
 * 元空间:-XX:MaxMetaspaceSize=30m
 * 指定垃圾回收器:-XX:+UseConcMarkSweepGC  
 * 禁止使用压缩指针:-XX:-UseCompressedOops  
 * -Xms30m -Xmx30m -XX:+UseConcMarkSweepGC -XX:-UseCompressedOops
 */
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.setName("Mark");
        T1.setSexType(MAN_TYPE);
        T1.setAge(36);
        
        //测试主动15次gc, 项目中不要这么用
        for (int i = 0; i < 15; i++) {
            //主动触发GC 垃圾回收 15次--- T1存D活
            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;
    // set get ...
}
  • jps命令查找对应程序的进程id

然后在jhsdb输入进程号

显示所有的线程,我们需要看的是main线程

点击main,显示main线程相关信息

JHSDB查看所有类信息

可以看出JVM中所有的对象,都是基于class对象

包路径搜索cn.xxl.ex2

双击,可以看到已经创建了两个实例对象,就是T1和T2对象

inspector,查看实例对象信息

实例T1的物理地址是:0X0000000106aa2590

实例T2的物理地址是:0X0000000106000000

查看堆参数

可以看到实际JVM启动过程中堆中参数的对照,在不启动内存压缩情况下。堆空间的分代划分都是连续的

Gen0是Eden区: 0X0000000106000000~0X0000000106800000(T2在这个范围)

from区: 0X0000000106800000~0X0000000106900000

to区: 0X0000000106900000~0X0000000106a00000

Gen1是老年代(old区):0X0000000106a00000~0X0000000107e00000(T1在这个范围)

最后再对比地址一下堆中分代划分可以得出为什么 T1 在 Eden,T2 在老年代

因为T2经历了15次垃圾回收进入了老年代

JHSDB 中查看栈信息

虚拟机栈信息

frame:栈帧

expression stack:操作数栈

从上图中可以验证栈内存,同时也可以验证到虚拟机栈和本地方法栈在 Hotspot 中是合二为一的实现了。

五、从底层深入理解运行时数据区

深入辨析堆和栈(重点)

存储内容

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

线程独享还是共享

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

空间大小

  • 栈的内存要远远小于堆内存

六、栈的优化技术-栈帧之间数据的共享

在一般的模型中,两个不同的栈帧的内存区域是独立的,但是大部分的 JVM 在实现中会进行一些优化,使得两个栈帧出现一部分重叠。(主要体现在方法中有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用时就可以直接公用一部分数据,无需进行额外的参数复制传递了

使用 JHSDB 工具查看栈空间一样可以看到。

/**
 * VM参数
 * JVM对栈帧空间的优化
 **/
public class JVMStack {
    public int work(int x) throws Exception{
        //局部变量表有, 32位
        int z =(x+5)*10;
        Thread.sleep(Integer.MAX_VALUE);
        return  z;
    }
    public static void main(String[] args)throws Exception {
        JVMStack jvmStack = new JVMStack();
        //10  放入main栈帧  10 ->操作数栈 共享
        jvmStack.work(10);
    }
}

跑这段代码,用jhsdb查看栈。可以看到从下到上,以此是main栈帧,work栈帧,本地方法栈栈帧

七、内存溢出

(1)栈溢出

参数:-Xss1m

HotSpot 版本中栈的大小是固定的,是不支持拓展的。

java.lang.StackOverflowError 一般的方法调用是很难出现的,如果出现了可能会是无限递归。

OutOfMemoryError:不断建立线程,JVM 申请栈内存,机器没有足够的内存。(一般演示不出,演示出来机器也死了)

虚拟机栈带给我们的启示:方法的执行因为要打包成栈桢,所以天生要比实现同样功能的循环慢,所以树的遍历算法中:递归和非递归(循环来实现)都有存在的意义。递归代码简洁,非递归代码复杂但是速度较快。

同时要注意,栈区的空间 JVM 没有办法去限制的,因为 JVM 在运行过程中会有线程不断的运行,没办法限制,所以只限制单个虚拟机栈的大小。

(2)堆溢出

内存溢出:申请内存空间,超出最大堆内存空间。

如果是内存溢出,则通过 -Xms,-Xmx 参数调整。

-Xms30m -Xmx30m

如果不是内存泄漏,就是说内存中的对象却是都是必须存活的,那么就应该检查 JVM 的堆参数设置,与机器的内存对比,看是否还有可以调整的空间,再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行时的内存消耗。

java.lang.OutOfMemoryError:java heap space (创建对象申请内存空间不足报错,扩容)

java.lang.OutOfMemoryError:GC overhead limit exceeded(对象不断的添加,对象回收不动了,他认为都是有用的,回收效率不足2%,检查代码可能有问题)

(3)方法区溢出

元空间:-XX:MaxMetaspaceSize=30m

a:运行时常量池溢出

b:方法区中保存的 Class 对象没有被及时回收掉或者 Class 信息占用的内存超过了我们配置。

注意 Class (卸载)要被回收,条件比较苛刻(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):

1、该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。

2、加载该类的 ClassLoader 已经被回收。

3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

java.lang.OutOfMemoryError: Metaspace(class太多)

cglib 是一个强大的,高性能,高质量的 Code 生成类库,它可以在运行期扩展 Java 类与实现 Java 接口。

CGLIB 包的底层是通过使用一个小而快的字节码处理框架 ASM,来转换字节码并生成新的类。除了 CGLIB 包,脚本语言例如 Groovy 和 BeanShell,也是使用 ASM 来生成 java 的字节码。当然不鼓励直接使用 ASM,因为它要求你必须对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉。

(4)本机直接内存溢出

-XX:MaxDirectMemorySize=128M

直接内存的容量可以通过 MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常;

由直接内存导致的内存溢出,一个比较明显的特征是在 HeapDump 文件中不会看见有什么明显的异常情况,如果发生了 OOM,同时 Dump 文件很小,可以考虑重点排查下直接内存方面的原因。

java.lang.OutOfMemoryError: Direct buffer memory

总结:

jps 只看pid

jps -v 查看详细pid信息

-XX:+UseConcMarkSweepGC 指定cms垃圾回收器

-XX:-UseCompressedOops 禁止使用压缩指针

-XX:+PrintGCDetails 打印GC日志

java XX:+PrintCommandLineFlags -version 参数可查看默认设置收集器类型

java -XX:+PrintGCDetails -version亦可通过打印的GC日志的新生代、老年代名称判断

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值