源码到类文件
编译过程
Test.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器
-> 注解抽象语法树 -> 字节码生成器 -> Test.class文件
.class字节码文件内容
魔数与class文件版本
常量池
访问标志
类索引、父类索引、接口索引
字段表集合
方法表集合
属性表集合
类加载器
类经过javac编译后,生成.class文件保存下来,然后经过类加载器加载类至内存,生成java.lang.Class类的实例,这个实例就是程序访问这个类的入口,通过这个class实例的newInstance方法即可得到这个类的实例对象
种类
启动类加载器(Bootstrap ClassLoader)
又称为引导类加载器,由C++编写,无法通过程序得到。主要负责加载JAVA中的一些核心类库,主要是位于<JAVA_HOME>/lib/rt.jar中
拓展类加载器(Extension ClassLoader)
主要加载JAVA中的一些拓展类,位于<JAVA_HOME>/lib/ext中,是启动类加载器 的子类。
应用类加载器(System ClassLoader)
又称为系统类加载器,主要用于加载CLASSPATH路径下我们自己写的类,是拓展类 加载器的子类
自定义加载器(Custom ClassLoader)
实例
当我们自己编写一个类,名为Test,经过编译后会得到Test.class文件,然后经过类加载器得到Class实例,例如通过Class.forName(“com.***.Test”),通过全路径加载进来。
- 用Test.class.getClassLoader()得到它的类加载器,得到的是AppClassLoader(即系统类加载器)
- 如果用Test.class.getClassLoader().getParent()得到的是它的父加载器ExtClassLoader(即拓展类加载器)
- 用Test.class.getClassLoader().getParent().getParent()得到将会是Null,因为启动类加载器是用C++写的,我们无法通过程序直接得到
常见问题
- Object类是由哪个类加载器加载的?
BootStrap ClassLoader - 我们自己写的类是由哪个类加载器加载的?
System ClassLoader - 类加载器都是我们Java中的一个类ClassLoader的子类吗?
BootStrap ClassLoader不是的,另外两个是的。
三大特性
委托性
每个类中都有一个自己的类加载器的属性,这也就是为什么可以通过Test.class.getClassLoader()来 获取自己的类加载器。当一个类加载器要加载一个类时,它会先委托自己的父类加载器来加载,只有当父加载器无法加载类时,才会自己去加载。例如我们写了一个类Student,它的类加载器是System ClassLoader,它首先会委托给它的父加载器即Extension ClassLoader,然后Extension ClassLoader又会委托给它的父加载器BootStrap ClassLoader,启动类加载器无法加载这个类,交给拓展类加载器,拓展类加载器也无法加载,然后才轮到系统类加载器进行加载。
可见性
可见性指的是父加载器无法利用子加载器加载的类,而子加载器可以利用父加载器加载的类。
单一性
一个类只会被一个类加载器加载一次,不会被重复加载。
双亲委派机制
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类
-
首先判断了该类是否已加载.
-
若没加载,则传给双亲加载器去加载,
-
若双亲加载器没能成功加载它,则自己用findClass()去加载.所以是个向上递归的过程.
-
自定义加载器时,需要重写findClass方法,因为是空的,没有任何内容:
源码
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// -----??-----
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
作用
-
保证安全性
防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。 -
保证唯一性
保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,但是在这种机制下这些系统的类已经被Bootstrap classLoader加载过了,所以并不会再去加载,从一定程度上防止了危险代码的植入
破坏
可以继承ClassLoader类,然后重写其中的loadClass方法,其他方式大家可以自己了解
拓展一下
沙箱安全机制
//举例:定义一个String类
package java.lang;
public class String{
public static void main(String[] args){
System.out.println("Im String");
}
}
效果:
保护Java核心源代码这种机制就称之为沙箱安全机制
JVM表示两个完全相同的类
- 类的全限定类名必须完全一致
- 加载这个类的ClassLoader必须完全一致(我们新建的String引导类加载器不会加载)
JVM内存模型图
运行时数据区域
程序计数器(The PC Register)
作用
当前线程所执行的字节码的行号指示器。
分支、循环、跳转、异常处理、线程恢复等基础功能都依赖该指示器的记录完成
特点
- 线程私有,每个线程都需要独立的程序计数器
- 如果当前线程正在执行java方法,计数器记录正在执行的虚拟机字节码指令的地址
- 如果正在执行native方法,计数器值为空
- 占用内存空间少,没有规定内存溢出
虚拟机栈(Java Virtual Machine Stacks)
- 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
- 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。
- 调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出
局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
操作数栈
操作数栈(Operand Stack)也常称为操作栈
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写人和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)
方法出口
当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且 这个异常没有在方法体内得到处理
方法区(Method Area)
- 方法区是线程共享的内存区域,在虚拟机启动时创建
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来
- 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
说明
(1)方法区在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space
(2)Run-Time Constant Pool
堆(Heap)
- Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享
- Java对象实例以及数组都在堆上分配
Native Method Stacks(本地方法栈)
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行
内存模型
Java对象内存分布
虚拟机内存模型
- 一块是非堆区,一块是堆区。
- 堆区分为两大块,一个是Old区,一个是Young区。
- Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区。 Eden:S0:S1=8:1:1
- S0和S1一样大,也可以叫From和To
- 新创建的对象在Eden区
Survivors区机制
Survivor区分为两块S0和S1,也可以叫做From和To。
在同一个时间点上,S0和S1只能有一个区有数据,另外一个是空的(GC机制)
Old区机制
一般Old区都是年龄比较大的对象,或者相对超过了某个阈值的对象。
在Old区也会有GC的操作,Old区的GC我们称作为Major GC。
当Old区满时,会出现OutOfMemory异常(内存溢出)
常见问题
- 如何理解Minor/Major/Full GC?
Minor GC:新生代
Major GC:老年代
Full GC:新生代+老年代 - 为什么需要Survivor区?只有Eden不行吗?
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代 - 为什么需要两个Survivor区?
最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设
现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循
环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的
存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
永远有一个Survivor space是空的,另一个非空的Survivor space无碎片 - 新生代中Eden:S1:S2为什么是8:1:1?
新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1区为8:1
即新生代中Eden:S1:S2 = 8:1:1
垃圾回收(Garbage Collect)
垃圾对象的确定
引用计数法
对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任
何指针对其引用,它就是垃圾
弊端:若A与B存在相互引用,则导致永远无法回收
可达性分析
通过GC Root的对象,开始向下寻找,看某个对象是否可达
垃圾收集算法
已经能够确定一个对象为垃圾之后,接下来要考虑的就是回收,怎么回收呢?
得要有对应的算法,下面聊聊常见的垃圾回收算法
标记-清除(Mark-Sweep)
- 标记:找出内存中需要回收的对象,并且把它们标记出来,此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时
- 清楚:清除掉被标记需要回收的对象,释放出对应的内存空间
缺点:
3. 标记和清除两个过程都比较耗时,效率不高
4. 会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无 法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
复制算法(Copying)
将内存划分为两块相等的区域,每次只使用其中一块,当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉
缺点:空间利用率低
标记-整理算法(Mark-Compact)
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活
的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)
垃圾回收器
…
JVM参数
常用命令
jps
查看java进程
jinfo
实时查看和调整JVM配置参数
- 查看
查看某个java进程的name属性的值
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID
- 修改
参数只有被标记为manageable的flags可以被实时修改
jinfo -flag [+|-] PID
jinfo -flag = PID - 查看曾经赋过值的一些参数
jinfo -flags PID
jstat
(1)查看虚拟机性能统计信息
(2)查看类装载信息
jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10 次
(3)查看垃圾收集信息
jstat -gc PID 1000 10
jstack
(1)查看线程堆栈信息
(2)用法
jstack PID
jmap
(1)生成堆转储快照
(2)打印出堆内存相关信息
-XX:+PrintFlagsFinal -Xms300M -Xmx300M jmap -heap PID
(3)dump出堆内存相关信息
jmap -dump:format=b,file=heap.hprof PID
jmap -dump:format=b,file=heap.hprof 44808
(4)要是在发生堆内存溢出的时候,能自动dump出该文件就好了
一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
设置堆内存大小: -Xms20M -Xmx20M 启动,然后访问localhost:9090/heap,使得堆内存溢出
(5)关于dump下来的文件,一般dump下来的文件可以结合工具来分析
常用工具
jconsole
JConsole工具是JDK自带的可视化监控工具。查看java应用程序的运行概况、监控堆信息、永久区使用
情况、类加载情况等
命令行中输入:jconsole
jvisualvm
-
监控本地Java进程
可以监控本地的java进程的CPU,类,线程等 -
监控远端Java进程
(1)在visualvm中选中“远程”,右击“添加”
(2)主机名上写服务器的ip地址,比如31.100.39.63,然后点击“确定”
(3)右击该主机“31.100.39.63”,添加“JMX”[也就是通过JMX技术具体监控远端服务器哪个Java进程]
(4)要想让服务器上的tomcat被连接,需要改一下 bin/catalina.sh 这个文件JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote - Djava.rmi.server.hostname=31.100.39.63 -Dcom.sun.management.jmxremote.port=8998 -Dcom.sun.management.jmxremote.ssl=false - Dcom.sun.management.jmxremote.authenticate=true - Dcom.sun.management.jmxremote.access.file=…/conf/jmxremote.access - Dcom.sun.management.jmxremote.password.file=…/conf/jmxremote.password" g
(5)在 …/conf 文件中添加两个文件jmxremote.access和jmxremote.password
jmxremote.access 文件guest readonly
manager readwritejmxremote.password 文件
guest guest
manager manager授予权限 : chmod 600 jmxremot
(6)将连接服务器地址改为公网ip地址hostname -i 查看输出情况
172.26.225.240 172.17.0.1
vim /etc/hosts
172.26.255.240 31.100.39.63(7)设置上述端口对应的阿里云安全策略和防火墙策略
(8)启动tomcat,来到bin目录
(9)查看tomcat启动日志以及端口监听tail -f …/logs/catalina.out
lsof -i tcp:8080(10)查看8998监听情况,可以发现多开了几个端口
lsof -i:8998 得到PID
netstat -antup | grep PID(11)在刚才的JMX中输入8998端口,并且输入用户名和密码则登录成功
端口:8998
用户名:manager
密码:manager
Arthas
github :https://github.com/alibaba/arthas
Arthas 是Alibaba开源的Java诊断工具,采用命令行交互模式,是排查jvm相关问题的利器。
下载安装:
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
然后可以选择一个Java进程
Print usage
java -jar arthas-boot.jar -h
调优
垃圾收集器选择
是否选用G1垃圾收集器的判断依据
(1)50%以上的堆被存活对象占用
(2)对象分配和晋升的速度变化非常大
(3)垃圾回收时间比较长
JVM性能优化
常见问题思考
内存泄漏与内存溢出的区别
内存泄漏:对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存溢出:内存泄漏到一定的程度就会导致内存溢出,但是内存溢出也有可能是大对象导致的。
young gc会有stw吗?
不管什么 GC,都会有 stop-the-world,只是发生时间的长短。
major gc和full gc的区别
major gc指的是老年代的gc,而full gc等于young+old+metaspace的gc。 (4)G1与CMS的区别是什么
CMS 用于老年代的回收,而 G1 用于新生代和老年代的回收。
G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生。
什么是直接内存
直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考
虑,读写频繁的场合可能会考虑使用直接内存。
不可达的对象一定要被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
方法区中的无用类回收
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满
足下面 3 个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然
被回收。
不同的引用
JDK1.2以后,Java对引用进行了扩充:强引用、软引用、弱引用和虚引用