1、jvm的内存结构

java 区别 堆 方法区 jvm方法区和堆的区别_加载

方法区和堆是所有线程共享的内存区域,而java栈、本地方法栈和程序计数器是运行时线程私有的内存区域。

1、Java堆(Heap):是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

2、方法区(Method Area):方法区与Java对一样,是所有线程共享的内存区域。

  • 它用于存储已被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码等数据。
  • 有时候会成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载。
  • 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。

3、程序计数器(Program Counter Register):是一块较小的内存空间,线程私有的,它的作用可以看做是当前线程所执行的字节码的信号指示器。就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,就是即将执行的指令代码)。

4、JVM栈(JVM Stacks):与程序计数器一样,是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表,操作栈,动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈到出栈的过程。

局部变量表里存储的是基本数据类型、returnAdress类型(指向一条字节码指令的地址)和对象引用, 这个对象引用有可能是指向对象起始地址的指针,也有可能是代表对象的句柄或者是与对象相关联的位置。局部变量所需的内存空间是在编译器编译期间确定。

5、**本地方法栈(Native Method Stacks)😗*本地方法栈与虚拟机栈的作用非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

2、java堆的结构是什么样子?

1、JVM内存划分为堆内存和非堆内存,堆内存分为年轻代、老年代,非堆内存就一个永久代,jdk1.8之后改为元空间。

2、年轻代又分为Eden区和Survivor区,Survivor区又叫form区,to区,Eden容量比Survivor大,默认比为8:1:1;

3、年轻代minor GC过程如下:GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生区中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年区中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生区中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年区进行分配担保,将这些对象存放在老年区中。

eden(新生区)

当初始加载对象时会进入新生区

survivor(幸存区)

  • 幸存区又分为from 和 to —谁为空谁为to ,始终都会有一个区域为空。
  • 幸存区不会主动进行垃圾回收,只会eden回收时才会附带进行gc
  • 当在幸存区中的阈值达到了15后(默认15可修改,为啥是15,因为gc分代年龄是存储在对象头中,存在4位bIt中,最大值就是1111,所以默认是15)会自动进入老年代
  • 当新生区(eden)出现了内存不足时,会进行YGC,那么会将没有指针的对象回收,还有指针引向的对象放入survivor1或者survivor2区域中,eden清空,数据放入一个survivor中。—当第二次进行gc那么会将eden数据放入另一个空的survivor中,并且将当前survivor中有效数据,放入空的survivor中,以此类推。

老年代

1.较大的对象数据会放入老年代

2.年代的数据都是相对于持久的不会频繁的gc

3.(MajorGC / Old GC) 在进行majorgc时会至少进行一次minorGc ,而且majorgc的效率是比minorGc 慢10倍的
4.老年代收集器:MajorGC / Old GC 要区分与Full GC

总结:

  • 年轻代用于放置临时对象或者生命周期短的对象
  • 老年代用于方式生命周期长的对象
  • 永久代或者元空间,用于存放常量
  • minor GC只管年轻代 , Full GC同时管理年轻代和老年代
  • 老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是2/3。

3、为什么要分代,分代有什么好处?年轻代为什么要分Eden区和Survivor区呢?为什么是两个Survivor区呢?

答:

1、经研究表明,不同对象的生命周期不一致,但是在具体的使用过程中70%-90%的对象是临时对象。

2、分代唯一的理由是优化GC的性能,如果没有分代,那么所有对象在一块空间,GC想要回收扫描他就必须扫描所有的对象,分代之后,长期持有的对象可以挑出,短期持有的对象可以固定在一个位置回收,省掉很大一部分的空间利用。

3、如果年轻代没有Survivor区,gc存留下来的直接放入老年区,那这样老年区很快就会填满,触发Major GC(Full Gc),Full GC频繁会影响程序的执行和响应速度。

4、年轻代中使用的是复制算法最高效,因为大部分都是临时对象,使用标记清除容易产生大量的内存碎片,浪费内存。而复制算法需要两个对等的空间,所有才有两个Survivor区。

4、为什么要废弃永久代?

1、移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。
2、有了元空间就不再会出现永久代OOM问题了!(字符串存在永久代中,容易出现性能问题和内存溢出)

5、你知道哪些垃圾回收算法?

  • 标记-清除算法(Mark-Sweep)
    GC分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。同时会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。

java 区别 堆 方法区 jvm方法区和堆的区别_java_02

  1. 效率问题,标记和清除效率不高。
  2. 空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
  • 复制算法(Copy)
    将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。缺点需要两倍的内存空间。

java 区别 堆 方法区 jvm方法区和堆的区别_面试_03

优点:不用考虑碎片问题,方法简单高效。
缺点:内存浪费严重。

  • 标记-整理(Mark-Compact)
    也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题。
    一般年轻代中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外过多内存空间分配,就需要使用标记-清理或者标记-整理算法来进行回收。

java 区别 堆 方法区 jvm方法区和堆的区别_java_04

6、什么是双亲委派机制?如何破坏呢?

概述:类加载器在加载过程中,先把类交给自身的父类的处理,以此类推,直至启动类加载器,父类加载器没加载到,再交由子类来处理。

java 区别 堆 方法区 jvm方法区和堆的区别_java 区别 堆 方法区_05

目的:1、防止内存中存在多份同样的字节码,保证JDK核心代码优先加载
2、防止一个类被不动类加载器重复加载
如何打破双亲委派机制:

1、自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)

2、线程上下文加载器:由于类加载规则,很可能导致父类加载器加载时依赖子类加载器,导致无法加载成功(BootStrap ClassLoader无法加载第三方的库的类),所以存在【线程上下文加载器】加载。

案例:1、tomcat加载多个web应用程序。

假设我现在有两个Web应用程序,它们都有一个类,叫做User,并且它们的类全限定名都一样,比如都是com.yyy.User。但是他们的具体实现是不一样的。那么Tomcat是如何保证它们是不会冲突的呢?

答案就是,Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找。那这样就做到了Web应用层级的隔离。

2、jdbc加载

类加载有个规则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。

我们用JDBC的时候,是使用DriverManager进而获取Connection,DriverManager在java.sql包下,显然是由BootStrap类加载器进行装载。当我们使用DriverManager.getConnection()时,得到的一定是厂商实现的类。DriverManager的解决方案就是,在DriverManager初始化的时候,得到「线程上下文加载器」去获取Connection的时候,是使用「线程上下文加载器」去加载Connection的,而这里的线程上下文加载器实际上还是App ClassLoader。

7、如果我自定义类加载器和核心类重名了,或者我编写了java核心目录下的class文件,怎么加载?

这时候沙箱安全机制就会保护核心类不会被修改,不会被加载。

什么是沙箱?
 Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

8、类的生命周期

类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下图;

java 区别 堆 方法区 jvm方法区和堆的区别_加载_06

1、加载,查找并加载类的二进制数据,在Java堆中创建一个java.lang.Class类的对象。

2、连接,连接又包括三块内容:

  • 验证,文件格式、元数据、字节码、符号引用验证。
  • 准备,为类的静态变量分配内存,并将其初始化为默认值。
  • 解析,把类中的符号引用转换为直接引用

3、初始化,为类的静态变量赋予正确的初始值。

4、使用,new出对象在程序中使用

5、卸载,执行垃圾回收

9、如何判断对象是否为垃圾回收对象?

1、引用计数法

概述:在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,计数器的值就-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了

缺点:但如果两个对象存在互相引用的话,那将一直不会回收

java 区别 堆 方法区 jvm方法区和堆的区别_加载_07

2、可达性分析

概述:基本思路是通过一系列称为GC Roots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链。如果某个对象到GC Roots没有任何引用链相连,则会被标记为垃圾。

在java中,可以作为GC Roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态变量属性引用对象
  • 方法区中常量引用的对象
  • 本地方法栈中(Native方法)引用对象

图片中红色箭头指向的都是没有GC Root引用链的,均被判定为垃圾

java 区别 堆 方法区 jvm方法区和堆的区别_面试_08

GC Root
虚拟机栈中的引用的对象
在程序中创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的;

全局的静态的对象
也就是使用了 static 关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为 GC Roots 是必须的;

常量引用
就是使用了 static final 关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为 GC Roots;

Native 方法引用对象
这一种是在使用 JNI 技术时,有时候单纯的 Java 代码并不能满足我们的需求,我们可能需要在 Java 中调用 C 或 C++ 的代码,因此会使用 native 方法,JVM 内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为 GC Roots。

10、Java的四种引用,强软弱虚,你都知道吗?

1、强引用
强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:

String str = new String("str");

2、软引用
软引用在程序内存不足时,会被回收,使用方式:

// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
// 这里的软引用指的是指向new String(“str”)的引用,也就是SoftReference类中T

SoftReference<String> wrf = new SoftReference<String>(new String("str"));

可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
3、弱引用
弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:

WeakReference<String> wrf = new WeakReference<String>(str);

可用场景: Java源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。

4、虚引用
虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。注意哦,其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue ,使用例子:

PhantomReference<String> prf = new PhantomReference<String>(new String("str"),
new ReferenceQueue<>());

可用场景: 对象销毁前的一些操作,比如说资源释放等。 Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效。

11、基本的JVM性能调优

  • 设定堆内存大小
    -Xmx:堆内存最大限制。
  • 设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
    -XX:NewSize:新生代大小
    -XX:NewRatio 新生代和老生代占比
    -XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
  • 设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC

想了解更多的JVM参数,看这里

12、OOM你遇到过哪些情况,SOF呢?

OOM:
1、OutOfMemoryError异常
除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。
Java Heap 溢出:
一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess
2、虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常这里需要注意当栈的大小越大可分配的线程数就越少。
3、运行时常量池溢出
异常信息:java.lang.OutOfMemoryError:PermGenspace
如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
4、方法区溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。异常信息:java.lang.OutOfMemoryError:PermGenspace方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。
SOF(堆栈溢出StackOverflow):
StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。

13、你知道哪几个垃圾收集器

本节主要讲解 7 种垃圾回收器,其中有 3 种垃圾回收器是作用于年轻代垃圾回收的收集器;另外 3 种圾回收器是作用于老年代垃圾回收的收集器;剩余的 1 种垃圾回收器能够同时作用于年轻代和老年代。

java 区别 堆 方法区 jvm方法区和堆的区别_java 区别 堆 方法区_09

1、Serial收集器

基本概念:Serial收集器是最基本、发展历史最久的收集器,这个收集器是采用复制算法单线程收集器。

单线程的收集器:单线程一方面意味着他只会使用一个 CPU 或者一条线程去完成垃圾收集工作,另一方面也意味着他进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。

不过实际上到目前为止,Serial收集器依然是虚拟机运行在 Client 模式下的默认新生代收集器,因为它简单而高效。Serial 收集器运行过程如下图所示:

java 区别 堆 方法区 jvm方法区和堆的区别_jvm_10

2、Parnew收集器

基本概念:Parnew 收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和 Serial 收集器完全一样,但是他却是 Server 模式下的虚拟机首选的新生代收集器。
Tips:从概念上来看,我们需要注意Parnew收集器的两个特点:一个是采用复制算法,另外一个是多线程收集。

特点:除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。CMS 收集器第一次实现了让垃圾收集器与用户线程基本上同时工作;

Parnew 收集器默认开启的收集线程数与 CPU 数量相同,在 CPU 数量非常多的情况下,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。
  Parnew 收集器运行过程如图所示:

java 区别 堆 方法区 jvm方法区和堆的区别_jvm_11

3、Parallel Scavenge收集器

基本概念:Parallel Scavenge 收集器也是一个新生代收集器,也采用了复制算法,也是并行的多线程收集器。Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量。Parallel Scavenge 收集器是虚拟机运行在 Server 模式下的默认垃圾收集器。被称为“吞吐量优先收集器”。
Tips:从概念上来看,我们需要注意Parallel Scavenge收集器的三个个特点:一个是采用复制算法,一个是多线程收集,一个是达到控制吞吐量的目标。

Parallel Scavenge 收集器运行过程同 Parnew 收集器一样:

java 区别 堆 方法区 jvm方法区和堆的区别_java 区别 堆 方法区_12

**控制吞吐量:**CMS 等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是 CPU 用于运行用户代码时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
**吞吐量参数介绍:**虚拟机提供了-XX:MaxGCPauseMills 和 -XX:GCTimeRatio 两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。不过不要以为前者越小越好,GC 停顿时间的缩短是以牺牲吞吐量和新生代空间换取的。由于与吞吐量关系密切,Parallel Scavenge 收集器也被称为“吞吐量优先收集器”。
  Parallel Scavenge 收集器有一个参数 -XX:UseAdaptiveSizePolicy 参数,这是一个开关参数,这个参数打开之后,就不需要手动指定新生代大小、Eden 区和 Survivor 参数等细节参数了,虚拟机会根据当前系统的运行情况以及性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
  如果对于垃圾收集器运作原理不太了解,以至于在优化比较困难的时候,可以使用 Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成。

4、Serial Old收集器
  基本概念: Serial Old 收集器同样是一个单线程收集器,作用于老年代,使用“标记-整理算法”,这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。
  Serial Old 收集器运行过程如图所示:

java 区别 堆 方法区 jvm方法区和堆的区别_jvm_13

5、 Parallel Old收集器
  基本概念: Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理算法”进行垃圾回收。
  这个收集器在 JDK 1.6 之后的出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge收集器+Parallel Old收集器 的组合。
  Parallel Scavenge 收集器+Parallel Old 收集器 的组合运行过程如下图所示:

java 区别 堆 方法区 jvm方法区和堆的区别_加载_14

6、 CMS收集器
  基本概念:CMS(Conrrurent Mark Sweep,连续标记扫描)收集器是以获取最短回收停顿时间为目标的收集器。使用标记-清除算法。
收集步骤

  • 初始标记:标记 GCRoots 能直接关联到的对象,时间很短;
  • 并发标记:进行 GCRoots Tracing(可达性分析)过程,时间很长;
  • 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长;
  • 并发清除:回收内存空间,时间很长。其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。
      CMS 收集器运行过程如下图所示:

java 区别 堆 方法区 jvm方法区和堆的区别_jvm_15

7、 G1收集器
  基本概念:G1 是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉 JDK1.5 中发布的 CMS 收集器。
  特点:
  a.并发和并行:使用多个 CPU 来缩短 Stop The World 停顿时间,与用户线程并发执行;
  b.分代收集:独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次 GC 的旧对象,以获取更好的收集效果;
  c.空间整合:基于标记-整理算法,无内存碎片产生;
  d.可预测的停顿:能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒。
  在G1之前的垃圾收集器,收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分(可以不连续)Region 的集合。