面试总结8——Java虚拟机

 平台无关性

Java 源文件----> 编译器-----> 字节码文件

字节码文件-----> JVM(解释器)-----> 机器码

每一种平台的解释器是不同的,但是实现的虚拟机是相同的

当一个程序从开始运行,这时虚拟机就开始实例化了,

多个程序启动就会存在多个虚拟机实例。

多个虚拟机实例之间数据不 能共享。

Java内存区域

运行时数据区域

    1. 程序计数器:行号指示器,用于保证线程切换
    2. Java虚拟机栈:java方法执行的内存模型
    3. 本地方法栈:native方法对应的,类似虚拟机栈
    4. Java堆:存放对象实例,
    5. 方法区:已被虚拟机加载的类信息、常量、静态变量。
    6. 运行时常量池:方法区的一部分,存放编译器生成的各种字面量和符号引用。
    7. 直接内存

 

对象的创建

加载并初始化类和创建对象

 

创建对象

1、在堆区分配对象需要的内存:分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量

2、对所有实例变量赋默认值:将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值

3、执行实例初始化代码:初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法

4、如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它

对象分配内存

1,指针碰撞

2,空闲列表

对象的内存布局:

  1. 对象头
  2. 实例数据
  3. 对齐填充

对象的访问定位

  1. 句柄
  2. 直接指针访问

OOM异常

解决方法:
修改JVM启动参数,直接增加内存。

检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误;

对代码进行走查和分析,找出可能发生内存溢出的位置。

  1. java堆:创建的对象太多;检查程序,看是否有死循环或不必要地重复创建大量对象;增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小;
  2. java虚拟机栈和本地方法栈
  3. 方法区和运行时常量池:类定义过多,常量过多;增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小;
  4.  程序计数器是唯一不会发生OOM异常的区域

内存区域控制参数及对应溢出异常

辅助参数说明

-XX:+HeapDumpOnOutOfMemoryError 打印堆内存异常时打印出快照信息

-XX:+HeapDumpPath 快照输出路径

-Xmn指定eden区的大小

-XX:SurvirorRation来调整幸存区的大小

-XX:PretenureSizeThreshold设置进入老年代的阀值

 

堆内存参数

-Xms:堆最小值

-Xmx:堆最大值

最小值等于最大值时,堆内存不可扩展。

栈内存参数

-Xss:

方法区参数

-XX:PermSize方法区内存最小值

-XX:MaxPermSize方法区内存最大值

本机直接内存参数

-XX:MaxDirectMemorySize

垃圾回收

需要回收的区域:java堆,方法区

如何判断对象已死

  1. 引用计数:
  2. 可达性分析:

GC Roots包括那些对象

虚拟机栈(栈桢中的本地变量表)中的引用的对象,本地方法栈中JNI的引用的对象

,方法区中的类静态属性引用的对象,方法区中的常量引用的对象

引用类型

  1. 强引用
  2. 软引用
  3. 弱引用
  4. 虚引用:它不能单独使用,必须和引用队列联合使用。虚 引用的主要作用是跟踪对象被垃圾回收的状态。

垃圾回收过程中的两次标记

 

回收方法区:性价比低

  1. 回收废弃常量
  2. 回收无用的类

垃圾回收算法

  1. 标记-清除
  2. 复制
  3. 标记-整理

垃圾回收器

新生代垃圾回收:

  1. Serial:单线程,复制算法,可以获得最高的单线程垃圾收集效率。
  2. ParNew:serial的多线程版本,复制算法
  3. Parallel Scavenge:达到可控制的吞吐量(高吞吐量为目标,即减少垃圾收集时间,就是每次垃圾收集时间短,但是收集次数多),多线程,复制算法。

老年代垃圾回收

  1. Serial old:单线程,标记-整理
  2. Parallel old
  3. CMS
  4. G1:可控制的停顿时间

 

CMS和G1的区别:

  1. CMS是并发标记清除。以获取最短回收停顿时间为目标。只能回收老年代;产生内存碎片;无法处理浮动垃圾;
  2. G1是并发标记整理。除了追求低停顿外,还能建立可预测的停顿时间模型。不分新生代和老年代,分region;
  3. CMS的fullGC原因:在年轻代晋升的时候老年代没有足够的连续空间容纳;在并发过程中jvm觉得在并发过程结束前堆就会满了,需要提前触发Full GC。
  4. G1的初衷就是要避免Full GC的出现,fullGC的原因:region复制整理的时候空闲的region了。

什么时候触发MinorGC?什么时候触发FullGC?

触发MinorGC(Young GC)

    虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总空间

    1、如果大于的话,直接执行minorGC

    2、如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC

    3、如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历次晋升的大小,如果小于直接执行FullGC

4、如果大于的话,执行minorGC

触发FullGC

老年代空间不足

YGC出现promotion failure

统计YGC发生时晋升到老年代的平均总大小大于老年代的空闲空间

显示调用System.gc

新生代

是用来存放新生的对象。

一般占据堆的 1/3 空间。

由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。

新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

MinorGC采用复制算法

老年代

主要存放应用程序中生命周期长的内存对象。

老年代的对象比较稳定,所以 MajorGC 不会频繁执行。

在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。

当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC采用标记清除算法

永久代

指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被 放入永久区域,

它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。

所以这 也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。

Eden: Survivor: Survivor=8:1:1

IBM公司的专项研究表明,新生代中的对象98%都是“朝生夕死”的(即:将被回收的对象:存活的对象 > 9:1),所以如果根据复制算法完成按照1:1的比例划分新生代的内存空间,将会造成相当大的浪费。

这样即使所有的对象都不会存活,那么也只会“浪费”10%的内存空间。不过我们也无法保证存活的对象一定<2%或10%,当新生代中Survivor to区内存不够用时,就会触发老年代的担保机制进行分配担保。

为什么要有survivor区

1) 如果没有survivor

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。

老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度。

2)解决

增加老年代空间:降低full gc频率,一旦full gc发生,执行需要的时间更长。

减少老年代空间:full gc所需要时间减少,full gc频率增加。

3) survivor存在的意义

减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么设置两个survivor

解决了碎片化。

1,如果只有一个survivor

新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。下一次Eden满了的时候,此时在Eden和Survivor都进行Minor GC, survivor垃圾回收之后剩下的空间是不连续的,也就导致了内存碎片化。

碎片化带来的风险是极大的,严重影响JAVA程序的性能。

2,两块servivor

刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor1,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S1中的存活对象又会被复制送入第二块survivor2,S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。

上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。

3,设置多个survivor

如果Survivor区再细分下去,每一块的空间就会比较小,很容易导致Survivor区满,因此,我认为两块Survivor区是经过权衡之后的最佳方案。

内存分配和回收策略(调优方法)

  1. 优先
  2. 大对象(通过XX:PretenureSizeThreshold设置)
  3. 长期存活
  4. 存活时间
  5. 空间分配担保

 

获取GC信息的方法

-verbose:gc或者-XX:+PrintGC  获取gc信息

-XX:+PrintGCDetails  获取更加详细的gc信息

-XX:+PrintGCTimeStamps  获取GC的频率和间隔

-XX:+PrintHeapAtGC  获取堆的使用情况

-Xloggc:D:\gc.log  指定日志情况的保存路径

JVM调优

在tomcat的bin/catalina.bat文件的开头添加相关的配置。

Jstatd:jvm监控服务,提供应用程序信息

Jps:列出所有jvm实例;

Jconsole:观察java进程信息

Jstack:所有线程中断信息和状态

Jinfo:运行环境参数

Jmap:jvm物理内存信息

Jstat:类加载,编译器,gc信息

Jps

列举正在运行的虚拟机进程并显示主类及这些进程的唯一ID

Jps [option] [hostid]

jps -q 只输出LVMID

jps -m 输出JVM启动时传给主类的方法

jps -l 输出主类的全名,如果是Jar则输出jar的路径

jps -v 输出JVM的启动参数

Jstat

jstat主要用于监控虚拟机的各种运行状态信息,如类的装载、内存、垃圾回收、JIT编译器等,在没有GUI的服务器上,这款工具是首选的一款监控工具。

Jinfo

jinfo的作用是实时查看虚拟机的各项参数信息

Jmap

用于生成堆快照(heapdump)

Jstack

Jstack用于JVM当前时刻的线程快照,又称threaddump文件,它是JVM当前每一条线程正在执行的堆栈信息的集合。生成线程快照的主要目的是为了定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部时长过长导致线程停顿的原因。

Jconsole

在JDK的bin目录下,监控内存,thread,堆栈等

Jprofile

类似于jconsole,比jconsole监控信息更全面,内存,线程,包,cup 类,堆栈,等等

类加载机制

类加载定义

Class文件加载,

类加载的过程

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化
  6. 使用
  7. 卸载

立即对类“初始化”的情况

New,静态变量,静态方法

反射调用

父类初始化

主类

类的加载器(安全性)

类加载阶段中通过类的全限定名来获取此类的二进制字节流,这一过程放在jvm外部实现,实现这个代码的动作模块成为类加载器。

类加载器就是负责检索并加载其他Java类或者资源(如文件)的对象,它一般继承于java.lang.ClassLoader这个抽象类(除了BootstrapClassLoader)。

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载 器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载 器最终得到的都是同样一个 Object 对象。

类加载器的分类

  1. 启动类加载器(bootstrapClassLoader):作为Java虚拟机的一部分,它使用C++语言实现,因此不继承自ClassLoader加载器,其他的类加载都必须继承自ClassLoader加载器。在程序刚启动时就被加载进来,负责Java标准库的加载,并且只有它能完成该任务。
  2. 扩展类加载器(ExtensionClassLoader):负责加载Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库
  3. 应用进程类加载器(Application):负责加载java扩展库(例如sun公司专门为连接数据库设计的JDBC的一组API)。类加载器负责加载系统类路径(CLASSPATH)中指定的类库。同时它常被称为系统(System)加载器,因为我们可以通过getSystemClassLoader()方法来获取它。
  4. 自定义类加载器(User):由程序员自己编写的类加载器被称为自定义类加载器,如果生成自定义类加载器时没有明确地指出父类加载器,会默认把应用程序(Application)类加载器作为自己的父亲。

组合关系,并非继承关系

线程上下文类加载器

  1. 定义:线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
  2. 应用场景:Java提供了很多服务提供者接口(SPI,Service Provider Interface),允许独立厂商(第三方)为此提供实现。常见的SPI有:JNDI、JDBC、JAXP等。这些接口由Java的核心库来提供,所以问题就在于,SPI的接口是Java核心库的一部分,它们是由启动类加载器来加载的。SPI实现的Java类一般是由应用程序类加载器(Application ClassLoader)来加载的。启动类无法找到SPI的实现类,因为它只加载核心库(SPI的实现类由第三方提供)。它也不能代理给应用程序类加载器,因为它又是应用程序类加载器的父类,双亲委派模型又会将它交给启动类来加载。所以在这个时候我们就要“打破”这个“双亲委派模型”。
  3. 使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。

双亲委派模型

如何破

  1. 自定义一个类加载器,重写classLoader的loadClass方法。
  2. 基础类都是右上层加载器加载的,当基础类又要调用用户的代码时,线程上下文类加载器,父类加载器请求子类加载器去完成类加载动作。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值