Java 知识杂记

来之不易的美团面试,结果居然挂了...(附面试答案)


阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作

我们使用的增强for循环,其实是Java提供的语法糖,其实现原理是借助Iterator进行元素的遍历。

但是如果在遍历过程中,不通过Iterator,而是通过集合类自身的方法对集合进行添加/删除操作。那么在Iterator进行下一次的遍历时,经检测发现有一次集合的修改操作并未通过自身进行,那么可能是发生了并发被其他线程执行的,这时候就会抛出异常,来提示用户可能发生了并发修改,这就是所谓的fail-fast机制。

Java Class文件编译的版本号与JDK版本号的对应关系

JDK版本号|    Class版本号|   16进制  
1.1|    45.0|   00 00 00 2D  
1.2|    46.0|   00 00 00 2E  
1.3|    47.0|   00 00 00 2F  
1.4|    48.0|   00 00 00 30  
1.5|    49.0|   00 00 00 31  
1.6|    50.0|   00 00 00 32  
1.7|    51.0|   00 00 00 33  
1.8|    52.0|   00 00 00 34  

线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

  双亲委派模型并不能解决所有的类加载器问题,比如,Java 提供了很多服务提供者接口,允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、JAXP 等,这些 SPI 的接口由核心类库提供,却由第三方实现,这样就存在一个问题
SPI 的接口是 Java 核心库的一部分,是由 BootstrapClassLoader 加载的;SPI 实现的 Java 类一般是由 AppClassLoader 来加载的。BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给 AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。

线程上下文类加载器( ContextClassLoader)正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是 AppClassLoader。在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。

线程上下文类加载器在很多 SPI 的实现中都会用到。但在 JDBC 中,你可能会看到一种更直接的实现方式,比如,JDBC 驱动管理 java.sql.DriverManager 中的 loadInitialDrivers()方法中,你可以直接看到 JDK 是如何加载驱动的:

for (String aDriver : driversList) {  
    try {
        println("DriverManager.Initialize: loading " + aDriver);
        // 直接使用 AppClassLoader
        Class.forName(aDriver, true,
                ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
        println("DriverManager.Initialize: load failed: " + ex);
    }
}

getClassLoader() 和 getContextClassLoader() 的区别

  • getClassLoader() 是当前类加载器,而 getContextClassLoader 是当前线程的类加载器
  • getClassLoader 是使用双亲委派模型来加载类的,而 getContextClassLoader 就是为了避开双亲委派模型的加载方式的,也就是说它不是用这种方式来加载类

当前类加载器加载和定义当前方法所属的那个类。这个类加载器在你使用带单个参数的 Class.forName() 方法、Class.getResource() 方法和相似方法时会在运行时类的链接过程中被隐式调用(也就是说当你用Class.forname(package.className)的时候已经调用当前类加载器来加载这个类了)

Java 程序 CPU 分析、内存、IO、网络等分析

jvm 分析

# 反编译(查看编译器生成的字节码)
javap -c -v ClassName  
# 查看某个进程内部线程占用情况分析
top -H -p PID  
# pstack显示每个进程的栈跟踪
pstack PID  
# 垃圾回收统计
jstat -gc PID  
# 查看jstack信息
jstack PID  
jstack PID | grep 1731c -A90  

CPU 分析

CPU 繁忙:线程中有无限空循环、无阻塞、正则匹配或者单纯的计算;发生了频繁的 GC;多线程的上下文切换;JIT 编译

  • 首先使用 top、vmstat、ps 等命令定位 CPU 使用率高的线程:top -p [processId] -H
  • jstack [pid] 打印繁忙进程的堆栈信息
  • 通过 printf %0x [processId] 转换进程 id 为 16 进制,在堆栈信息中查找对应的堆栈信息。
  • jstat -gcutil [pid] 查看 GC 的情况是否正常,是否 GC 引起了 CPU 飙高。
  • JVM 加入 -XX:+PrintCompilation 参数,查看是否是 JIT 编译引起了 CPU 飙高。

内存 分析

内存使用不当:频繁 GC,响应缓慢;OOM,堆内存、永久代内存、本地线程内存

  • 堆外内存: JNI、Deflater/Inflater、DirectByteBuffer。通过 vmstat、top、pidstat 等查看 swap 和物理内存的消耗状况。通过 Google-preftools 来追踪 JNI、Deflater 这种调度的资源使用情况。
  • 堆内存:创建的对象、全局集合、缓存、ClassLoader、多线程
    • 查看 JVM 内存使用情况:jmap -heap <pid>
    • 查看 JVM 存活的对象:jmap -histo:live <pid>
    • 把 heap 里所有对象都 dump 下来,无论对象是否死活:jmap -dump:format=b,file=xxx.hprof <pid>
    • 先做一次 Full GC 再 dump,只包含存活的对象信息:jmap -dump:format=b,live,file=xxx.hprof <pid>
    • 使用 Eclipse MAT 或者 jhat 打开堆 dump 的文件,根据内存中的具体对象使用情况分析
    • VJTools 中的 vjmap 可以分代打印出堆内存的对象实例占用信息。

磁盘 IO 分析

IO 性能差,大量的随机读写,设备慢,文件太大。

  • iostat -xz l 查看磁盘 IO 情况
  • r/s, w/s, rkB/s, wkB/s 等指标过大,可能会引起性能问题。
  • await 过大,可能是硬件设备遇到了瓶颈或者出现故障。一次 IO 操作一般超过20ms就说明磁盘压 力过大。
  • avgqu-sz 大于1,可能是硬件设备已经饱和。
  • %util 越大表示磁盘越繁忙,100% 表示已经饱和。
  • 通过使用 strace 工具定位对文件 IO 的系统调用。

需要安装linux性能优化工具包:sysstat

yum install -y sysstat  

网络 IO 分析

  • netstat -anpt查看网络连接状况。当TIMEWAIT或者CLOSEWAIT连接过多时,会影响应 用的响应速度。前者需要优化内核参数,后者一般是代码Bug没有释放网络连接。
  • 使用tcpdump来具体分析网络 IO 的数据。tcpdump出的文件直接打开是一堆二进制的数据,可 以使用Wireshark查看具体的连接以及其中数据的内容。tcpdump -i eth0 -w tmp.cap -tnn dst port 8080
  • sar -n DEV 查看否吐率和否吐数据包数,判断是否超过两卡限制。

IO 分析 Tips

  • %iowait在 Linux 的计算为 CPU 空闲、并且有仍未完成的 IO 请求的时间占总时间的比例。
  • %iowait升高并不一定代表 IO 设备有瓶颈。需要结合其他指标来判断,如 await (IO 操作等待耗时)、svctm(IO 操作服务耗时)等。
  • avgqu-sz是按照单位时间的平均值,所以不能反映瞬间的 IO 洪水。

CPU 使用优化

  • 不要存在一直运行的线程(无限循环),可以使用 Sleep 休眠一段时间。这种情况带着存在于一些 pull 方式消费数据的场景下,当一次 pull 没有拿到数据的时候建议 sleep 一下,再做下一次 pull。
  • 轮询的时候可以使用wait/notity机制代替循环。
  • 避免正则表达式匹配、过多的计算。例如,避免使用String的format、split、replace方法; 避免免使用正则去判断邮箱格式(有时候会造成死循环);避免序列/反序列化。
  • 使用线程池,减少线程数以及线程的切换。
  • 多线程对于锁的竞争可以考虑减小锁的拉度(使用Reetrantllock)、拆分锁(类似 ConcurrentHashMap 分 bucket 上锁),或者使用 CAS、ThreadLocal、不可变对象等无锁技术。此外,多线程代码的编写最好使用JDK提供的并发包、Executors框架以及ForkJoin等,此外外Disruptor和Actor在合适的场景也可以使用。
  • 结合JVM和代码一起进行分析,避免产生频繁的CC,尤英是Full GC。

内存使用优化

  • 使用基本数据类型而不是其包装类型能够节省内存。
  • 尽量避免分配大对象。大对象分配的代价以及初始化的代价很大,不同大小的大对象可能导致 Java 堆碎片,尤其是 CMS;
  • 避免改变数据结构大小。如避免改变数组或orray backed collections/containers的大小;对象构建(初始化)时最好显式批量定教组大小;改变大小导致不必要的对象分配,可能导致 Java 堆碎片。
  • 避免保存重复的 String 对象,同时也需要小心 String.substringt() 和 String.intern() 的使用,中间过程会生成不少字符串。
  • 尽量不要使用 finalizer。
  • 释放不必要的引用:Threadlocal 使用完记得释放以防止内存泄露,各种 stream 使用完也记得 close。
  • 使用对象注池避免无节制创建对象,造成频繁 CC,但也不要随便使用对象池,除非像连接池、线程池这种初始化/创建资源消耗较大的场景。
  • 缓存失效算法,可以考虑使用 SoftReference、WeakReference 保存缓存对象。
  • 谨慎热部署/加载的使用,尤其是动态加载类等。
  • 打印日志时不要输出文件名、行号,因为日志框架一般都是通过打印线程堆栈实现,生成大量 String。 此外,打印日志时,先判断对应级别的日志是否打开再做操作,否则也会生成大量 String。

IO 使用优化

  • 考虑使用异步写入代替同步写入,可以借鉴Redis的AOP机制。
  • 利用预读取或者缓存,减少随机读。
  • 尽量批量写入,减少 IO 次数和寻址。
  • 使用数据库代替文件存储。
  • 使用异步 IO /多路复用 IO /事件驱动 IO 代替同步阻塞 IO。
  • 使用协程提高网络 IO 性能:Quasar。

JVM 配置

  • 合理设置各个代的大小。新生代尽量设置的大,但不能过大(会产生碎片),同样也要避免Survivor设置的过大和过小。
  • 选择合适的 GC 策略。需要根据不同的场景选择合适的 GC 策略。CMS 并非全能的。除非特别需要再设置,毕竟 CMS 的新生代回收策略 ParNew 并非最快的,且会产生碎片。此外,G1 直到 JDK8 的出现也并没有得到广泛应用,并不建议使用。
  • 老年代优先使用 Parallel GC (-XX:+UserParallel[Old]GC),可以保证最大的吞吐量。由于 CMS 会产生碎片,确实有必要才改成 CMS 或 G1。
  • 注意内存强(严重阻碍处理器性能发挥的内存瓶颈),一般来讲单点应用对内存设置为4G到5G即可,依靠可扩展性提高并发能力。
  • 设置 JVM 的内存大小有个经验法则:完成 Full GC 后,应该释放出来 70% 的内存。
  • 配置堆内存和永久代/元空间内存之和小于 32GB ,从而可以使用压缩指针节省对象指针的占用。
  • 打开 GC 日志并读懂 GC 日志,以便于排查问题。GC的日志文件可以通过 GC Histogram (gchisto) 生成图表和表格。

代码性能建议

  • 算法、逻辑上是程序性能的首要,遇到性能问题,应该首先优化程序的逻辑处理
  • 优先考虑使用返回值而不是异常表示错误。虽然现代 JVM 已经做了大量优化工作,但毕竟异常是有代价的,需要在合适的地方使用。一般用错误码返回值处理可能会发生的事情,用异常捕捉处理不期望发生的事情。如果使用异常并且比较关注性能,可以通过覆盖掉异常类的fillInStackTrace()方法为空方法,使其不拷贝栈信息。
  • 查看自己的代码是否对内联是友好的,内联友好指的方法的大小不超过 35 字节(默认的内联阈值,不建议修改)、非虚方法(虚方法指的是在运行期才能确定执行对象的方法,最新的 JVM 对非虚方法会通过 CHA 类层次分析来判断是否可以内联)。

协程

协程,英文 Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

Java语言并没有对协程的原生支持,但是某些开源框架模拟出了协程的功能,例如:Kilim框架。

Spring AOP

  • 如果被代理的目标对象实现了接口,那么Spring会默认使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。
  • 如果是被代理类的方法自调用,在自调用的过程中,是类自身的调用,而不是代理对象去调用,那么就不会产生 AOP,因为这样Spring就不能把你的代码织入到约定的流程中。
  • 需要代理的对象方法不能是private的,因为Spring不管使用的是JDK动态代理还是CGLIB动态代理,一个是针对接口实现的类,一个是通过子类实现。无论是接口还是父类,显然都不能出现 private 方法,否则子类或实现类都不能覆盖到。如果方法为private,那么在代理过程中,根本找不到这个方法,引起代理对象创建出现问题,也就可能会导致有的对象没有注入成功。

【JVM】12_空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

JDK 6.0.24以后就认为HandlePromotionFailure一直是true(只有定义,未使用)。


漫画说算法--动态规划算法三

三个核心元素:最优子结构、边界、状态转移方程式

1.问题建模 -- 状态转移方程
2.问题求解
爬楼梯问题:
- 递归时间复杂度太高 O(2^N) - 备忘录算法(暂存计算结果)时间复杂度:O(N) 空间复杂度:O(N) (自顶向下计算) - 动态规划求解 时间复杂度:O(N) 空间复杂度:O(1) (自底向上计算,不需要保留全部临时结果)

国王和金矿问题:
当金矿只有5座的时候,动态规划的性能优势还没有体现出来。当金矿有10座,甚至更多的时候,动态规划就明显具备了优势。
由于动态规划的时间和空间复杂度都和工人数量成正比,而简单递归却和工人数量无关,所以当工人数量很多时,动态规划算法反而不如递归。 3.


RocketMQ

RocketMQ 无法避免消息重复,如果业务对消费重复非常敏感【需要消费过程要做到幂等(即消费端去重)】


总结在多线程中几种释放锁和不释放锁的操作

不释放锁

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep(Long l)、Thread.yield()方法暂停当前线程的执行
  • 线程执行同步代码块时,其它线程调用该线程suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)
  • 尽量避免使用suspend()和resume()来控制线程

释放锁

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程的同步方法、同步代码块遇到break、return终止该代码块、该方法的继续执行
  • 当前线程的同步方法、同步代码块中出现了未处理Error和Exception,导致异常结束
  • 当前线程在同步方法、同步代码块中执行了线程对象的wait()方法,当前线程暂停,并释放锁

Log4j NDC MDC 区别及用法

NDC(Nested Diagnostic Context)MDC(Mapped Diagnostic Context)是log4j中非常有用的两个类,它们用于存储应用程序的上下文信息(context infomation),从而便于在log中使用这些上下文信息。

NDC

采用了一个类似栈的机制来pushpop上下文信息,每一个线程都独立地储存上下文信息。比如说一个servlet就可以针对每一个request创建对应的NDC,储存客户端地址等等信息。
当使用的时候,我们要尽可能确保在进入一个context的时候,把相关的信息使用NDC.push(message);在离开这个context的时候使用NDC.pop()将信息删除。另外由于设计上的一些问题,还需要保证在当前thread结束的时候使用NDC.remove()清除内存,否则会产生内存泄漏的问题。
存储了上下文信息之后,我们就可以在log的时候将信息输出。在相应的PatternLayout中使用”%x”来输出存储的上下文信息,下面是一个PatternLayout的例子:

log4j.appender.console.layout.ConversionPattern=%-d{yyyy/MM/dd HH:mm:ss,SSS} [%X] -[%c]-[%p] %m%n  

使用NDC最重要的好处就是,当我们想输出一些上下文的信息的时候,不需要让logger去寻找这些信息,而只需要在适当的位置进行存储,然后再配置文件中修改PatternLayout。在最新的log4j 1.3版本中增加了一个org.apache.log4j.filters.NDCMatchFilter,用来根据NDC中存储的信息接受或拒绝一条log信息。

MDC

MDC和NDC非常相似,所不同的是MDC内部使用了类似map的机制来存储信息,上下文信息也是每个线程独立地储存,所不同的是信息都是以它们的key值存储在”map”中。相对应的方法,MDC.put(key, value); MDC.remove(key); MDC.get(key);
在配置PatternLayout的时候使用:%x{key}来输出对应的value。

log4j.appender.console.layout.ConversionPattern=%-d{yyyy/MM/dd HH:mm:ss,SSS} [%X{ip}] -[%c]-[%p] %m%n  

如果在项目中有过滤器,你可以把获取ip 的方法直接定义在过滤器中,然后在配置文件中配置获取ip的显示就可以了。
同样地,MDC也有一个org.apache.log4j.filters.MDCMatchFilter。这里需要注意的一点,MDC是线程独立的,但是一个子线程会自动获得一个父线程MDC的copy。

至于选择NDC还是MDC要看需要存储的上下文信息是堆栈式的还是key/value形式的。

Java对BIO、NIO、AIO的支持:

  • Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
  • Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
  • Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,

BIO、NIO、AIO适用场景分析:

  • BIO:方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
  • NIO:方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
  • AIO:方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

一般来说I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO

  • 同步阻塞IO:在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式!
  • 同步非阻塞IO:在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。
  • 异步阻塞IO:此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!
  • 异步非阻塞IO:在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。目前Java中还没有支持此种IO模型。

java中右移运算符 >> 和无符号右移运算符 >>> 的区别

  • 左移<< :就是该数对应二进制码整体左移,左边超出的部分舍弃,右边补零。

    举个例子:253的二进制码1111 1101,在经过运算253<<2后得到1111 0100。很简单

  • 右移>> :该数对应的二进制码整体右移,左边的用原有标志位补充(正数补0负数补1),右边超出的部分舍弃。

  • 无符号右移>>> :不管正负标志位为0还是1,将该数的二进制码整体右移,左边部分总是以0填充,右边部分舍弃。

    因为左移始终是在右边补,不会产生符号问题,所以没有必要存在 无符号左移 <<<(Java中也不存在)。

举例对比:

-5 用二进制表示 1111 1011,加粗为该数标志位
-5 >> 2 : 1111 1011 --------------> 1111 1110。
11为标志位补充的 -5 >>> 2 : 1111 1011--------------> 0011 1110。
00为补充的0

负数的二进制表示

为什么-x=!x+1???

其中x为一任意int型正整数,左式表示取x的相反数后的二进制形式,右式表示先将x的二进制按位取反后再加一得到的二进制形式。

假设有一个 int 类型的数,值为5,那么,我们知道它在计算机中表示为:

00000000 00000000 00000000 00000101  

5转换成二制是101,不过int类型的数占用4字节(32位),所以前面填了一堆0。
现在想知道,-5在计算机中如何表示?
在计算机中,负数以原码的补码形式表达。
什么叫补码呢?这得从原码,反码说起。

原码:

一个正数,按照绝对值大小转换成的二进制数;一个负数按照绝对值大小转换成的二进制数,然后最高位补1,称为原码。 比如:

00000000 00000000 00000000 00000101 是 5的 原码。  
10000000 00000000 00000000 00000101 是 -5的 原码。  

反码:

*正数的反码与原码相同,负数的反码为对该数的原码除符号位外各位取反。 *

取反操作指:原为1,得0;原为0,得1。(1变0; 0变1)

比如:

正数 00000000 00000000 00000000 00000101 的反码
还是 00000000 00000000 00000000 00000101 

负数 10000000 00000000 00000000 00000101 每一位取反(除符号位),
得到 11111111 11111111 11111111 11111010。 

称:

11111111 11111111 11111111 11111010 是  
10000000 00000000 00000000 00000101 的反码。  

反码是相互的,所以也可称:

10000000 00000000 00000000 00000101 和  
11111111 11111111 11111111 11111010 互为反码。  

补码:

*正数的补码与原码相同,负数的补码为对该数的原码除符号位外各位取反,然后在最后一位加1. *

比如:

10000000 00000000 00000000 00000101 的反码是:  
11111111 11111111 11111111 11111010。  

那么,补码为:

11111111 11111111 11111111 11111010 + 1 =  
11111111 11111111 11111111 11111011  

所以,-5 在计算机中表达为:11111111 11111111 11111111 11111011。转换为十六进制:0xFFFFFFFB

再举一例,我们来看整数-1在计算机中如何表示。 假设这也是一个int类型,那么:

1、先取原码:10000000 00000000 00000000 00000001  
2、得反码:  11111111 11111111 11111111 11111110(除符号位按位取反)  
3、得补码:  11111111 11111111 11111111 11111111  

可见,-1在计算机里用二进制表达就是全1。16进制为:0xFFFFFF

正零和负零的补码相同
[+0]原码=0000 0000, [-0]原码=1000 0000
[+0]反码=0000 0000, [-0]反码=1111 1111
[+0]补码=0000 0000, [-0]补码=0000 0000

再看看这个规律表

   原码        补码       值
0111 1111   0111 1111   +127  
0111 1110   0111 1110   +126  
     ...   .. 补码不断-1...
0000 0000   0000 0000     0  
1000 0001   1111 1111    -1  
1000 0010   1111 1110    -2  
1000 0011   1111 1101    -3  
    ...    . 补码不断-1...
1111 1111   1000 0001   -127  
无法表达      1000 0000   -128

于是就有了规定 1000 0000 定为 -128的补码,这种定法和数学层面的表述是一致的。