JVM 全部学习笔记——很简单!

学完了再来回答

三种JVM

我们用的是Sun公司的HotSpot

  1. JVM的位置
  2. JVM的体系结构
    1. 类加载器
      1. 双亲委派机制
      2. 沙箱安全机制
    2. Native
    3. PC寄存器
    4. 方法区
      1. 三种JVM
      2. 新生区、老年区
      3. 永久区
      4. 堆内存调优
      5. GC
  3. JMM
  4. 总结

一、 JVM的位置

操作系统之上

二、 JVM的体系结构         

JVM主要由3部分构成:类加载器+jvm运行时数据区+执行引擎

        jdk6之前:

                                

        jdk8之后:永久代没了,改成了元空间

                                         

注意:方法区只是一个规范,方法区和永久代、元空间的关系可以理解为接口和实现类的关系。Hotspot在JDK6及之前对方法区的实现是永久代,JDK8改为元空间。把方法区从JVM内存空间搬到了本地内存空间,这样方法区内存大小就不受JVM的限制了,减少了溢出的可能。

JVM中哪部分肯定不存在垃圾回收?

                 

 哪部分存在?堆+方法区。方法区是一个特殊的堆。绝大部分都是在堆Heap中。

JVM中哪部分占得内存最大?堆。因为JVM内存调优就是堆调优,肯定堆占了JVM大部分空间。

2.0 类加载器

Class对象new一个示例 和 实例调用getClass方法还原回Class的关系:

1.1 启动类/根加载器:

根加载器加载的是rt.jar包。

rt.jar包是java的核心jar包。之前学的java的基础(String包、Time包、Math包)都在这里。

路径:jre1.8.0_131\lib\rt.jar

1.2 扩展类加载器

扩展类加载器加载的是jre1.8.0_131\lib\ext路径下的jar包

路径:jre1.8.0_131\lib\ext

1.3 应用程序加载器

为什么根加载器获取不到?

因为根加载器是用C/C++写的,JVM获取不到(我猜:因为C++写的编译出来的不是.class文件,所以无法在JVM里进行类的加载,所以跟加载器类就获取不到)。

双亲委派机制——保证安全性

如果我们自定义了一个java.lang.String包,可以替换java自带的吗?

不可以! 类加载器收到加载类的请求后,会层层向上委托给父类,一直达到根加载器,根加载器负责查找是否已经加载过了。首先,这个String类是我们自定义的,所以交给应用程序类加载器(先不检查),再向上交给扩展类加载器(先不检查),再向上交给根加载器(检查!),根加载器会发现已经加载过自带的java.lang.String包了,所以自己写的会失效。

如果根加载器没加载过,再交给扩展类加载器去检查,扩展类没加载过,再交给应用程序类加载器。

当然,如果你想改原始的String类也不是不可以。把rt.jar包的String类删了换成你自己的。

沙箱安全机制——保证安全性

很老的了,jdk1.0时候的了。了解就行

 

2.1 本地方法栈——native关键字

凡是带着native关键字的,表示java的作用范围达不到了,需要调用底层的C。

                        ↓

一个方法一旦被标为native,他就被登记在本地方法栈(Native Method Stack)

                        ↓

当我们调用native方法时,先去JVM先去本地方法栈中查看是否登记过

                        ↓

然后去调用相应的JNI(java native interface)

                        ↓

JNI去调用本地方法库

JNI的作用:扩展java语言的使用,融合不同的编程语言(C/C++、Python)为java所用

2.2 PC寄存器(程序计数器) 

还用说吗?

2.3 方法区

背过:静态变量(static)、常量(final)、类信息(Class模板 )、运行时的常量池存储在方法区

一个共享,三个不变。静态变量是被所有对象共享,可以改变。剩下的常量、类模板、运行时常量只要被加载进来了就不会变了。

2.4 栈

1. 栈,又叫栈内存,主管程序的运行。

2. 栈不需要垃圾回收。

3. 栈是线程级的,有几个线程就有几个栈(栈不止一个但堆只有一个)。栈和线程同生同死,栈和线程有相同的生命周期,当线程创建时,栈里被压入main()方法,当线程结束时,main出栈。

4. 存储的东西:方法+方法的形参+方法里的局部变量

5. 栈顶是什么?栈顶是正在运行的方法呀~

6. 栈里存放的叫栈帧

问题(一):为什么main方法最先执行,最后退出?

问题(二):如果方法a()调用方法b(),方法b()又调用方法a(),会报什么错误?

StackOverflow栈溢出错误

2.5 堆

1.堆,又叫堆内存

2. 堆只有一个

3. 堆内存的内存大小可以调节(内存调优)

4. 堆存储的是new出来的对象实体

2.5.1 大致划分

大致划分为3部分:

  • 新生区
    • 伊甸园区:对象new完以后都在伊甸园区
    • 幸存区0
    • 幸存区1
      • 在伊甸园发生GC后,没有被回收的幸存者进入幸存区
      • 为什么要有两个幸存区呢?复制算法的时候就有了答案
      • 幸存区一个是from一个是to,谁空谁是to
  • 养老区
  • 永久存储区

        GC垃圾回收,主要发生在堆区,堆区又主要发生在伊甸园区(轻量级GC)和养老区(重量级GC)

        什么样的对象实体会被回收呢?后续不再用了的。调查发现,99%的对象实体只用了一次便不再用了。所以大部分的对象在轻量级GC时就被回收,也就是说大部分活不到养老区(得是那种为程序做了很多贡献的才能养老)

2.5.2 永久区(已无

  1.  常驻内存,开启虚拟机时就有,关闭虚拟机时才释放这部分内存。
  2.  永久区存储的是java运行时环境(jdk自身携带的Class对象,interface元数据),不参与垃圾回收。 
  3. 永久区什么时候会崩呢?(OOM)
    1. 一个启动类,加载了大量的第三方jar包
    2. Tomcat部署了太多的应用
    3. 大量动态生成的反射类不断被加载
  4. 永久区的变动
    1. jdk1.6,使用永久区,
    2. jdk1.7,不想用永久区了,逐渐把一些东西从永久区搬到主机内存中
    3. jdk1.8,取消永久区,存到元空间中,完全使用主机内存

2.5.3 OOM 堆溢出错误

        由于堆是由JVM自动管理的,所以一般不会溢出,如果爆出OOM错误,说明程序非常差劲,JVM也管不了了        

 遇到过OOM吗?发生了OOM怎么处理?

 1. 首先先扩大堆内存,看能否消除这个错误

        2. 不能的话那就是程序的问题。用内存分析工具看一下哪里出了问题。

如何进行内存调优? 

 获取一下JVM的最大内存和初始内存:

-Xms2048m -Xms2048m -XX:+PrintGCDetails

我们先不用内存分析工具,只是通过运行参数“-XX:+PrintGCDetails”观察一下OOM错误

 但是我们无从知晓为何出现OOM,所以就要用内存分析工具

 2.5.4 内存分析工具

先下一个Windows的客户端,然后再IDEA中下一个它的插件

 

 

给程序加一段如下命令

当这个程序出现OOM错误的时候,就会dump出一个文件(内存快照!!)。然后我们在JProfile客户端打开这个文件就能查看程序哪里出现错误了。

调优参数:-XX:+HeapDumpOn+错误名 

这个调优参数是用来保存内存快照的,当发生参数指定的错误时,就会Dump下一个文件,保存此刻的内存快照。

 

 

那可不可以像编译器一样直接定位到哪一行出现问题呢(这是我们程序员最想知道的)? 

 

2.5.5 GC算法

面试问题:如何判断一个对象是否应该被回收?两个方法

(1)引用计数法。给每个对象一个计数器,当引用次数为0时,就可以被回收了。但是这种方法存在两个问题,一个是一个对象一个计数器资源代价太大了,另一个是当发生循环引用时会失效,虽然Object1和Object2在程序里用不到了,但是因为他俩存在互相引用,那么引用次数永远不可能为0,也就无法被回收,所以jvm不采用引用计数法。

(2)可达性分析法。jvm维护了这样一个集合“GC root Set”。引用计数法就是遍历所有的GC root节点,根据引用关系顺着引用链去遍历。那些没有被便利到的就可以被淘汰了。

1. 引用计数法(已淘汰

描述:给每个分配一个计数器,记录着被引用的次数,GC去回收那些次数为0的。

缺点:java一个大项目里有好多好多对象,每个对象一个计数器,代价太大了!

2. 复制算法(用于新生区,所以这就是轻GC

(1)为什么要有两个幸存区?

主要为了消除内存碎片,有了两个就可以来回倒腾(拷贝),消除内存碎片

(2)复制算法倒腾的过程

每一次倒腾完,都要保证to区是空的。

每经过一次复制算法,from和to就交换一次位置

        所以每一次GC清理完成后,伊甸园区是空的,to区也是空的,只有from里有对象的实体。

        当一个对象经历了15次轻GC还没有被回收,就(才有资格)进入养老区。这个15次是默认的,通过如下运行参数可以调整 

-XX:MaxTenuringThreshold=5

// 非常形象,Tenuring有任期的意思,任期满了就可以退休了哈哈

弊端:需要两个幸存区,且有一个一直是空的,所以这块内存是浪费的。

优点:解决内存碎片

想象一个场景,每个对象都100%存活,gc一个也回收不了。那么所有的对象就在from和to之间拷贝过来拷贝过去。所以复制算法最佳使用场景:当对象的存活率较低的地方,也就是我们的新生区。

3. 标记-清除-压缩算法(用于老年代,所以这就是重GC

(1)先看一下标记-清除算法

缺点:产生内存碎片

 (2)优化——压缩

为了清除内存碎片,再扫描一遍。

 (3)优化——多次标记-清除,一次压缩

优点:不用开辟两个幸存区。和复制算法相比省空间

缺点:标记-清除-压缩经历了3次扫描,浪费时间。适合不怎么需要垃圾回收的地方,也就是老年区,当老年区满了才调用一次重GC。

4. 分代收集算法

  • 内存效率(时间复杂度):复制算法 >优于)标记清除算法 > 标记清除压缩算法  
    • 解释:复制需要扫描一趟,标记清除两趟,标记清除压缩三趟
  • 内存整齐度: 复制算法 = 标记清除压缩 > 标记清除
  • 内存利用率: 标记清除= 标记清除压缩 > 复制算法

各有优劣,所以只能因地制宜——>分代收集算法

上面说过,新生代存活率低,所以适合复制算法。老年代存活率高,产生的内存碎片少,适合标记-清除-压缩算法。

2.5.6 总结一下内存调优用到的参数

  1.  -Xms8m -Xms2048m (初始内存  最大内存)

  2. -XX:+PrintGCDetails
  3. -XX:+HeapDumpOnOutOfMemoryError
  4. -XX:MaxTenuringThreshold=5

2.5.7 JVM其他调优

jstack工具:是jvm自带的拍片子工具。主要有两个功能

1)查看每个线程堆栈的使用情况,看哪个线程用的堆内存、栈内存最多

2)查看每个线程使用CPU的情况,分析CPU过高的原因(阿里云面试题)

小林coding给出的官方解答:

查看每个线程使用CPU的情况,分析CPU过高的原因主要分以下5步

  1. top  查看哪个进程使用CPU最多,记录pid
  2. top -Hp pid   用pid指定进程,查看这个进程中的哪个线程使用CPU最多
  3. jstack pid  线程ID还是进程ID?
  4. jstack -l [PID] >/tmp/log.txt
  5. 分析堆栈信息,堆栈信息里会给出多少行出现错误,然后我们去看源码就行

1.在服务器上,我们可以通过top命令查看各个进程的cpu使用情况,它默认是按cpu使用率由高到低排序的

 由上图中,我们可以找出pid为21340的java进程,它占用了最高的cpu资源,凶手就是它,哈哈!

2.通过top -Hp 21340可以查看该进程下,各个线程的cpu使用情况,如下:

 可以发现pid为21350的线程,CPU资源占用最高~,嘻嘻,小本本把它记下来,接下来拿jstack给它拍片子~但是之前呢我们需要得到这个线程PID的十六进制表示,用

printf "%x\n" 21350

命令得到21350的十六进制表示0x5366

3. jstack 21350 | grep 5366 或者直接输入jstack 21350命令(这里应该用线程号还是进程号?不确定哈),然后肉眼去找0x5366这一行,发现21350占用CPU最高的原因在24行!定位成功!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值