jvm

1.JVM运行时划分哪几个区域?哪些区域是线程共享的?哪些区域是线程独占的?

JVM运行时一共划分:程序计数器、虚拟机栈、堆、本地方法栈、方法区。

线程共享的数据区域:堆、方法区。

线程独享的数据区域区域:程序计数器、虚拟机栈、本地方法栈。

连问(1)这几个内存区域分别存放什么数据?

程序计数器: 记录当前线程执行的位置

虚拟机栈: 存储基本数据类型以及对象的引用等

: 存储对象实例

本地方法栈: 与虚拟机栈类似,它为Native方法服务

方法区: 存储被JVM加载的类信息、常量、静态变量等。

2.JVM内存怎么分配的

方法区:

有时候也称为永久代(Permanent Generation)

注意: 在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。

方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。

在方法区中,存储了每个类的信息(包括类的名称、修饰符、方法信息、字段信息)、类中静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息以及编译器编译后的代码等。当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,在这里进行的GC主要是方法区里的常量池和类型的卸载。当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

在方法区中有一个非常重要的部分就是运行时常量池,用于存放静态编译产生的字面量和符号引用。运行时生成的常量也会存在这个常量池中,比如String的intern方法。它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。

JVM堆分代

1、JVM堆被分为了年轻代和老年代。年轻代的GC过程称为Yong GC,速度快较频繁。老年代的GC过程称为Full GC,速度较慢应该尽量避免。

2、对象被创建后,除了少部分大对象会在老年代分配内存外,大部分的对象首先都是在年轻代进行内存分配,而且大部分的对象都是“朝生夕死”,很快就会被年轻代的Yong GC回收掉。

3、老年代的内存空间一般会比年轻代的内存空间大,能存放的对象多,老年代的空间不足后会进行Full GC操作,比Yong GC耗时,所以应尽量避免频繁的Full GC操作。

年轻代的分区

1、年轻代中分为一个Eden区和两个Surviver区,比例为8:1:1,两个Surviver区分别称为“From”区和“To”区。对象在Eden区创建,经过一次Yong GC后,还存活的对象将会被复制到Surviver区的“From”区,此时“To”区是空的。到了下一次GC的时候,Eden区还存活的对象会复制到Surviver区的“To”区,而“Form”区的对象有两个去处,“From”区的对象会根据经过的GC次数计算年龄,如果年龄到达了阈值(默认15),则会被移动到老年代中,否则就复制到“To”区,此时“From”区变成了空的,然后“From”区和“To”区进行角色互换,到下一次进行GC时,还是有一块空的“To”区,用来存放从eden区和“From”区移动过来的对象。

2、那这种分区有什么好处呢?

a、在年轻代新增Surviver区,有利于减轻老年代的负担,尽可能的让大部分对象在年轻代通过较高效的Yong GC回收掉,不至于老年代里存放的对象过多导致内存不足而进行频繁的Full GC操作。

b、这种分区有利于减少内存碎片的产生。

首先我们来看看,如果年轻代只分为Eden区和Surviver区两个区域并且比例是8:2的时候,内存的回收和分配情况会怎么样。第一次Yong GC后,Eden区还存活的对象移动到Surviver区,Surviver区还存活的对象保留在Surviver区,而这些对象的内存是不连续的,Surviver区里就会产生很多内存碎片,这就会导致有些大对象要移动到Surviver区的时候,没有足够的连续内存进行分配,而不得不移动到老年代中,增加老年代的负担,降低效率。

然后我们看看Eden区和Surviver区的比例是8:1:1时会有什么样的效果。第一次Yong GC后,Eden区还存活的对象复制到Surviver区的“To”区,“From”区还存活的对象也复制到“To”区,再清空Eden区和From区,这样就等于“From”区完全是空的了,而“To”区也不会有内存碎片产生,等到第二次Yong GC时,“From”区和“To”区角色互换,很好的解决了内存碎片的问题。

详细的过程:

1.当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收

一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区。

2.这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区

3.再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区

4.经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。

img

3.JVM怎么回收内存,gc机制是什么?

垃圾收集需要完成的三件事情:

  • 哪些内存需要回收?

    程序计数器,虚拟机栈和本地方法栈都是随线程而生,随线程而灭。 栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。 每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑如何回收的问题, 当方法结束或者线程结束时, 内存自然就跟随着回收了。

    而Java堆和方法区这两个区域则有着很显著的不确定性: 一个接口的多个实现类需要的内存可能会不一样, 一个方法所执行的不同条件分支所需要的内存也可能不一样, 只有处于运行期间, 我们才能知道程序究竟会创建哪些对象, 创建多少个对象, 这部分内存的分配和回收是动态的。 因此,垃圾收集器所关注的正是这部分内存该如何管理。

  • 什么时候回收? 哪些还“存活”着, 哪些已经“死去”了。

    判断对象是否死去通常有两种方法:引用计数算法和可达性分析算法。

    引用计数算法

    引用计数算法:在对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加一; 当引用失效时, 计数器值就减一; 任何时刻计数器为零的对象就是不可能再被使用的。

    优点:

    • 原理简单,判断效率高
    • 实时性,任何内存,一旦没有指向它的引用,就会立即被回收

    缺点:

    • 内存分配和释放次数变多,维护引用计数代价越高(执行效率低)
    • 循环引用不能去使用(关键缺点)

    在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作。

    可达性分析算法

    可达性分析算法:通过一系列称为“GC Roots”的根对象作为起始节点集, 从这些节点开始, 根据引用关系向下搜索, 搜索过程所走过的路径称为“引用链”(Reference Chain) ,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时, 则证明此对象是不可能再被使用的。

    如下图所示, 对象object 5、 object 6、 object 7虽然互有关联, 但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。

    image.png

    在Java技术体系里面, 固定可作为GC Roots的对象包括以下几种:

    1
    2
    3
    4
    5
    6
    7
    在虚拟机栈(栈帧中的本地变量表)中引用的对象, 譬如,各个线程被调用的方法堆栈中使用到的参数、 局部变量、 临时变量等。
    在方法区中类静态属性引用的对象, 譬如,Java类的引用类型静态变量。
    在方法区中常量引用的对象, 譬如,字符串常量池(String Table)里的引用。
    在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
    Java虚拟机内部的引用,如,基本数据类型对应的Class对象, 一些常驻的异常对象(比如NullPointExcepiton、 OutOfMemoryError)等, 还有系统类加载器。
    所有被同步锁(synchronized关键字)持有的对象。
    反映Java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、本地代码缓存等。
  • 如何回收?

    即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段。

    这边就要用到垃圾收集算法

    标记-清除算法

    算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它是最基础的收集算法,因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

    缺点:

    • 标记和清除过程的效率都不高
    • 标记清除之后会产生大量的不连续的内存碎片,分配较大对象无法找到连续内存不得不触发另一次垃圾回收

    标记-清除算法执行过程

    复制算法

    为了解决效率问题,一种称为“复制”的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用一块。当这块内存用完了。就将还活着的对象复制到另一块上面,然后再把已经用过的内存空间一次清理掉。这样使得每次都是对其中一块内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免也太高了一点。

    如图所示,“半区域复制”这样实现的垃圾回收算法缺点显而易见
    (1)内存利用效率太低,只能利用一半的内存
    (2)如果内存中出现对象大都是存活的情况,将会产生大量内存间复制的开销

    复制算法执行过程

    在1989年,Andrew Appel针对具备“朝生夕灭”特点的对象,提出了一种更为优化的半区复制分代策略,现在称为“Appel式回收”。HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局。
      具体做法就是,把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生内存收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已使用过的那一块Survivor空间。HotSpot虚拟机默认的Eden和Survivor的大小默认时8:1,也即每次新生代中可用内存空间为整个新生代容量的90%,只有另一个Survivor空间,即10%的新生代是“浪费”的。如图:
    在这里插入图片描述

    标记-整理算法

    复制算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对内存中被使用的所有的对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
    根据老年代的特点,有人提出了一种“标记-整理“算法,标记过程仍然与”标记-清除“算法一样,但是后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。
    标记-整理算法执行过程

    分代收集算法

    当前商业虚拟机的垃圾回收都采用”分代收集“算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为老年代和新生代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批的对象死去,只有少量存活,那就选用复制算法,只需要付出少量的存活对象的复制成本就可以完成收集。而老年代中因为对象的存活率高、没有额外的空间对他进行分配担保,就必须使用”标记-整理“或者”标记-清理“算法来进行回收。

    (1) 年轻代(Young Gen)

    年轻代特点是区域相对老年代较小,对像存活率低。

    这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

    (2) 老年代(Tenure Gen)

    老年代的特点是区域较大,对像存活率高。

    这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。

    Mark阶段的开销与存活对像的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程利用,对并发、并行的形式提标记效率。

    Sweep阶段的开销与所管理区域的大小形正相关,但Sweep“就地处决”的特点,回收的过程没有对像的移动。使其相对其它有对像移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题。

    Compact阶段的开销与存活对像的数据成开比,如上一条所描述,对于大量对像的移动是很大开销的,做为老年代的第一选择并不合适。

    基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

    总结:没有最好的算法,只有最合适的算法

4.static变量的初始化前后在jvm内存中的位置?

成员变量数据存储在堆内存的对象中,所以也叫对象的特有数据。

静态变量数据存储在方法区(共享数据区)的静态区,所以也叫对象的共享数据。

5.Java方法调用在jvm中是怎样的过程(方法栈、入参、返回值)

方法(Java中称为方法,其他语言一般称为函数)调用主要是通过栈来存储相关的数据,系统就方法调用者和方法如何使用栈做了约定,返回值可以简单认为是通过一个专门的返回值存储器来存储的。

1
2
3
4
5
6
7
8
9
10
11
12
public class Sum{
public static int sum(int a, int b){
int c = a * b;
return c;
}

public static void main(String[] args){
int d = Sum.sum(1, 2);
System.out.println(d);
}
}

当程序在 main 方法调用 Sum.sum 之前,栈的情况大概如图所示。

调用Sum.sum之前的栈示意图

在 main 方法调用 Sum.sum 时,首先将参数 1 和 2 入栈,然后将返回地址(也就是调用方法结束后要执行的指令地址)入栈,
接着跳转到 sum 函数,在 sum 函数内部,需要为局部变量 c 分配一个空间,而参数变量 a 和 b 则直接对应于入栈的数据 1 和 2,在返回之前,返回值保存到了专门的返回值存储器中。

在调用 return 后,程序会跳转到栈中保存的返回地址,即 main 的一条指令地址,而 sum 函数相关的数据会出栈,从而又变回上图中的样子。

main 的下一条指令是根据方法返回值给变量 d 赋值,返回值从专门的返回值存储器中获得。
在Sum.sum内部,准备返回之前的栈示意图

程序执行的基本原理

CPU有一个指令指示器,指向下一条要执行的指令,要么顺序执行,要么进行跳转(条件跳转或无条件跳转)。

具体到Java程序来说就是,程序从 main 方法开始顺序执行,方法调用可以看作一个无条件跳转,跳转到对应方法的指令处开始执行,
碰到 return 语句或者方法结尾的时候,再执行一次无条件跳转, 跳转回调用方,执行调用方法后的下一条指令。

6.如果一个程序频繁触发Full GC,原因可能是什么?

(1)System.gc()方法的调用。此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI(Java远程方法调用)调用System.gc。

(2)旧生代空间不足。旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space 。为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

(3)Permanet Generation空间满了。Permanet Generation中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出错误信息:java.lang.OutOfMemoryError: PermGen space 。为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。

(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

1
2
3
4
5
1.java远程调用System.gc()方法
2.老年代空间不足
3.永久代空间满了(java8被元空间替代,使用本地内存,所以这个现在不太可能发生)
4.假如young gc后,进入老年代的平均大小大于老年代可用内存会触发full gc
5.由Eden区,from区向to区复制时(或者颠倒顺序),对象大于to区内存,则移动到老年代,但是老年代内存小于该对象大小

7.jvm如何知道new了一个对象要多大内存

对象在堆内存中的存储布局可以划分为三个部分: 对象头,实例数据,对齐填充

对象头

对象的对象头包括两类信息。第一类是存储对象自身的运行时数据,第二类是类型指针

Mark World

存储对象自身的运行时数据,如哈希码,GC分代年龄、锁状态标志、线程持有的锁等等。这部分的数据长度在32位虚拟机中为4个字节,在64位虚拟机中是8个字节,官方称之为 Mark Word

类型指针

指向它的类型元数据的指针(指向它的Class对象的指针),大小是4个字节。Java虚拟机通过这个指针来确定该对象是哪个类的实例

实例数据

实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容。计算方式是累加,如下对象:

1
2
3
4
public class O {
private int o1;
private long o2
}

实例数据部分长度就是 int(4字节)+long(8字节)=12字节

对齐填充

第三部分就是对齐填充。HotSpot虚拟机的自动内存管理要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。如果对象大小没到8字节的整数倍,那就需要通过对齐填充来补全。

实战
已经知道了对象的内存布局,我们就可以来尝试计算一个类对象占用的内存:
我们就来计算 String类的内存:
我们先来查看String类里面的实例数据有哪些;

1
2
3
4
5
6
7
8
@Stable
private final byte[] value;
private final byte coder;
private int hash; // Default to 0
private boolean hashIsZero; // Default to false;
private static final long serialVersionUID = -6849794470754667710L;
static final boolean COMPACT_STRINGS;
可以得到实例数据部分的字节是 数组对象 + byte1字节)+ int4字节)+ boolean(1字节) + long8字节) + Boolean(1字节)

那么数组对象也是一个对象,它的占用内存是 对象头(8字节)+ 引用(4字节)+ 记录长度的int(4字节)=16字节。

所以空的String对象占用内存是 8+16+1+4+1+8+1+ 1字节(字节填充)=40字节

非空的String对象

非空的string对象比空的string对象只有在数组对象里的 实例数据部分变化了,其他都没变,
所以非空的String对象占用内存是 8+16+1+4+1+8+1+ 1字节(字节填充)=40+ n字节(n是byte数组的长度)

8.堆,方法区,永久代,元空间 之间的关系

  1. 方法区和永久代的关系

    方法区是 《Java虚拟机规范 》中的规范,在 HotSpot 虚拟机中,JDK 6.0 及以前由永久代实现,就如同 Java 中的接口和实现一样。

    永久代并不是很好的方法区实现,《Java虚拟机规范》中方法区不需要连续的内存20)并且可以选择固定大小或可扩展,而永久代有 -XX:MaxPermSize 上限,即使不设置也有默认大小,很容易造成内存溢出。

  2. 堆和方法区的关系

    《Java虚拟机规范》把方法区描述为堆的逻辑部分,但它却有一个名字 Non-Heap 非堆,两者物理上并无联系。

    两者的共同点就是都可以选择固定大小或可扩展。

  3. 元空间:JDK 8 把永久代迁移到由本地内存实现的元空间当中

  4. 永久代数据迁移

    • 1.6 及以前永久代是方法区的实现,其中包括:已加载的类型信息,常量,静态变量,即时编译器编译过后的代码缓存等。
    • 1.7 将字符串常量池,静态变量移出到堆中。
    • 1.8 将老年代剩余内容(主要是类型信息)全部移到元空间中。
  5. 方法区的实现永久代真的永久吗?

    垃圾回收在这个区域出现的比较少,但是并不是没有,主要是常量池的回收和类型的卸载。主要是动态类的生成:CGLib字节码增强,JSP 等。

  6. 永久代和元空间

    JDK 8 以后,永久代完全退出历史舞台,由元空间替代

  7. 常量池在 JDK 6,7,8 中存在位置证明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import java.util.HashSet;
    import java.util.Set;

    public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
    Set<String> set = new HashSet<>();
    int i = 0;
    while (true){
    // String.intern();去字符串常量池中查找有没有这个字符串,有的话指向引用,没有将字符串拷贝进字符串常量池当中
    set.add(String.valueOf(i++).intern());
    }
    }
    }
    12345678910111213
    1. JDK 6:-XX:MaxPermSize 限制永久代大小,异常 OOM:PermGen space ,代表永久代空间不足,此时字符串常量池在永久代当中。
    2. JDK 7及以上:-XX:MaxPermSize 限制永久代大小,循环一直下去,不会爆异常。-Xmx512M 限制最大堆就可以看到 OOM:heap space堆内存不足,代表字符串常量池在堆中。

    image-20220914163105961

    指令补充

    -Xmssize 设置初始化堆内存大小,这个值的大小必须是1024的倍数,并且大于1M,可以指定单位k(K),m(M),g(G)。例如 -Xms6m。如果没有设置这个值,那么它的初始化大小就是年轻代和老年代的和。等价于-XX:InitialHeapSize
    -Xmxsize 设置最大堆内存大小,这个值的大小必须是1024的倍数,并且大于2M,可以指定单位k(K),m(M),g(G)。默认值是根据运行时的系统配置来确定的。一般服务器部署时,把-Xms-Xmx的值设置成相同的大小。-Xmx选项和-XX:MaxHeapSize相同。
    -Xmnsize 设置年轻代大小。可以指定单位k(K),m(M),g(G) .例如-Xmn256m。还可以通过其他两个选项来代替这个选项来指定年轻代最小和最大内存:-XX:NewSize指定初始化大小,-XX:MaxNewSize指定最大内存大小
    -XX:NewSize=< n >[g|m|k] 年轻代的初始值。
    -XX:MaxNewSize=< n >[g|m|k] 年轻代的最大值。
    -Xsssize 设置线程栈的大小。可以指定单位k(K),m(M),g(G)。默认值根据内存而定。 这个选项和-XX:ThreadStackSize相同
    -XX:ThreadStackSize=size 设置线程栈大小。默认值依赖于机器内存。这个选项和-Xss选项的功能相同。
    -XX:MetaspaceSize=size 设置元数据空间初始大小。
    -XX:MaxMetaspaceSize=size 设置元数据空间最大值。
    -XX:NewRatio 设置老生代和新生代大小的比例,比如-XX:NewRatio=2表示1/3的Heap是新生代,2/3的Heap是老生代。
    -XX:SurvivorRatio 用来设置新生代中Eden和Survivor空间大小的比例,需要注意的是有两个Survivor。比如-XX:SurvivorRatio=8表示Eden区域在新生代的8/10,两个Survivor分别占1/10。

    在这里插入图片描述