请选择 进入手机版 | 继续访问电脑版

热点推荐

    查看: 81|回复: 0

    2021-02-23

    [复制链接]
  • TA的每日心情
    开心
    昨天 00:08
  • 签到天数: 362 天

    [LV.8]以坛为家I

    5万

    主题

    5万

    帖子

    16万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    160408
    发表于 7 天前 | 显示全部楼层 |阅读模式
    2021-02-19
    " V" q/ K: u; B0 E1 Y; i8 ^/ ^! S4 m基本问题:+ R+ R0 X9 C8 v& S4 P7 ]
    介绍下 Java 内存区域(运行时数据区)
    * y3 `/ Q2 h0 P& E0 X1 Y; u. jJava 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)  H' O1 x3 s6 Z
    对象的访问定位的两种方式(句柄和直接指针两种方式)
    6 o% N3 |  l4 A2 y6 `' k: n( w拓展问题:- j0 t2 B8 h6 U* B
    String类和常量池5 a6 ?) G+ K0 n& ^" V- W
    8种基本类型的包装类和常量池
    ' `8 K. |# |# ~1 概述9 y$ v6 W! a; g, i8 k' {
    对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
    ! G8 |1 m0 N" a- x3 m4 Q2 运行时数据区域3 Y$ U( g' V  k1 W6 }$ N# p( P3 d
    Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。, t1 o9 }% Q, s. `' p: v: N
    这应该是把Java内存区域讲的最清楚的一篇文章# X. a6 g5 g8 l8 Z- a$ n- M3 C( O
    这应该是把Java内存区域讲的最清楚的一篇文章
    9 E1 K( ~9 A- M" y$ V% c$ k5 u' L这些组成部分一些是线程私有的,其他的则是线程共享的。% W/ {1 Z' v% K" @7 i
    线程私有的:
    " \# W$ R4 g8 j' L! y程序计数器" f) p6 H! U, ?) L
    虚拟机栈5 N6 f1 s/ H/ Z$ O
    本地方法栈
    " h! x6 V5 g+ V( J3 n7 p线程共享的:( _0 b9 T) D2 K
    - v6 E1 L" z4 X- r* l7 j
    方法区
    8 D. t  ^; T. ~4 @9 ~直接内存
    # T6 o- n+ {. X4 R- y2.1 程序计数器
    5 l# X- J$ _$ x1 D& p0 }程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。9 p1 b8 ~9 |! }0 S% d0 _
    另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。  f! Y: L$ X8 _
    从上面的介绍中我们知道程序计数器主要有两个作用:
    / X. w/ q) E7 B# f2 }6 V字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
    - G' y3 k- d; G7 N" M4 ]. \在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。% t3 r, N. y8 }2 S" p3 c
    注意:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。: l# H( a% c: G( {
    2.2 Java 虚拟机栈/ o9 H, ?+ ]) Q+ e. ?& \
    与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。" ^9 W& X; |$ P" U4 `. C
    Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)% H+ R0 N" X% l5 G* |- A# }  `# Z
    局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。: I6 B/ Y5 V1 }5 R
    Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
    ! Q. u7 W/ E9 Y3 X2 H6 nStackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
    , S' o. f1 P0 R3 cOutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
    * _: L0 _1 X+ ~) b4 ^Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
    8 K8 L0 b. S: N) i4 @6 W+ k2.3 本地方法栈+ K' j3 _# K9 N' Y7 Q5 H3 O
    和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。, \3 L7 Z* Y% s0 @
    本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。9 z7 T! M" B" q" E( ]6 V$ j
    方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。( {5 a" G. D1 J/ p8 y" H3 ]
    2.4 堆
    6 e& i0 [0 x4 k% Z) k; WJava 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。, Y9 S" [& ^* _- j- F! I* _) l
    Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
      x8 k% F  [! s% b7 W( [这应该是把Java内存区域讲的最清楚的一篇文章
    & E' }, o6 D7 T" h这应该是把Java内存区域讲的最清楚的一篇文章( H2 ^: I* @4 Z
    在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。3 y' D  _8 ]3 t* |7 U5 k- [
    推荐阅读:9 \# B9 w/ b3 y4 g% ?8 h. k
    《Java8内存模型—永久代(PermGen)和元空间(Metaspace)》:http://www.cnblogs.com/paddix/p/5309550.html
    / e8 D" G- H0 z2.5 方法区2 V( z! v4 D% S
    方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
    + d" b- z$ Y) i% r/ Q& U5 gHotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。- Q' y: ^; @) n$ Y' q, a1 |+ Z) m+ ], p
    相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
    ( W) l6 i8 r9 f2.6 运行时常量池1 ]) w* c, l3 p+ s0 t& V! S# D
    运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)- b" N: C( H. e+ p
    既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
    1 q1 S/ }: Q& x/ O8 lJDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
    - i4 ^% x( s/ Z1 Y0 w' e. w这应该是把Java内存区域讲的最清楚的一篇文章
    4 M& E) F) n% K" `' F  V1 x, @这应该是把Java内存区域讲的最清楚的一篇文章- m; S4 x$ Y2 Q; }# C+ e1 v: n* r
    ——图片来源:https://blog.csdn.net/wangbiao007/article/details/78545189
    5 ]* ?# l" a+ ?5 t& \9 |推荐阅读:
    , K# n( M9 Z: r, f0 J* s  Z2 P' F' ^0 X《Java 中几种常量池的区分》: https://blog.csdn.net/qq_26222859/article/details/73135660$ R2 y/ j, M9 T! w) v8 _4 J& F5 o) J% U
    2.7 直接内存+ J) S  s/ b7 k1 q  q) a
    直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。4 h) `! V5 S3 J: E+ |  R8 ^) c
    JDK1.4中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。4 ]' a4 d4 T7 s  E
    本机直接内存的分配不会收到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。: P) }1 H, L* H  G' o$ ?, K; r2 ^
    3 HotSpot 虚拟机对象探秘: L1 j9 {5 w3 C; f7 ~  v# L5 b
    通过上面的介绍我们大概知道了虚拟机的内存情况,下面我们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。! S! Y7 L/ s6 P3 m0 ?7 k
    3.1 对象的创建$ q; W6 y' }" |! B
    下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。5 v8 U5 r7 o" }7 j
    这应该是把Java内存区域讲的最清楚的一篇文章
    ( B4 f+ y4 ]6 @* h& C这应该是把Java内存区域讲的最清楚的一篇文章! g/ p: Y& U9 v% F# h
    ①类加载检查: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
    6 L, B" A2 m. B" R2 A) W4 O) k7 ^②分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
    $ D" }- [+ b+ ?1 y2 T4 v内存分配的两种方式:(补充内容,需要掌握)2 e+ D1 X1 Q$ c
    选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
    ( b. T5 g8 e- Z这应该是把Java内存区域讲的最清楚的一篇文章6 \0 a) [! Z9 s8 r! K
    这应该是把Java内存区域讲的最清楚的一篇文章0 x) {4 p' K4 F% i1 U, N
    内存分配并发问题(补充内容,需要掌握); R3 I& l+ O5 I) S4 q
    在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:8 D! h; u8 o& a5 E
    CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。! ]5 P) h, w5 M8 W
    TLAB: 为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配
    7 \+ I4 B3 o7 @2 F0 x/ ~③初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
    4 U0 I3 }$ h% }7 d7 [, |2 o④设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
    1 V% G' `! s) ]( c⑤执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行  方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。1 S& |( L3 \9 k0 a
    3.2 对象的内存布局
    + z  Z8 V3 O1 I在 Hotspot 虚拟机中,对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。8 `4 g  i& J+ K7 M
    Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希码、GC分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过' a6 [# y. J$ `; T) a9 R% S7 I
    / j$ p) b  v5 P# m  E
    Java吧 收集整理 java论坛 www.java8.com
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    快速回复 返回顶部 返回列表