java虚拟机的内存管理二

Java 堆概念

对于Java应用程序来说, Java堆(Java Heap) 是虚拟机管理的内存中最大的一块。 Java堆是被所有线程共享
的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, Java 世界里几乎所有的对
象实例都在这里分配内存。
在这里插入图片描述

堆的特点

1、是Java虚拟机所管理的内存中最大的一块
2、堆是jvm所有线程共享的。
3、在虚拟机启动的时候创建。
4、唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
5、Java堆是垃圾收集器管理的主要区域。
6、因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器
基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。
7、java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。
8、方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。
9、如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

设置堆空间大小

使用示例: -Xmx20m -Xms5m
说明: 当下Java应用最大可用内存为20M, 最小内存为5M
测试:
在这里插入图片描述
设置堆大小:
在这里插入图片描述
执行结果:
在这里插入图片描述
大家可以发现,这里打印出来的Xmx值和设置的值之间是由差异的,total Memory和最大的内存之间还是存在一定
差异的,就是说JVM一般会尽量保持内存在一个尽可能低的层面,而非贪婪做法按照最大的内存来进行分配。

在测试代码中新增如下语句,申请内存分配:
在这里插入图片描述
在申请分配了4m内存空间之后,total memory上升了,同时可用的内存也上升了,可以发现其实JVM在分配内存过
程中是动态的, 按需来分配的。
结果:
在这里插入图片描述

堆的分类

现在垃圾回收器都使用分代理论,堆空间也分类如下
在Java7 Hotspot虚拟机中将Java堆内存分为3个部分:

  • 青年代Young Generation
  • 老年代Old Generation
  • 永久代Permanent Generation

在这里插入图片描述
在Java8以后,由于方法区的内存不在分配在Java堆上,而是存储于本地内存元空间Metaspace中,所以永久代就不
存在了。
在这里插入图片描述

年轻代和老年代

1、 年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分
成1个Eden Space和2个Suvivor Space(from 和to)
2、老年代(Tenured Gen):老年代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。
在这里插入图片描述
配置新生代和老年代堆结构占比:
默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3
修改占比 -XX:NewPatio=4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5 Eden空间和另外两个Survivor空间占比分别为8:1:1可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=8
几乎所有的java对象都在Eden区创建, 但80%的对象生命周期都很短,创建出来就会被销毁。
在这里插入图片描述
从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生
代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

对象分配过程

分配过程:
1、new的对象先放在伊甸园区。该区域有大小限制。
2、当Eden区域填满时,程序又需要创建对象,JVM的垃圾回收器将对Eden预期进行垃圾回收(Minor GC),将Eden区域中不再被其他对象引用的额对象进行销毁,再加载新的对象放到Eden区。
3、将Eden区中的剩余对象移动到Suvivor0区。
4、如果再次触发垃圾回收,此时上次幸存下来的放在Suvivor0区的,如果没有回收,就会放到Suvivor1区。
5、如果再次经历垃圾回收,此时会重新返回Suvivor0区,接着再去Suvivor1区。
6、如果累计次数到达默认的15次,这会进入老年代。(可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N)
7、老年代内存不足是会再次出发GC:Major GC 进行老年代的内存清理。
8、如果老年代执行了Major GC后仍然没有办法进行对象的保存,就会报OOM异常。
在这里插入图片描述
在这里插入图片描述
分配对象的流程:
在这里插入图片描述

堆GC

Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:一种是部分收集器(Partial GC)另一类是整堆收集器
(Fu’ll GC)。
部分收集器: 不是完整收收集器,它又分为:

  • 新生代收集(Minor GC / Young GC): 只是新生代的垃圾收集
  • 老年代收集 (Major GC / Old GC): 只是老年代的垃圾收集 (CMS GC 单独回收老年代)
  • 混合收集(Mixed GC):收集整个新生代及老年代的垃圾收集 (G1 GC会混合回收, region区域回收)
    整堆收集(Full GC):收集整个java堆和方法区的垃圾收集器。

1、年轻代GC触发条件:

  • 年轻代空间不足,就会触发Minor GC, 这里年轻代指的是Eden代满,Survivor不满不会引发GC。
  • Minor GC会引发STW(stop the world) ,暂停其他用户的线程,等垃圾回收接收,用户的线程才恢复。

2、老年代GC (Major GC)触发机制

  • 老年代空间不足时,会尝试触发MinorGC. 如果空间还是不足,则触发Major GC。
  • 如果Major GC , 内存仍然不足,则报错OOM。
  • Major GC的速度比Minor GC慢10倍以上。

3、FullGC 触发机制

  • 调用System.gc() , 系统会执行Full GC ,不是立即执行。
  • 老年代空间不足。
  • 方法区空间不足。
  • 通过Minor GC进入老年代平均大小大于老年代可用内存。

元空间

在JDK1.7之前,HotSpot 虚拟机把方法区当成永久代来进行垃圾回收。而从 JDK 1.8 开始,移除永久代,并把方法
区移至元空间,它位于本地内存中,而不是虚拟机内存中。 HotSpots取消了永久代,那么是不是也就没有方法区了
呢?当然不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是元空间(Metaspace)而已。
它和永久代有什么不同呢?

  • 存储位置不同: 永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
  • 存储内容不同: 在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信
    息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
    在这里插入图片描述
为什么要废弃永久代,引入元空间?
  • 在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其
    中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久
    代内存溢出。
  • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。不会遇到永久代存在时的内存溢出错误。
  • 将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性。
  • 将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。
Metaspace相关参数、
  • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。如果没有使用该参数来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进而可能出现swap内存被耗尽;最终导致进程直接被系统直接kill掉。
  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。
  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。

方法区

方法区(Method Area) 与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载 的类型信息、常量、 静态变量、 即时编译器编译后的代码缓存等数据。元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载的类信息的。
在这里插入图片描述
创建对象各数据区域的声明:
在这里插入图片描述
方法区的特点:
1、方法区与堆一样是各个线程共享的内存区域。
2、方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续。
3、方法区的大小跟堆空间一样 可以选择固定大小或者动态变化。
4、方法区的对象决定了系统可以保存多少个类,如果系统定义了太多的类 导致方法区溢出虚拟机同样会跑出(OOM)异常(Java7之前是 PermGen Space (永久带) Java 8之后 是MetaSpace(元空间) )。
5、关闭JVM就会释放这个区域的内存。

方法区结构在这里插入图片描述

类加载器将Class文件加载到内存之后,将类的信息存储到方法区中。
方法区中存储的内容:
1、类型信息(域信息、方法信息)。
2、运行时常量池。
在这里插入图片描述

类型信息

对每个加载的类型(类Class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:

  • 这个类型的完整有效名称(全名 = 包名.类名)
  • 这个类型直接父类的完整有效名(对于 interface或是java.lang. Object,都没有父类)
  • 这个类型的修饰符( public, abstract,final的某个子集)
  • 这个类型直接接口的一个有序列表
域信息
  • 域信息,即为类的属性,成员变量
  • JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序。
  • 域的相关信息包括:域名称、域类型、域修饰符(pυblic、private、protected、static、final、volatile、transient的某个子集)
方法信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

  • 方法名称方法的返回类型(或void)
  • 方法参数的数量和类型(按顺序)
  • 方法的修饰符public、private、protected、static、final、synchronized、native,、abstract的一个子集
  • 方法的字节码bytecodes、操作数栈、局部变量表及大小( abstract和native方法除外)
  • 异常表( abstract和 native方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。
方法区设置

方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。
一、jdk7及以前

  • 通过-xx:Permsize来设置永久代初始分配空间。默认值是20.75M
  • -XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
  • 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。

查看JDK PermSpace区域默认大小:

jps #是java提供的一个显示当前所有java进程pid的命令
jinfo -flag PermSize 进程号 #查看进程的PermSize初始化空间大小
jinfo -flag MaxPermSize 进程号 #查看PermSize最大空间

二、JDK8以后

  • 元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定。默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace。
  • -XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-xx:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,FullGC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
  • 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到FullGC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

运行时常量池

1、常量池vs运行时常量池

  • 字节码文件中,内部包含了常量池
  • 方法区中,内部包含了运行时常量池
  • 常量池:存放编译期间生成的各种字面量与符号引用
  • 运行时常量池:常量池表在运行时的表现形式
    编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。理解为字节码中的常量池 Constant pool 只是文件信息,它想要执行就必须加载到内存中。而Java程序是靠JVM,更具体的来说是JVM的执行引擎来解释执行的。执行引擎在运行时常量池中取数据,被加载的字节码常量池中的信息是放到了方法区的运行时常量池中。

它们不是一个概念,存放的位置是不同的。一个在字节码文件中,一个在方法区中。
在这里插入图片描述
对字节码文件反编译之后,查看常量池相关信息:
在这里插入图片描述
要弄清楚方法区的运行时常量池,需要理解清楚字节码中的常量池。一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表( Constant pool table),包括各种字面量和对类型、域和方法的符号引用。常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。常量池表Constant pool table:
在这里插入图片描述
在方活中对常量池表的符号引用:在这里插入图片描述

为什么需要常量池?

举例来说:

public class Solution {
	public void method() {
		System.out.println("are you ok");
		}
}

这段代码很简单,但是里面却使用了 String、 System、 PrintStream及Object等结构。如果代码多,引用到的结构会更多!这里就需要常暈池,将这些引用转变为符号引用,具体用到时,采取加载。

直接内存

直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分。在JDK 1.4中新加入了NIO(New Input/Output) 类, 引入了一种基于通道(Channel) 与缓冲区 (Buffer) 的I/O方式, 它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了 在Java堆和Native堆中来回复制数据。
在这里插入图片描述
在这里插入图片描述NIO的Buffer提供一个可以直接访问系统物理内存的类——DirectBuffer。DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同。普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的 限制。而DirectBuffer直接分配在物理内存中,并不占用堆空间。在访问普通ByteBuffer时,系统总是会使用一个“内核缓冲区”进行操作。而DirectBuffer所处的位置,就相当于这个“内核缓冲区”。因此,使用DirectBuffer是一种更加接近内存底层的方法,所以它的速度比普通的ByteBuffer更快。

在这里插入图片描述
通过使用堆外内存,可以带来以下好处:
1、 改善堆过大时垃圾回收效率,减少停顿。Full GC时会扫描堆内存,回收效率和堆大小成正比。Native的内存,由OS负责管理和回收。
2、 减少内存在Native堆和JVM堆拷贝过程,避免拷贝损耗,降低内存使用。
3、 可突破JVM内存大小限制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

youngerone123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值