标签 Java 下的文章 - 第 30 页 - 酷游博客
首页
关于
友链
Search
1
阿里的简历多久可以投递一次?次数多了有没有影响?可以同时进行吗?
45 阅读
2
Java中泛型的理解
41 阅读
3
Java 14 发布了,再也不怕 NullPointerException 了!
38 阅读
4
Java中的可变参数
37 阅读
5
该如何创建字符串,使用" "还是构造函数?
30 阅读
技术
登录
/
注册
找到
191
篇与
Java
相关的结果
- 第 30 页
2025-01-22
深入探索 Java 热部署
在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作。对于某些大型的应用来说,每次的重启都需要花费大量的时间成本。虽然 osgi 架构的出现,让模块重启成为可能,但是如果模块之间有调用关系的话,这样的操作依然会让应用出现短暂的功能性休克。本文将探索如何在不破坏 Java 虚拟机现有行为的前提下,实现某个单一类的热部署,让系统无需重启就完成某个类的更新。 类加载的探索 首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。 另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。本文将具体探索如何实现这个方案。首先需要了解一下 Java 虚拟机现有的加载机制。目前的加载机制,称为双亲委派,系统在使用一个 classloader 来加载类时,会先询问当前 classloader 的父类是否有能力加载,如果父类无法实现加载操作,才会将任务下放到该 classloader 来加载。这种自上而下的加载方式的好处是,让每个 classloader 执行自己的加载任务,不会重复加载类。但是这种方式却使加载顺序非常难改变,让自定义 classloader 抢先加载需要监听改变的类成为了一个难题。 不过我们可以换一个思路,虽然无法抢先加载该类,但是仍然可以用自定义 classloader 创建一个功能相同的类,让每次实例化的对象都指向这个新的类。当这个类的 class 文件发生改变的时候,再次创建一个更新的类,之后如果系统再次发出实例化请求,创建的对象讲指向这个全新的类。 下面来简单列举一下需要做的工作。 创建自定义的 classloader,加载需要监听改变的类,在 class 文件发生改变的时候,重新加载该类。 改变创建对象的行为,使他们在创建时使用自定义 classloader 加载的 class。 自定义加载器的实现 自定义加载器仍然需要执行类加载的功能。这里却存在一个问题,同一个类加载器无法同时加载两个相同名称的类,由于不论类的结构如何发生变化,生成的类名不会变,而 classloader 只能在虚拟机停止前销毁已经加载的类,这样 classloader 就无法加载更新后的类了。这里有一个小技巧,让每次加载的类都保存成一个带有版本信息的 class,比如加载 Test.class 时,保存在内存中的类是 Test_v1.class,当类发生改变时,重新加载的类名是 Test_v2.class。但是真正执行加载 class 文件创建 class 的 defineClass 方法是一个 native 的方法,修改起来又变得很困难。所以面前还剩一条路,那就是直接修改编译生成的 class 文件。 利用 ASM 修改 class 文件 可以修改字节码的框架有很多,比如 ASM,CGLIB。本文使用的是 ASM。先来介绍一下 class 文件的结构,class 文件包含了以下几类信息: 第一个是类的基本信息,包含了访问权限信息,类名信息,父类信息,接口信息。第二个是类的变量信息。第三个是方法的信息。 ASM 会先加载一个 class 文件,然后严格顺序读取类的各项信息,用户可以按照自己的意愿定义增强组件修改这些信息,最后输出成一个新的 class。 首先看一下如何利用 ASM 修改类信息。清单 1. 利用 ASM 修改字节码 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassReader cr = null; String enhancedClassName = classSource.getEnhancedName(); try { cr = new ClassReader(new FileInputStream( classSource.getFile())); } catch (IOException e) { e.printStackTrace(); return null; } ClassVisitor cv = new EnhancedModifier(cw, className.replace(".", "/"), enhancedClassName.replace(".", "/")); cr.accept(cv, 0); ASM 修改字节码文件的流程是一个责任链模式,首先使用一个 ClassReader 读入字节码,然后利用 ClassVisitor 做个性化的修改,最后利用 ClassWriter 输出修改后的字节码。 之前提过,需要将读取的 class 文件的类名做一些修改,加载成一个全新名字的派生类。这里将之分为了 2 个步骤。 第一步,先将原来的类变成接口。清单 2. 重定义的原始类 public Class redefineClass(String className){ ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassReader cr = null; ClassSource cs = classFiles.get(className); if(cs==null){ return null; } try { cr = new ClassReader(new FileInputStream(cs.getFile())); } catch (IOException e) { e.printStackTrace(); return null; } ClassModifier cm = new ClassModifier(cw); cr.accept(cm, 0); byte[] code = cw.toByteArray(); return defineClass(className, code, 0, code.length); } 首先 load 原始类的 class 文件,此处定义了一个增强组件 ClassModifier,作用是修改原始类的类型,将它转换成接口。原始类的所有方法逻辑都会被去掉。 第二步,生成的派生类都实现这个接口,即原始类,并且复制原始类中的所有方法逻辑。之后如果该类需要更新,会生成一个新的派生类,也会实现这个接口。这样做的目的是不论如何修改,同一个 class 的派生类都有一个共同的接口,他们之间的转换变得对外不透明。清单 3. 定义一个派生类 // 在 class 文件发生改变时重新定义这个类 private Class redefineClass(String className, ClassSource classSource){ ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassReader cr = null; classSource.update(); String enhancedClassName = classSource.getEnhancedName(); try { cr = new ClassReader( new FileInputStream(classSource.getFile())); } catch (IOException e) { e.printStackTrace(); return null; } EnhancedModifier em = new EnhancedModifier(cw, className.replace(".", "/"), enhancedClassName.replace(".", "/")); ExtendModifier exm = new ExtendModifier(em, className.replace(".", "/"), enhancedClassName.replace(".", "/")); cr.accept(exm, 0); byte[] code = cw.toByteArray(); classSource.setByteCopy(code); Class clazz = defineClass(enhancedClassName, code, 0, code.length); classSource.setClassCopy(clazz); return clazz; } 再次 load 原始类的 class 文件,此处定义了两个增强组件,一个是 EnhancedModifier,这个增强组件的作用是改变原有的类名。第二个增强组件是 ExtendModifier,这个增强组件的作用是改变原有类的父类,让这个修改后的派生类能够实现同一个原始类(此时原始类已经转成接口了)。 自定义 classloader 还有一个作用是监听会发生改变的 class 文件,classloader 会管理一个定时器,定时依次扫描这些 class 文件是否改变。 改变创建对象的行为 Java 虚拟机常见的创建对象的方法有两种,一种是静态创建,直接 new 一个对象,一种是动态创建,通过反射的方法,创建对象。 由于已经在自定义加载器中更改了原有类的类型,把它从类改成了接口,所以这两种创建方法都无法成立。我们要做的是将实例化原始类的行为变成实例化派生类。 对于第一种方法,需要做的是将静态创建,变为通过 classloader 获取 class,然后动态创建该对象。清单 4. 替换后的指令集所对应的逻辑 // 原始逻辑Greeter p = new Greeter();// 改变后的逻辑IGreeter p = (IGreeter)MyClassLoader.getInstance().findClass(“com.example.Greeter”).newInstance(); 这里又需要用到 ASM 来修改 class 文件了。查找到所有 new 对象的语句,替换成通过 classloader 的形式来获取对象的形式。 清单 5. 利用 ASM 修改方法体 @Override public void visitTypeInsn(int opcode, String type) { if(opcode==Opcodes.NEW && type.equals(className)){ List variables = node.localVariables; String compileType = null; for(int i=0;i
技术
# Java
酷游
1月22日
0
5
0
2025-01-22
《阿里巴巴Java开发手册-泰山版》提到的三目运算符的空指针问题到底是个怎么回事?
作者:Hollis 作者简介:Hollis(个人公众号Id:Hollis),一个对Coding有着独特追求的人,现任阿里巴巴技术专家,个人技术博主,技术文章全网阅读量数千万,《程序员的三门课》联合作者。 最近,阿里巴巴Java开发手册发布了最新版——泰山版,这个名字起的不错,一览众山小。 新版新增了30+规约,其中有一条规约引起了作者的关注,那就是手册中提到在三目运算符使用过程中,需要注意自动拆箱导致的NullPointerException(后文简称:NPE)问题:  因为这个问题我很久之前(2015年)遇到过,曾经在博客中也记录过,刚好最新的开发手册再次提到了这个知识点,于是把之前的文章内容翻出来并重新整理了一下,带大家一起回顾下这个知识点。 可能有些人看过我之前那篇文章,本文并不是单纯的”旧瓶装新酒”,在重新梳理这个知识点的时候,作者重新翻阅了《The Java Language Specification》,并且对比了Java SE 7 和 Java SE 8之后的相关变化,希望可以帮助大家更加全面的理解这个问题。 基础回顾 在详细展看介绍之前,先简单介绍下本文要涉及到的几个重要概念,分别是”三目运算符”、”自动拆装箱”等,如果大家对于这些历史知识有所掌握的话,可以先跳过本段内容,直接看问题重现部分即可。 三目运算符 在《The Java Language Specification》中,三目运算符的官方名称是 Conditional Operator ? : ,我一般称呼他为条件表达式,详细介绍在JLS 15.25中,这里简单介绍下其基本形式和用法: 三目运算符是Java语言中的重要组成部分,它也是唯一有3个操作数的运算符。形式为: ? : 以上,通过?、:组合的形式得到一个条件表达式。其中?运算符的含义是:先求表达式1的值,如果为真,则执行并返回表达式2的结果;如果表达式1的值为假,则执行并返回表达式3的结果。 值得注意的是,一个条件表达式从不会既计算,又计算。条件运算符是右结合的,也就是说,从右向左分组计算。例如,a?b:c?d:e将按a?b:(c?d:e)执行。 自动装箱与自动拆箱 介绍过了三目运算符(条件表达式)之后,我们再来简单介绍下Java中的自动拆装箱相关知识点。 每一个Java开发者一定都对Java中的基本数据类型不陌生,Java中共有8种基本数据类型,这些基础数据类型带来一个好处就是他们直接在栈内存中存储,不会在堆上分配内存,使用起来更加高效。 但是,Java语言是一个面向对象的语言,而基本数据类型不是对象,导致在实际使用过程中有诸多不便,如集合类要求其内部元素必须是Object类型,基本数据类型就无法使用。 所以,相对应的,Java提供了8种包装类型,更加方便在需要对象的地方使用。 有了基本数据类型和包装类,带来了一个麻烦就是需要在他们之间进行转换。在Java SE5中,为了减少开发人员的工作,Java提供了自动拆箱与自动装箱功能。 自动装箱: 就是将基本数据类型自动转换成对应的包装类。 自动拆箱:就是将包装类自动转换成对应的基本数据类型。 Integer i =10; //自动装箱 int b= i; //自动拆箱 我们可以简单理解为,当我们自己写的代码符合装(拆)箱规范的时候,编译器就会自动帮我们拆(装)箱。 自动装箱都是通过包装类的valueOf()方法来实现的.自动拆箱都是通过包装类对象的xxxValue()来实现的(如booleanValue()、longValue()等)。 问题重现 在最新版的开发手册中给出了一个例子,提示我们在使用三目运算符的过程中,可能会进行自动拆箱而导致NPE问题。 原文中的例子相对复杂一些,因为他还涉及到多个Integer相乘的结果是int的问题,我们举一个相对简单的一点的例子先来重现下这个问题: boolean flag = true; //设置成true,保证条件表达式的表达式二一定可以执行 boolean simpleBoolean = false; //定义一个基本数据类型的boolean变量 Boolean nullBoolean = null;//定义一个包装类对象类型的Boolean变量,值为null boolean x = flag ? nullBoolean : simpleBoolean; //使用三目运算符并给x变量赋值 以上代码,在运行过程中,会抛出NPE: Exception in thread "main" java.lang.NullPointerException 而且,这个和你使用的JDK版本是无关的,作者分别在JDK 6、JDK 8和JDK 14上做了测试,均会抛出NPE。 为了一探究竟,我们尝试对以上代码进行反编译,使用jad工具进行反编译后,得到以下代码: boolean flag = true; boolean simpleBoolean = false; Boolean nullBoolean = null; boolean x = flag ? nullBoolean.booleanValue() : simpleBoolean; 可以看到,反编译后的代码的最后一行,编译器帮我们做了一次自动拆箱,而就是因为这次自动拆箱,导致代码出现对于一个null对象(nullBoolean.booleanValue())的调用,导致了NPE。 那么,为什么编译器会进行自动拆箱呢?什么情况下需要进行自动拆箱呢? 原理分析 关于为什么编辑器会在代码编译阶段对于三目运算符中的表达式进行自动拆箱,其实在《The Java Language Specification》(后文简称JLS)的第15.25章节中是有相关介绍的。 在不同版本的JLS中,关于这部分描述虽然不尽相同,尤其在Java 8中有了大幅度的更新,但是其核心内容和原理是不变的。我们直接看Java SE 1.7 JLS中关于这部分的描述(因为1.7的表述更加简洁一些): The type of a conditional expression is determined as follows: • If the second and third operands have the same type (which may be the null type),then that is the type of the conditional expression. • If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T. 简单的来说就是:当第二位和第三位操作数的类型相同时,则三目运算符表达式的结果和这两位操作数的类型相同。当第二,第三位操作数分别为基本类型和该基本类型对应的包装类型时,那么该表达式的结果的类型要求是基本类型。 为了满足以上规定,又避免程序员过度感知这个规则,所以在编译过程中编译器如果发现三目操作符的第二位和第三位操作数的类型分别是基本数据类型(如boolean)以及该基本类型对应的包装类型(如Boolean)时,并且需要返回表达式为包装类型,那么就需要对该包装类进行自动拆箱。 在Java SE 1.8 JLS中,关于这部分描述又做了一些细分,再次把表达式区分成布尔型条件表达式(Boolean Conditional Expressions)、数值型条件表达式(Numeric Conditional Expressions)和引用类型条件表达式(Reference Conditional Expressions)。 并且通过表格的形式明确的列举了第二位和第三位分别是不同类型时得到的表达式结果值应该是什么,感兴趣的大家可以去翻阅一下。 其实简单总结下,就是:当第二位和第三位表达式都是包装类型的时候,该表达式的结果才是该包装类型,否则,只要有一个表达式的类型是基本数据类型,则表达式得到的结果都是基本数据类型。如果结果不符合预期,那么编译器就会进行自动拆箱。(即Java开发手册中总结的:只要表达式1和表达式2的类型有一个是基本类型,就会做触发类型对齐的拆箱操作,只不过如果都是基本类型也就不需要拆箱了。) 如下3种情况是我们熟知该规则,在声明表达式的结果的类型时刻意和规则保持一致的情况(为了帮助大家理解,我备注了注释和反编译后的代码): boolean flag = true; boolean simpleBoolean = false; Boolean objectBoolean = Boolean.FALSE; //当第二位和第三位表达式都是对象时,表达式返回值也为对象; Boolean x1 = flag ? objectBoolean : objectBoolean; //反编译后代码为:Boolean x1 = flag ? objectBoolean : objectBoolean; //因为x1的类型是对象,所以不需要做任何特殊操作。 //当第二位和第三位表达式都为基本类型时,表达式返回值也为基本类型; boolean x2 = flag ? simpleBoolean : simpleBoolean; //反编译后代码为:boolean x2 = flag ? simpleBoolean : simpleBoolean; //因为x2的类型也是基本类型,所以不需要做任何特殊操作。 //当第二位和第三位表达式中有一个为基本类型时,表达式返回值也为基本类型; boolean x3 = flag ? objectBoolean : simpleBoolean; //反编译后代码为:boolean x3 = flag ? objectBoolean.booleanValue() : simpleBoolean; //因为x3的类型是基本类型,所以需要对其中的包装类进行拆箱。 因为我们熟知三目运算符的规则,所以我们就会按照以上方式去定义x1、x2和x3的类型。 但是,并不是所有人都熟知这个规则,所以在实际应用中,还会出现以下三种定义方式: //当第二位和第三位表达式都是对象时,表达式返回值也为对象; boolean x4 = flag ? objectBoolean : objectBoolean; //反编译后代码为:boolean x4 = (flag ? objectBoolean : objectBoolean).booleanValue(); //因为x4的类型是基本类型,所以需要对表达式结果进行自动拆箱。 //当第二位和第三位表达式都为基本类型时,表达式返回值也为基本类型; Boolean x5 = flag ? simpleBoolean : simpleBoolean; //反编译后代码为:Boolean x5 = Boolean.valueOf(flag ? simpleBoolean : simpleBoolean); //因为x5的类型是对象类型,所以需要对表达式结果进行自动装箱。 //当第二位和第三位表达式中有一个为基本类型时,表达式返回值也为基本类型; Boolean x6 = flag ? objectBoolean : simpleBoolean; //反编译后代码为:Boolean x6 = Boolean.valueOf(flag ? objectBoolean.booleanValue() : simpleBoolean); //因为x6的类型是对象类型,所以需要对表达式结果进行自动装箱。 所以,日常开发中就有可能出现以上6种情况。聪明的读者们读到这里也一定想到了,在以上6种情况中,如果是涉及到自动拆箱的,一旦对象的值为null,就必然会发生NPE。 举例验证,我们把以上的x3、x4以及x6中的的对象类型设置成null,分别执行下代码: Boolean nullBoolean = null; boolean x3 = flag ? nullBoolean : simpleBoolean; boolean x4 = flag ? nullBoolean : objectBoolean; Boolean x6 = flag ? nullBoolean : simpleBoolean; 以上三种情况,都会在执行时发生NPE。 其中x3和x6是三目运算符运算过程中,根据JLS的规则确定类型的过程中要做自动拆箱而导致的NPE。由于使用了三目运算符,并且第二、第三位操作数分别是基本类型和对象。就需要对对象进行拆箱操作,由于该对象为null,所以在拆箱过程中调用null.booleanValue()的时候就报了NPE。 而x4是因为三目运算符运算结束后根据规则他得到的是一个对象类型,但是在给变量赋值过程中进行自动拆箱所导致的NPE。 小结 如前文介绍,在开发过程中,如果涉及到三目运算符,那么就要高度注意其中的自动拆装箱问题。 最好的做法就是保持三目运算符的第二位和第三位表达式的类型一致,并且如果要把三目运算符表达式给变量赋值的时候,也尽量保持变量的类型和他们保持一致。并且,做好单元测试!!! 所以,Java开发手册中提到要高度注意第二位和第三位表达式的类型对齐过程中由于自动拆箱发生的NPE问题,其实还需要注意使用三目运算符表达式给变量赋值的时候由于自动拆箱导致的NPE问题。 至此,我们已经介绍完了Java开发手册中关于三目运算符使用过程中可能会导致NPE的问题。 如果一定要给出一个方法论去避免这个问题的话,那么在使用的过程中,无论是三目运算符中的三个表达式,还是三目运算符表达式要赋值的变量,最好都使用包装类型,可以减少发生错误的概率。 正文内容已完,如果大家对这个问题还有更深的兴趣的话,接下来部分内容是扩展内容,也欢迎学习,不过这部分涉及到很多JLS的规范,如果实在看不懂也没关系~ 扩展思考 为了方便大家理解,我使用了简单的布尔类型的例子说明了NPE的问题。但是实际在代码开发中,遇到的场景可能并没有那么简单,比如说以下代码,大家猜一下能否正常执行: Map map = new HashMap(); Boolean b = (map!=null ? map.get("Hollis") : false); 如果你的答案是”不能,这里会抛NPE”那么说明你看懂了本文的内容,但是,我只能说你只是答对了一半。 因为以上代码,在小于JDK 1.8的版本中执行的结果是NPE,在JDK 1.8 及以后的版本中执行结果是null。 之所以会出现这样的不同,这个就说来话长了,我挑其中的重点内容简单介绍下吧,以下内容主要内容还是围绕Java 8 的JLS 。 JLS 15中对条件表达式(三目运算符)做了细分之后分为三种,区分方式: 如果表达式的第二个和第三个操作数都是布尔表达式,那么该条件表达式就是布尔表达式 如果表达式的第二个和第三个操作数都是数字型表达式,那么该条件表达式就是数字型表达式 除了以上两种以外的表达式就是引用表达式 因为Boolean b = (map!=null ? map.get("Hollis") : false);表达式中,第二位操作数为map.get("test"),虽然Map在定义的时候规定了其值类型为Boolean,但是在编译过程中泛型是会被擦除的(泛型的类型擦除),所以,其结果就是Object。那么根据以上规则判断,这个表达式就是引用表达式。 又跟据JLS15.25.3中规定: 如果引用条件表达式出现在赋值上下文或调用上下文中,那么条件表达式就是合成表达式 因为,Boolean b = (map!=null ? map.get("Hollis") : false);其实就是一个赋值上下文(关于赋值上下文相见JLS 5.2),所以map!=null ? map.get("Hollis") : false;就是合成表达式。 那么JLS15.25.3中对合成表达式的操作数类型做了约束: 合成的引用条件表达式的类型与其目标类型相同 所以,因为有了这个约束,编译器就可以推断(Java 8 中类型推断,详见JLS 18)出该表达式的第二个操作数和第三个操作数的结果应该都是Boolean类型。 所以,在编译过程中,就可以分别把他们都转成Boolean即可,那么以上代码在Java 8中反编译后内容如下: Boolean b = maps == null ? Boolean.valueOf(false) : (Boolean)maps.get("Hollis"); 但是在Java 7中可没有这些规定(Java 8之前的类型推断功能还很弱),编译器只知道表达式的第二位和第三位分别是基本类型和包装类型,而无法推断最终表达式类型。 那么他就会先根据JLS 15.25的规定,把返回值结果转换成基本类型。然后在进行变量赋值的时候,再转换成包装类型: Boolean b = Boolean.valueOf(maps == null ? false : ((Boolean)maps.get("Hollis")).booleanValue()); 所以,相比Java 8中多了一步自动拆箱,所以会导致NPE。 参考资料: 《Java开发手册——泰山版》 http://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.25 http://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.25 https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.2 https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.2.7 https://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html
技术
# Java
酷游
1月22日
0
7
0
2025-01-22
Java中的Switch对整型、字符型、字符串型的具体实现细节
Java 7中,switch的参数可以是String类型了,这对我们来说是一个很方便的改进。到目前为止switch支持这样几种数据类型:byte short int char String 。但是,作为一个程序员我们不仅要知道他有多么好用,还要知道它是如何实现的,witch对整型的支持是怎么实现的呢?对字符型是怎么实现的呢?String类型呢?有一点Java开发经验的人这个时候都会猜测switch对String的支持是使用equals()方法和hashcode()方法。那么到底是不是这两个方法呢?接下来我们就看一下,switch到底是如何实现的。 一、switch对整型支持的实现 下面是一段很简单的Java代码,定义一个int型变量a,然后使用switch语句进行判断。执行这段代码输出内容为5,那么我们将下面这段代码反编译,看看他到底是怎么实现的。 public class switchDemoInt { public static void main(String[] args) { int a = 5; switch (a) { case 1: System.out.println(1); break; case 5: System.out.println(5); break; default: break; } } } //output 5 反编译后的代码如下: public class switchDemoInt { public switchDemoInt() { } public static void main(String args[]) { int a = 5; switch(a) { case 1: // '\001' System.out.println(1); break; case 5: // '\005' System.out.println(5); break; } } } 我们发现,反编译后的代码和之前的代码比较除了多了两行注释以外没有任何区别,那么我们就知道,switch对int的判断是直接比较整数的值。 二、switch对字符型支持的实现 直接上代码: public class switchDemoInt { public static void main(String[] args) { char a = 'b'; switch (a) { case 'a': System.out.println('a'); break; case 'b': System.out.println('b'); break; default: break; } } } 编译后的代码如下: `public class switchDemoChar public class switchDemoChar { public switchDemoChar() { } public static void main(String args[]) { char a = 'b'; switch(a) { case 97: // 'a' System.out.println('a'); break; case 98: // 'b' System.out.println('b'); break; } } } 通过以上的代码作比较我们发现:对char类型进行比较的时候,实际上比较的是ascii码,编译器会把char型变量转换成对应的int型变量 三、switch对字符串支持的实现 还是先上代码: public class switchDemoString { public static void main(String[] args) { String str = "world"; switch (str) { case "hello": System.out.println("hello"); break; case "world": System.out.println("world"); break; default: break; } } } 对代码进行反编译: public class switchDemoString { public switchDemoString() { } public static void main(String args[]) { String str = "world"; String s; switch((s = str).hashCode()) { default: break; case 99162322: if(s.equals("hello")) System.out.println("hello"); break; case 113318802: if(s.equals("world")) System.out.println("world"); break; } } } 看到这个代码,你知道原来字符串的switch是通过equals()和hashCode()方法来实现的。记住,switch中只能使用整型,比如byte。short,char(ackii码是整型)以及int。还好hashCode()方法返回的是int,而不是long。通过这个很容易记住hashCode返回的是int这个事实。仔细看下可以发现,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。因为Java编译器只增加了一个equals方法,如果你比较的是字符串字面量的话会非常快,比如”abc” ==”abc”。如果你把hashCode()方法的调用也考虑进来了,那么还会再多一次的调用开销,因为字符串一旦创建了,它就会把哈希值缓存起来。因此如果这个siwtch语句是用在一个循环里的,比如逐项处理某个值,或者游戏引擎循环地渲染屏幕,这里hashCode()方法的调用开销其实不会很大。 好,以上就是关于switch对整型、字符型、和字符串型的支持的实现方式,总结一下我们可以发现,其实swich只支持一种数据类型,那就是整型,其他数据类型都是转换成整型之后在使用switch的。
技术
# Java
酷游
1月22日
0
4
0
2025-01-22
[译]Java虚拟机是如何执行线程同步的
想介绍下synchronized的原理,但是又不知道从何下手,在网上看到一篇老外的文章,介绍了和线程同步相关的几个基础知识点。所以想把它翻译一下给大家看看。相信看过这些基础知识之后再看我后面要写的synchronized的原理就会好理解一点了。 原文地址:How the Java virtual machine performs thread synchronization 了解Java语言的人都知道,Java代码要想被JVM执行,需要被转换成由字节码组成的class文件。本文主要来分析下Java虚拟机是如何在字节码层面上执行线程同步的。 线程和共享数据 Java编程语言的优点之一是它在语言层面上对多线程的支持。这种支持大部分集中在协调多个线程对共享数据的访问上。JVM的内存结构主要包含以下几个重要的区域:栈、堆、方法区等。 在Java虚拟中,每个线程独享一块栈内存,其中包括局部变量、线程调用的每个方法的参数和返回值。其他线程无法读取到该栈内存块中的数据。栈中的数据仅限于基本类型和对象引用。所以,在JVM中,栈上是无法保存真实的对象的,只能保存对象的引用。真正的对象要保存在堆中。 在JVM中,堆内存是所有线程共享的。堆中只包含对象,没有其他东西。所以,堆上也无法保存基本类型和对象引用。堆和栈分工明确。但是,对象的引用其实也是对象的一部分。这里值得一提的是,数组是保存在堆上面的,即使是基本类型的数据,也是保存在堆中的。因为在Java中,数组是对象。 除了栈和堆,还有一部分数据可能保存在JVM中的方法区中,比如类的静态变量。方法区和栈类似,其中只包含基本类型和对象应用。和栈不同的是,方法区中的静态变量可以被所有线程访问到。 对象和类的锁 如前文提到,JVM中有两块内存区域可以被所有线程共享: 堆,上面存放着所有对象 方法区,上面存放着静态变量 那么,如果有多个线程想要同时访问同一个对象或者静态变量,就需要被管控,否则可能出现不可预期的结果。 为了协调多个线程之间的共享数据访问,虚拟机给每个对象和类都分配了一个锁。这个锁就像一个特权,在同一时刻,只有一个线程可以“拥有”这个类或者对象。如果一个线程想要获得某个类或者对象的锁,需要询问虚拟机。当一个线程向虚拟机申请某个类或者对象的锁之后,也许很快或者也许很慢虚拟机可以把锁分配给这个线程,同时这个线程也许永远也无法获得锁。当线程不再需要锁的时候,他再把锁还给虚拟机。这时虚拟机就可以再把锁分配给其他申请锁的线程。 类锁其实通过对象锁实现的。因为当虚拟机加载一个类的时候,会会为这个类实例化一个 java.lang.Class 对象,当你锁住一个类的时候,其实锁住的是其对应的Class 对象。 监视器(Monitors) 监视器和锁同时被JVM使用(我理解作者的意思应该是想说锁其实是通过监视器实现的。),监视器主要功能是监控一段代码,确保在同一时间只有一个线程在执行。 每个监视器都与一个对象相关联。当线程执行到监视器监视下的代码块中的第一条指令时,线程必须获取对被引用对象的锁定。在线程获取锁之前,他是无法执行这段代码的,一旦获得锁,线程便可以进入“被保护”的代码开始执行。 当线程离开代码块的时候,无论如何离开,都会释放所关联对象的锁。 多次加锁 同一个线程可以对同一个对象进行多次加锁。每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。 同步 在Java中,当有多个线程都必须要对同一个共享数据进行访问时,有一种协调方式叫做同步。Java语言提供了两种内置方式来使线程同步的访问数据:同步代码块和同步方法。 这篇文章中后面还介绍了同步代码块和同步方法,以及简单的介绍了下实现方式。这里就不做翻译了,因为我觉得他介绍的太简单了。我后面专门写篇文章详细介绍。
技术
# Java
酷游
1月22日
0
4
0
2025-01-22
Java代码的编译与反编译那些事儿
编程语言 在介绍编译和反编译之前,我们先来简单介绍下编程语言(Programming Language)。编程语言(Programming Language)分为低级语言(Low-level Language)和高级语言(High-level Language)。 机器语言(Machine Language)和汇编语言(Assembly Language)属于低级语言,直接用计算机指令编写程序。 而C、C++、Java、Python等属于高级语言,用语句(Statement)编写程序,语句是计算机指令的抽象表示。 举个例子,同样一个语句用C语言、汇编语言和机器语言分别表示如下: 计算机只能对数字做运算,符号、声音、图像在计算机内部都要用数字表示,指令也不例外,上表中的机器语言完全由十六进制数字组成。最早的程序员都是直接用机器语言编程,但是很麻烦,需要查大量的表格来确定每个数字表示什么意思,编写出来的程序很不直观,而且容易出错,于是有了汇编语言,把机器语言中一组一组的数字用助记符(Mnemonic)表示,直接用这些助记符写出汇编程序,然后让汇编器(Assembler)去查表把助记符替换成数字,也就把汇编语言翻译成了机器语言。 但是,汇编语言用起来同样比较复杂,后面,就衍生出了Java、C、C++等高级语言。 什么是编译 上面提到语言有两种,一种低级语言,一种高级语言。可以这样简单的理解:低级语言是计算机认识的语言、高级语言是程序员认识的语言。 那么如何从高级语言转换成低级语言呢?这个过程其实就是编译。 从上面的例子还可以看出,C语言的语句和低级语言的指令之间不是简单的一一对应关系,一条a=b+1;语句要翻译成三条汇编或机器指令,这个过程称为编译(Compile),由编译器(Compiler)来完成,显然编译器的功能比汇编器要复杂得多。用C语言编写的程序必须经过编译转成机器指令才能被计算机执行,编译需要花一些时间,这是用高级语言编程的一个缺点,然而更多的是优点。首先,用C语言编程更容易,写出来的代码更紧凑,可读性更强,出了错也更容易改正。 将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序的过程就是编译。负责这一过程的处理的工具叫做编译器 现在我们知道了什么是编译,也知道了什么是编译器。不同的语言都有自己的编译器,Java语言中负责编译的编译器是一个命令:javac javac是收录于JDK中的Java语言编译器。该工具可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。 当我们写完一个HelloWorld.java文件后,我们可以使用javac HelloWorld.java命令来生成HelloWorld.class文件,这个class类型的文件是JVM可以识别的文件。通常我们认为这个过程叫做Java语言的编译。其实,class文件仍然不是机器能够识别的语言,因为机器只能识别机器语言,还需要JVM再将这种class文件类型字节码转换成机器可以识别的机器语言。 什么是反编译 反编译的过程与编译刚好相反,就是将已编译好的编程语言还原到未编译的状态,也就是找出程序语言的源代码。就是将机器看得懂的语言转换成程序员可以看得懂的语言。Java语言中的反编译一般指将class文件转换成java文件。 有了反编译工具,我们可以做很多事情,最主要的功能就是有了反编译工具,我们就能读得懂Java编译器生成的字节码。如果你想问读懂字节码有啥用,那么我可以很负责任的告诉你,好处大大的。比如我的博文几篇典型的原理性文章,都是通过反编译工具得到反编译后的代码分析得到的。如深入理解多线程(一)——Synchronized的实现原理、深度分析Java的枚举类型—-枚举的线程安全性及序列化问题、Java中的Switch对整型、字符型、字符串型的具体实现细节、Java的类型擦除等。我最近在GitChat写了一篇关于Java语法糖的文章,其中大部分内容都用到反编译工具来洞悉语法糖背后的原理。 Java反编译工具 本文主要介绍3个Java的反编译工具:javap、jad和cfr javap javap是jdk自带的一个工具,可以对代码反编译,也可以查看java编译器生成的字节码。javap和其他两个反编译工具最大的区别是他生成的文件并不是java文件,也不像其他两个工具生成代码那样更容易理解。拿一段简单的代码举例,如我们想分析Java 7中的switch是如何支持String的,我们先有以下可以编译通过的源代码: public class switchDemoString { public static void main(String[] args) { String str = "world"; switch (str) { case "hello": System.out.println("hello"); break; case "world": System.out.println("world"); break; default: break; } } } 执行以下两个命令: javac switchDemoString.java javap -c switchDemoString.class 生成代码如下: public class com.hollis.suguar.switchDemoString { public com.hollis.suguar.switchDemoString(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String world 2: astore_1 3: aload_1 4: astore_2 5: iconst_m1 6: istore_3 7: aload_2 8: invokevirtual #3 // Method java/lang/String.hashCode:()I 11: lookupswitch { // 2 99162322: 36 113318802: 50 default: 61 } 36: aload_2 37: ldc #4 // String hello 39: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 42: ifeq 61 45: iconst_0 46: istore_3 47: goto 61 50: aload_2 51: ldc #2 // String world 53: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 56: ifeq 61 59: iconst_1 60: istore_3 61: iload_3 62: lookupswitch { // 2 0: 88 1: 99 default: 110 } 88: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 91: ldc #4 // String hello 93: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 96: goto 110 99: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 102: ldc #2 // String world 104: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 107: goto 110 110: return } 我个人的理解,javap并没有将字节码反编译成java文件,而是生成了一种我们可以看得懂字节码。其实javap生成的文件仍然是字节码,只是程序员可以稍微看得懂一些。如果你对字节码有所掌握,还是可以看得懂以上的代码的。其实就是把String转成hashcode,然后进行比较。 个人认为,一般情况下我们会用到javap命令的时候不多,一般只有在真的需要看字节码的时候才会用到。但是字节码中间暴露的东西是最全的,你肯定有机会用到,比如我在分析synchronized的原理的时候就有是用到javap。通过javap生成的字节码,我发现synchronized底层依赖了ACC_SYNCHRONIZED标记和monitorenter、monitorexit两个指令来实现同步。 jad jad是一个比较不错的反编译工具,只要下载一个执行工具,就可以实现对class文件的反编译了。还是上面的源代码,使用jad反编译后内容如下: 命令:jad switchDemoString.class public class switchDemoString { public switchDemoString() { } public static void main(String args[]) { String str = "world"; String s; switch((s = str).hashCode()) { default: break; case 99162322: if(s.equals("hello")) System.out.println("hello"); break; case 113318802: if(s.equals("world")) System.out.println("world"); break; } } } 看,这个代码你肯定看的懂,因为这不就是标准的java的源代码么。这个就很清楚的可以看到原来字符串的switch是通过equals()和hashCode()方法来实现的。 但是,jad已经很久不更新了,在对Java7生成的字节码进行反编译时,偶尔会出现不支持的问题,在对Java 8的lambda表达式反编译时就彻底失败。 CFR jad很好用,但是无奈的是很久没更新了,所以只能用一款新的工具替代他,CFR是一个不错的选择,相比jad来说,他的语法可能会稍微复杂一些,但是好在他可以work。 如,我们使用cfr对刚刚的代码进行反编译。执行一下命令: java -jar cfr_0_125.jar switchDemoString.class --decodestringswitch false 得到以下代码: public class switchDemoString { public static void main(String[] arrstring) { String string; String string2 = string = "world"; int n = -1; switch (string2.hashCode()) { case 99162322: { if (!string2.equals("hello")) break; n = 0; break; } case 113318802: { if (!string2.equals("world")) break; n = 1; } } switch (n) { case 0: { System.out.println("hello"); break; } case 1: { System.out.println("world"); break; } } } } 通过这段代码也能得到字符串的switch是通过equals()和hashCode()方法来实现的结论。 相比Jad来说,CFR有很多参数,还是刚刚的代码,如果我们使用以下命令,输出结果就会不同: java -jar cfr_0_125.jar switchDemoString.class public class switchDemoString { public static void main(String[] arrstring) { String string; switch (string = "world") { case "hello": { System.out.println("hello"); break; } case "world": { System.out.println("world"); break; } } } } 所以--decodestringswitch表示对于switch支持string的细节进行解码。类似的还有--decodeenumswitch、--decodefinally、--decodelambdas等。在我的关于语法糖的文章中,我使用--decodelambdas对lambda表达式警进行了反编译。 源码: public static void main(String... args) { List strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com"); strList.forEach( s -> { System.out.println(s); } ); } java -jar cfr_0_125.jar lambdaDemo.class --decodelambdas false反编译后代码: public static /* varargs */ void main(String ... args) { ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com"); strList.forEach((Consumer)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)()); } private static /* synthetic */ void lambda$main$0(String s) { System.out.println(s); } CFR还有很多其他参数,均用于不同场景,读者可以使用java -jar cfr_0_125.jar --help进行了解。这里不逐一介绍了。 如何防止反编译 由于我们有工具可以对Class文件进行反编译,所以,对开发人员来说,如何保护Java程序就变成了一个非常重要的挑战。但是,魔高一尺、道高一丈。当然有对应的技术可以应对反编译咯。但是,这里还是要说明一点,和网络安全的防护一样,无论做出多少努力,其实都只是提高攻击者的成本而已。无法彻底防治。 典型的应对策略有以下几种: 隔离Java程序 让用户接触不到你的Class文件 对Class文件进行加密 提到破解难度 代码混淆 将代码转换成功能上等价,但是难于阅读和理解的形式
技术
# Java
酷游
1月22日
0
9
0
上一页
1
...
29
30
31
...
39
下一页
易航博客