JDK1.8-Java虚拟机运行时数据区域和HotSpot虚拟机的内存模型

2024-04-10 13:18

本文主要是介绍JDK1.8-Java虚拟机运行时数据区域和HotSpot虚拟机的内存模型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

介绍
初学Java虚拟机几天, 被方法区, 永久代这些混杂的概念搞混了. 我觉得学习这部分知识应该把官方定义的虚拟机运行时数据区域和虚拟机内存结构分开叙述, 要不然容易误导.
本文先介绍官方文档规定的运行时数据区域, 然后以JDK1.8的HotSpot虚拟机为例, 介绍虚拟机的内存结构.
官方文档规定的运行时数据区域
官方文档中规定的运行时数据区一共就几块: PC计数器, 虚拟机栈, 本地方法栈, 堆区, 方法区, 运行时常量池. 这里的官方规定是说, 如果你要做一个Java虚拟机的话, 必须要包含这几个区域, 但是这几个区域在你的虚拟机中是用哪块内存实现的, 这由虚拟机制作者决定.
程序计数器
The pc Register, 程序计数器. 如果了解过计算机系统, 对这个名词应该不陌生了, 它指向下一条指令的地址, 程序靠它跑起来.
Java虚拟机支持多线程, 每条线程都有自己的程序计数器.
如果当前线程正在执行一个Java方法, 它的计数器记录的是正在执行的Java虚拟机指令的地址. 如果执行的是本地方法(比如系统的C语言函数), 计数器中的值为空(Undefined).
正因为程序计数器记录的是指令地址, 所以它占用的空间较少, Java虚拟机规范中并没有规定这块内存有OutOfMemoryError(内存溢出)的情况.

Java虚拟机栈
Java Virtual Machine Stacks, Java虚拟机栈.
Java虚拟机栈是线程私有的, 生命周期与线程相同. 虚拟机栈存放栈帧, 栈帧用于存储局部变量表, 部分结果值, 方法的初始化参数和返回信息, 方法的执行通过栈帧的压栈和出栈实现.
本地方法栈
本地方法栈和上面的虚拟机栈是相似的, 从名字也看出, 虚拟机方法栈是用来执行Java代码的, 而本地方法栈则是用来执行本地系统代码的, 比如C代码.
也因为规范中没有规定本地方法栈执行的代码, 如果想执行Java代码也是可以的, 我们可以看到Oracle官方的虚拟机HotSpot虚拟机把Java虚拟机栈和本地方法栈合二为一, 这么做避免了要为不同的语言设计栈, 提高了虚拟机的性能.
虚拟机栈和本地方法栈溢出
那么当出现错误信息后, 我们在什么错误信息下可以去排查是否虚拟机栈和本地方法栈这两块内存出错呢? 这里以HotSpot虚拟机为例讲解(HotSpot把两块栈结构合在一起实现了), 在JDK1.8的虚拟机规范中对这两块栈空间可能出现的错误给出了相同的描述.
一: 如果一条线程所需要的内存大于虚拟机所分配给它的内存, 将抛出StackOverflowError异常.
二: 如果栈内存可以扩展并尝试扩展时可用的内存不足, 或者创建新线程并为其分配栈内存时可能的内存不足, 会抛出OutOfMemoryError
下面先演示第一个StackOverflowError异常
//设置虚拟机参数 -Xss128k, 设置单个线程的栈空间大小为128k
public class StackErrorTest1 {
private int stackLength = 1;

public void stackLeak(){stackLength++;stackLeak();
}public static void main(String[] args) {StackErrorTest1 set1 = new StackErrorTest1();try{set1.stackLeak();}catch (Throwable e){System.out.println("stack length:" + set1.stackLength);e.printStackTrace();}
}

}
//输出异常信息
stack length:1000
java.lang.StackOverflowError
at jvm.StackErrorTest1.stackLeak(StackErrorTest1.java:7)
at jvm.StackErrorTest1.stackLeak(StackErrorTest1.java:8)

所以当遇到StackOverflowError时可以考虑是否是是虚拟机的栈容量太小, 比如这里的无穷递归, 栈空间不够用. 当然生产环境中肯定不会写无穷递归, 这时可以通过设置-Xss参数调整单条线程的栈内存大小.
上面描述的栈内存可以扩展并尝试扩展时可用的内存不足导致出现OutOfMemoryError的情况暂时没有好的演示代码, 在周志明的《深入理解Java虚拟机》中提到"定义了大量本地变量,增大方法帧中本地变量表的长度, 结果仍抛出StackOverflowError". 不知道是不是没有触发虚拟机动态扩充栈空间, 所以仍然判定是栈所需的空间超出了虚拟机规定的大小. 总结来说无论是栈帧太大还是栈空间太小都会抛出StackOverflowError, 可以考虑调整-Xss参数.
上面还提到当创建新线程并分配新的栈空间时, 如果可用的内存不够, 会抛出OutOfMemoryError异常, 下面是这种情况的代码演示.
public class StackErrorTest2 {

private void keepRunning(){while(true){}
}public void stackLeakByThread(){while(true){Thread thread = new Thread(new Runnable() {@Overridepublic void run() {keepRunning();}});thread.start();}
}public static void main(String[] args){StackErrorTest2 set2 = new StackErrorTest2();set2.stackLeakByThread();
}

}
//运行结果, 来源《深入理解Java虚拟机》
Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread
这段代码也来自深入理解jvm, 书中也说明跑这段代码要小心, 因为Java的线程是映射到内核线程上的, 果不其然我的机子一跑就死机了.
问什么会出现这样的错误? 32位Windows系统分配给一个进程的内存最大为2GB(32位能寻址4GB地址空间, 除去内核的空间剩2GB, 64位则大得多). 这2GB减去最大堆容量, 减去方法区的容量, 剩下的就是虚拟机栈和本地方法区栈的内存空间了. (补充: PC计数器占的空间很小, 运行时常量池在方法区中, HotSpot中虚拟机栈和本地方法栈一起实现, 所以能分成这么三大块内存).
了解了三大块内存区后(HotSpot下), 解决思路也出来了: 1. 减小最大堆内存, 腾出更多位置给栈空间. 2. 如果程序的线程数量不可以减少, 那么就看看是否可以减少每条线程的栈内存.
当然用一台配置高的机器, 该用64位的Java虚拟机也是一种方法.
Java堆
Java堆是随着虚拟机的启动而创建的, 用于存放对象实例, 所有的对象实例和数组都在堆内存分配, 它被所有线程共享. Java堆是Java虚拟机管理的内存中最大的一块, 也是垃圾回收器管理的主要区域. 从内存回收的角度看, Java堆内存还可以被继续划分, 并且和具体的虚拟机实现有关.
当前主流的虚拟机都是支持堆内存动态扩展的, 就是说当堆内存的大不够时, 它会扩充容量; 当不要太多的空间时, 它能自己进行压缩. 我们可以人为地通过-Xmx和-Xms设定堆内存的最大值和最小值(初始大小). 如果我们把-Xmx和-Xms设置为相同的值, 就等同于设定了固定大小的Java堆. (这是gc调优的一种手段)
若堆内存分配内存时发现已经没有更过可用空间时, 会抛出OutOfMemoryError.
演示堆内存溢出
堆内存是存放对象实例的地方, 这个应该比较好理解, 直接上代码
/**

  • VM Args: -Xms20m -Xmx20m
    */
    public class HeapErrorTest {
    static class Object{
    }

    public static void main(String[] args) {
    List list = new ArrayList<>();
    while(true){
    list.add(new Object());
    }
    }
    }
    //运行结果
    Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
    由结果可以看到当堆内存溢出后除了有java.lang.OutOfMemoryError外, 还会提示Java heap space. 在这个例子中, 我们明确地知道了是由于堆内存不够大而造成的溢出. 然而在生产环境中, 当系统报出堆内存溢出时, 我们首先要搞清楚是因为内存泄漏导致的内存溢出, 还是纯粹的内存溢出.
    内存溢出指的是分配内存的时候, 没有足够的空间供其使用. 内存泄漏指的是在分配一块内存使用完后没有释放, 在Java中对应的场景是没有被垃圾回收器回收. 一点点的内存泄漏用户可能感受不到, 但是当泄漏的内存积少成多的时候, 会耗尽内存, 导致内存溢出.
    有一些常用的分析内存溢出的手段和工具, 这里就不详细叙述了, 可以参考书籍或网上的资料. 当我们判断是内存泄漏导致的溢出后, 可以根据工具定位出现泄漏的代码位置; 如果不存在泄漏只是单纯的溢出的话, 可以通过设置虚拟参数调整堆内存大小(前提是机器的配置能够支持相应的内存大小), 或者看看代码中是否存在一些生命周期很长的对象实例, 看看能否作出修改.
    方法区
    方法区用于存储以被虚拟机加载的类信息, 常量, 静态变量, 即时编译器编译后的代码数据等, 它是所有线程共享的. 虚拟机规范中说方法区在逻辑上是堆的一部分, 但是它的别名叫"non-Heap"也就是非堆的意思, 表明它和堆内存是两块独立的内存. 至于说在逻辑上是堆区的一部分, 是因为在物理实现上, 方法区的内存地址包含于堆中, 所以说是逻辑上的一部分, 实际用的时候是完全不同的部分. 这么设计可能是因为便于垃圾收集器统一管理吧.
    运行时常量池
    运行时常量池的内存由方法区分配, 也就是说它属于方法区的一部分. 它用于存储Class文件中的类版本, 字段, 方法, 接口和常量池等, 也用于存放编译期生成的各种字面量和符号引用.
    运行时常量池区别于Class文件常量池的一个重要特征是具备动态特性. 也就说并非在Class文件中定义的常量才能进入运行时常量池, 在程序运行的过程中也有可能将新的常量放入池中.
    演示方法区溢出
    演示方法区溢出和堆区的思路一样, 不断往方法堆中加入东西使其溢出. 只是方法区中保存的是类信息, 我们通过不断动态生成类演示
    本代码示例来源于深入理解jvm, 但是其中的参数需要改变, 该书的最新版本是基于JDK1.7的, JDK1.7中方法区是在永久代中实现的, 而JDK1.8中已经没有永久代了, 方法区中Metaspace元数据区中, 通过设置-XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定方法区的大小
    /**

  • VM Args: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
    */
    public class MethodAreaTest {

    static class Object{
    }

    public static void main(String[] args) {
    int count = 0;
    while (true) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(Object.class);
    enhancer.setUseCache(false);
    enhancer.setCallback(new MethodInterceptor() {
    @Override
    public java.lang.Object intercept(java.lang.Object o, Method method, java.lang.Object[] objects, MethodProxy methodProxy) throws Throwable {
    return methodProxy.invokeSuper(objects, objects);
    }
    });
    enhancer.create();
    System.out.println(++count);
    }
    }
    }

运行结果:
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
… 8 more

HotSpot虚拟机的内存模型
在介绍完Java虚拟机运行时数据区域后, 接着以HotSpot虚拟机为例介绍虚拟机内存模型.
首先有一个重要的概念要搞清楚, 要不然容易犯晕.
在前面介绍Java运行时数据区域时我们谈到PC计数器, 虚拟机栈, 本地方法栈这3块内存都是线程私有的, 它们的随线程的创建而分配, 随线程的结束而释放, 也就是说Java虚拟机是明确知道这三块内存是什么时候该被回收的, 只要线程没执行完就不能回收, 否则线程跑不起来.
而我们在谈论虚拟机的内存模型时, 通常要和垃圾回收结合在一起讨论. 既然上面的三块内存回收的时间已定, 暂时不需要过多考虑, 虚拟机分配内存时给它们留有空间就行.
但另外的两块内存堆内存和方法区则不一样, 它们是所有线程共享的, 在这里面内存的分配和释放具有不确定性. 比如说在多态的情况下, 一个接口对应的实现类不同, 具体的实现方法也不同, 虚拟机只有在程序运行的过程中才知道要创建哪些对象, 这部分内存的分配和释放都是动态的, 垃圾收集器关注的也是这部分的内容.
所以说我们后续描述的虚拟机内存模型是建立在Java堆内存和方法区上的.
JVM实现的堆内存和方法区
正如上述所说, 当谈论JVM的内存结构时, 讨论的重点就由整个运行时数据区域转为对堆内存和方法区的讨论, 因为这两部分是垃圾回收的重点区域(如果两者要比较的话, 重点收集区域是堆区).
而HotSpot虚拟机的内存结构由三大部分组成: 新生代, 老年代和元数据区(JDK1.7及以前叫老年代). 其中新生代和老年代是虚拟机规范中Java堆内存的实现, 元数据区是规范中方法区的实现. 在讲述为什么这么定义之前, 先明确这个关系对于理解概念是很重要的, 下面有幅图帮助理解.

这里有个小失误, 题目中明明讲的是JDK1.8, 为什么还提永久代呢? 由于永久代存在的时间长, 永久代的说法经过这么多年可能已经深入人心, 所以先并列讲, 要知道永久代和元数据区是有本质的差别的, 这留到后面讲, 先认清概念.
希望图片加描述能够帮助你立即规范定义的数据区域和JVM内存结构之间的关系. 下面将对HotSpot虚拟机的内存模型做进一步分析.
新生代和老年代.
Java堆内存被实现为新生代和老年代, 是为了更方便地进行垃圾回收. 我们知道对象是存储在堆内存中的, 从字面上理解新生代就是新创建的对象区域, 老年代就是使用多次生命周期长的对象区域. 新生代对象生命周期通常较短, 很多用完即可以释放; 老年代对象的生命周期较长, 可能在整个程序的运行过程中都是有用的.
由于新对象和老对象具有不同的性质, 为对这两种对象设计的垃圾回收算法也不同, 所以要把它们分开.
新生代中的内存划分
新生代的内存被分为一个Eden区和两个Survivor区. 为了讲述为什么要这么分, 需简单引入垃圾回收算法.
首先最基础, 最简单的垃圾回收算法叫标记-清除算法. 算法流程和算法名完全一致: 首先标记出哪些是可以回收的对象, 标记完后把对象清除. 如果按照这么个流程, 新生代应该就是一块简单的内存就行, 现实结论告诉我们这个算法是可以优化的.
标记清除算法的不足在于一块完整的内存在经过标记-清除算法后有些内存会被释放掉, 这时会造成内存空间不连续, 可能不能够存放一些较大的对象.
标记-清除算法的升级版是复制算法, 它在标记-清除的思路上作出了些改变. 首先将内存分为两块, 当创建新对象分配内存的时候只用两块中的一块A. 当进行垃圾回收的时候只对有对象的一块A内存使用标记-清除算法进行回收, 回收后剩余的存活对象从内存A移到另一块空的内存B中, 这样A内存重新变为空内存, 继续重复此分配回收过程. 这个算法似乎更好一些, 但是也只是两块内存, 说明还不是现实中的最优解.
考虑新的算法, 把内存分配成均等两块, 等同于能够使用的内存变为原来的二分之一了, 根据IBM专门部分研究新生代中百分之98%的对象都是"朝生夕死"的, 也就是说在进行垃圾回收时98%的对象都被回收掉, 只有2%会从A内存移动到B内存. 这么一想我们把两块内存割为相同的两块是不是有点太亏了?
下面揭晓答案: HotSpot虚拟机回收虚拟机时使用的是复制算法, 但是它分成三块内存, 一个占80%内存的Eden区(堆内存), 两个分别占10%的Survivor区. 具体操作是这样的: 程序运行时, 用Eden区和一个Survivor区A存放新创建的对象. 当发生垃圾回收时, 把存活下来的对象(很少)复制到另一块Survivor区B中, 使得Eden区和Survivor区A重新为空, 然后继续重复这个分配回收的过程.
所以说详细点的Jvm的内存模型是下面这样的

由JDK1.7及以前的永久代到JDK1.8的元数据区
搞定完堆区在JVM内存模型中的实现, 下面谈论方法区的实现.
在JDK1.7及以前, JVM使用永久代来实现方法区. 这里用"实现"二字是经过斟酌的, 因为永久代并不等同于方法区. 从名字也可以看出它和新生代, 老年代是一脉相承的, 逻辑上是一体的, 命名为永久代是因为这部分内存很少几乎不被回收. 这一很少几乎不被回收的特性正好对应方法区中存储的类信息, 常量, 静态变量等元素. 所以说用永久代来实现方法区.
但是用永久代来实现方法区并不是最优解, 比如容易出现内存溢出问题(具体分析去除永久代, 改用Metaspace的原因可以参考文章末尾所列出的资料). 在JDK1.8中JVM改为使用元数据区来实现方法区.
元数据区和永久代有着本质的区别, 永久代属于虚拟机内存的一部分, 也就是说当在操作系统中启动虚拟机进程时为它分配了一块内存, 而虚拟机为永久代分配内存时用的是它自己分配得的内存.
而元数据区Metaspace是直接在本地内存(Native Memory)中申请的, 这样元数据区的大小(方法区大小)只会受本地内存大小限制, 和虚拟机进程所分得内存无关.
所以最后JVM内存模型图的终极版应该是这样子
深圳网站建设www.sz886.com

这篇关于JDK1.8-Java虚拟机运行时数据区域和HotSpot虚拟机的内存模型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/891166

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定